From a09983ae35713f5a2bbb100981116d31ce99826e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Jul 2020 12:26:25 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-2-stable-ee --- spec/frontend/__mocks__/@gitlab/ui.js | 21 +- .../__mocks__/document-register-element/index.js | 1 + spec/frontend/__mocks__/monaco-editor/index.js | 2 + .../components/alert_management_detail_spec.js | 31 +- .../alert_management_empty_state_spec.js | 54 + .../components/alert_management_list_spec.js | 489 ------- .../alert_management_list_wrapper_spec.js | 57 + .../alert_management_sidebar_todo_spec.js | 76 + .../alert_management_system_note_spec.js | 34 - .../components/alert_management_table_spec.js | 590 ++++++++ .../alert_managment_sidebar_assignees_spec.js | 133 -- .../components/alert_metrics_spec.js | 67 + .../components/alert_sidebar_spec.js | 55 - .../components/alert_sidebar_status_spec.js | 107 -- .../alert_managment_sidebar_assignees_spec.js | 154 ++ .../components/sidebar/alert_sidebar_spec.js | 64 + .../sidebar/alert_sidebar_status_spec.js | 129 ++ .../alert_management_system_note_spec.js | 38 + spec/frontend/alert_management/mocks/alerts.json | 4 +- .../__snapshots__/alert_settings_form_spec.js.snap | 48 + .../alert_settings/alert_settings_form_spec.js | 233 +++ .../components/alerts_service_form_spec.js | 14 + spec/frontend/api_spec.js | 95 ++ spec/frontend/awards_handler_spec.js | 292 ++-- .../stores/modules/batch_comments/actions_spec.js | 28 +- spec/frontend/behaviors/copy_as_gfm_spec.js | 5 +- .../behaviors/gl_emoji/unicode_support_map_spec.js | 52 - spec/frontend/behaviors/gl_emoji_spec.js | 110 ++ .../behaviors/shortcuts/shortcuts_issuable_spec.js | 2 +- .../blob_header_filepath_spec.js.snap | 1 + .../blob/components/blob_content_error_spec.js | 6 +- .../components/blob_header_default_actions_spec.js | 5 +- .../components/blob_header_viewer_switcher_spec.js | 6 +- spec/frontend/blob/components/mock_data.js | 14 + spec/frontend/blob_edit/edit_blob_spec.js | 31 + spec/frontend/boards/components/board_form_spec.js | 5 +- spec/frontend/boards/issue_card_spec.js | 2 +- .../components/ci_key_field_spec.js | 244 ---- .../components/ci_variable_modal_spec.js | 73 +- .../ci_variable_list/services/mock_data.js | 2 + .../ci_variable_list/store/actions_spec.js | 68 +- .../ci_variable_list/store/mutations_spec.js | 78 +- spec/frontend/close_reopen_report_toggle_spec.js | 7 +- .../components/remove_cluster_confirmation_spec.js | 17 +- .../components/ancestor_notice_spec.js | 51 + .../clusters_list/components/clusters_spec.js | 59 +- spec/frontend/clusters_list/store/actions_spec.js | 63 +- .../frontend/clusters_list/store/mutations_spec.js | 60 + .../components/__snapshots__/popover_spec.js.snap | 110 +- .../code_navigation/components/popover_spec.js | 35 +- .../frontend/code_navigation/store/actions_spec.js | 24 +- spec/frontend/code_navigation/utils/index_spec.js | 2 +- .../cycle_analytics/stage_nav_item_spec.js | 3 +- .../design_notes/design_discussion_spec.js | 2 +- .../design_management/pages/design/index_spec.js | 33 +- .../frontend/design_management/pages/index_spec.js | 2 +- .../design_management/utils/tracking_spec.js | 28 +- .../__snapshots__/design_note_pin_spec.js.snap | 42 + .../__snapshots__/design_presentation_spec.js.snap | 104 ++ .../__snapshots__/design_scaler_spec.js.snap | 115 ++ .../components/__snapshots__/image_spec.js.snap | 68 + .../components/delete_button_spec.js | 51 + .../components/design_note_pin_spec.js | 49 + .../__snapshots__/design_note_spec.js.snap | 67 + .../__snapshots__/design_reply_form_spec.js.snap | 15 + .../design_notes/design_discussion_spec.js | 322 +++++ .../components/design_notes/design_note_spec.js | 170 +++ .../design_notes/design_reply_form_spec.js | 184 +++ .../design_notes/toggle_replies_widget_spec.js | 98 ++ .../components/design_overlay_spec.js | 410 ++++++ .../components/design_presentation_spec.js | 553 +++++++ .../components/design_scaler_spec.js | 67 + .../components/design_sidebar_spec.js | 236 +++ .../design_management_new/components/image_spec.js | 133 ++ .../list/__snapshots__/item_spec.js.snap | 472 ++++++ .../components/list/item_spec.js | 168 +++ .../toolbar/__snapshots__/index_spec.js.snap | 63 + .../__snapshots__/pagination_button_spec.js.snap | 28 + .../toolbar/__snapshots__/pagination_spec.js.snap | 29 + .../components/toolbar/index_spec.js | 123 ++ .../components/toolbar/pagination_button_spec.js | 61 + .../components/toolbar/pagination_spec.js | 79 + .../upload/__snapshots__/button_spec.js.snap | 85 ++ .../__snapshots__/design_dropzone_spec.js.snap | 501 +++++++ .../design_version_dropdown_spec.js.snap | 141 ++ .../components/upload/button_spec.js | 59 + .../components/upload/design_dropzone_spec.js | 151 ++ .../upload/design_version_dropdown_spec.js | 114 ++ .../components/upload/mock_data/all_versions.js | 14 + .../mock_data/all_versions.js | 8 + .../design_management_new/mock_data/design.js | 74 + .../design_management_new/mock_data/designs.js | 17 + .../design_management_new/mock_data/no_designs.js | 11 + .../design_management_new/mock_data/notes.js | 46 + .../pages/__snapshots__/index_spec.js.snap | 317 ++++ .../pages/design/__snapshots__/index_spec.js.snap | 216 +++ .../pages/design/index_spec.js | 294 ++++ .../design_management_new/pages/index_spec.js | 571 ++++++++ spec/frontend/design_management_new/router_spec.js | 70 + .../utils/cache_update_spec.js | 44 + .../utils/design_management_utils_spec.js | 176 +++ .../utils/error_messages_spec.js | 62 + .../design_management_new/utils/tracking_spec.js | 59 + spec/frontend/diffs/components/app_spec.js | 55 + .../diffs/components/diff_expansion_cell_spec.js | 4 +- .../diffs/components/diff_file_header_spec.js | 1 + .../diffs/components/diff_file_row_spec.js | 11 + spec/frontend/diffs/components/diff_file_spec.js | 3 + .../diffs/components/diff_gutter_avatars_spec.js | 2 +- .../diffs/components/diff_line_note_form_spec.js | 16 +- .../diffs/components/diff_table_cell_spec.js | 15 +- .../diffs/components/inline_diff_table_row_spec.js | 53 +- spec/frontend/diffs/components/no_changes_spec.js | 8 + .../components/parallel_diff_table_row_spec.js | 28 +- spec/frontend/diffs/store/actions_spec.js | 82 +- spec/frontend/diffs/store/utils_spec.js | 22 + spec/frontend/editor/editor_lite_spec.js | 70 + spec/frontend/editor/editor_markdown_ext_spec.js | 204 +++ spec/frontend/emoji/emoji_spec.js | 382 +++++ .../emoji/support/unicode_support_map_spec.js | 52 + spec/frontend/emoji_spec.js | 485 ------- spec/frontend/environment.js | 7 +- spec/frontend/environments/emtpy_state_spec.js | 16 - .../components/error_details_spec.js | 12 +- .../frontend/filtered_search/dropdown_user_spec.js | 8 +- .../filtered_search_manager_spec.js | 4 +- .../stores/recent_searches_store_spec.js | 9 + .../filtered_search/visual_token_value_spec.js | 3 +- spec/frontend/fixtures/branches.rb | 50 +- spec/frontend/fixtures/commit.rb | 49 +- spec/frontend/fixtures/emojis.rb | 17 + spec/frontend/fixtures/metrics_dashboard.rb | 12 +- spec/frontend/fixtures/services.rb | 2 +- .../fixtures/static/global_search_input.html | 15 - .../fixtures/static/mini_dropdown_graph.html | 24 +- .../fixtures/static/search_autocomplete.html | 15 + spec/frontend/fixtures/tags.rb | 28 + spec/frontend/gfm_auto_complete_spec.js | 2 +- spec/frontend/gl_form_spec.js | 18 + spec/frontend/global_search_input_spec.js | 215 --- spec/frontend/helpers/event_hub_factory_spec.js | 142 +- .../helpers/fake_request_animation_frame.js | 13 + spec/frontend/helpers/init_vue_mr_page_helper.js | 46 + spec/frontend/helpers/monitor_helper_spec.js | 9 - spec/frontend/helpers/test_constants.js | 22 +- spec/frontend/helpers/vue_mock_directive.js | 20 +- spec/frontend/helpers/wait_using_real_timer.js | 7 + spec/frontend/ide/commit_icon_spec.js | 1 - .../ide/components/ide_status_list_spec.js | 8 +- .../jobs/__snapshots__/stage_spec.js.snap | 2 +- spec/frontend/ide/components/repo_editor_spec.js | 113 +- spec/frontend/ide/helpers.js | 1 - spec/frontend/ide/lib/editor_spec.js | 22 + spec/frontend/ide/services/index_spec.js | 6 +- spec/frontend/ide/stores/actions/file_spec.js | 361 +++-- .../ide/stores/actions/merge_request_spec.js | 1 + spec/frontend/ide/stores/actions/tree_spec.js | 3 +- spec/frontend/ide/utils_spec.js | 52 + .../helpers/comment_indicator_helper_spec.js | 3 +- .../image_diff/helpers/utils_helper_spec.js | 3 +- spec/frontend/image_diff/image_diff_spec.js | 3 +- .../image_diff/replaced_image_diff_spec.js | 7 +- .../frontend/import_projects/store/actions_spec.js | 27 +- .../__snapshots__/alerts_form_spec.js.snap | 99 ++ .../incidents_settings_tabs_spec.js.snap | 63 + .../__snapshots__/pagerduty_form_spec.js.snap | 89 ++ .../components/alerts_form_spec.js | 49 + .../components/incidents_settings_service_spec.js | 55 + .../components/incidents_settings_tabs_spec.js | 55 + .../components/pagerduty_form_spec.js | 67 + .../edit/components/active_toggle_spec.js | 18 +- .../edit/components/dynamic_field_spec.js | 242 +++- .../edit/components/integration_form_spec.js | 94 +- .../edit/components/jira_issues_fields_spec.js | 96 ++ .../edit/components/jira_trigger_fields_spec.js | 25 +- .../edit/components/trigger_fields_spec.js | 51 +- spec/frontend/integrations/edit/mock_data.js | 18 + .../integrations/edit/store/actions_spec.js | 19 + .../integrations/edit/store/getters_spec.js | 71 + .../integrations/edit/store/mutations_spec.js | 19 + .../frontend/integrations/edit/store/state_spec.js | 26 + .../issuable_suggestions/components/app_spec.js | 4 +- .../issuable_suggestions/components/item_spec.js | 5 +- spec/frontend/issuable_suggestions/mock_data.js | 8 +- .../components/issuable_list_root_app_spec.js | 24 +- .../issuables_list/components/issuable_spec.js | 167 ++- .../components/issuables_list_app_spec.js | 148 +- .../components/issuable_header_warnings_spec.js | 79 + .../issue_show/components/pinned_links_spec.js | 15 +- .../__snapshots__/jira_import_form_spec.js.snap | 277 ++++ .../jira_import/components/jira_import_app_spec.js | 208 +-- .../components/jira_import_form_spec.js | 179 ++- .../components/jira_import_progress_spec.js | 7 +- .../components/jira_import_setup_spec.js | 4 +- spec/frontend/jira_import/mock_data.js | 53 + .../jira_import/utils/jira_import_utils_spec.js | 59 + spec/frontend/jobs/components/job_app_spec.js | 148 +- spec/frontend/jobs/components/job_log_spec.js | 65 - .../components/log/collapsible_section_spec.js | 4 +- spec/frontend/jobs/store/mutations_spec.js | 25 +- spec/frontend/jobs/store/utils_spec.js | 4 +- spec/frontend/lib/utils/common_utils_spec.js | 47 - spec/frontend/lib/utils/datetime_utility_spec.js | 26 + spec/frontend/lib/utils/dom_utils_spec.js | 50 +- spec/frontend/lib/utils/grammar_spec.js | 12 +- spec/frontend/lib/utils/text_markdown_spec.js | 108 +- spec/frontend/lib/utils/text_utility_spec.js | 50 + spec/frontend/lib/utils/url_utility_spec.js | 8 + .../logs/components/environment_logs_spec.js | 2 + .../logs/components/log_control_buttons_spec.js | 8 +- spec/frontend/logs/mock_data.js | 15 + spec/frontend/logs/stores/actions_spec.js | 27 + spec/frontend/logs/stores/mutations_spec.js | 35 + spec/frontend/merge_request_tabs_spec.js | 2 +- .../__snapshots__/dashboard_template_spec.js.snap | 55 +- .../__snapshots__/empty_state_spec.js.snap | 68 +- .../monitoring/components/charts/anomaly_spec.js | 133 +- .../monitoring/components/charts/column_spec.js | 10 +- .../components/charts/single_stat_spec.js | 54 +- .../components/charts/time_series_spec.js | 95 +- .../components/create_dashboard_modal_spec.js | 48 + .../monitoring/components/dashboard_header_spec.js | 232 +++ .../monitoring/components/dashboard_panel_spec.js | 59 +- .../monitoring/components/dashboard_spec.js | 237 ++- .../components/dashboard_template_spec.js | 4 +- .../components/dashboard_url_time_spec.js | 5 +- .../components/dashboards_dropdown_spec.js | 184 +-- .../components/duplicate_dashboard_modal_spec.js | 111 ++ .../monitoring/components/empty_state_spec.js | 23 +- .../monitoring/components/graph_group_spec.js | 126 +- .../monitoring/components/links_section_spec.js | 2 +- .../monitoring/components/refresh_button_spec.js | 143 ++ .../components/variables/custom_variable_spec.js | 52 - .../components/variables/dropdown_field_spec.js | 65 + .../components/variables/text_field_spec.js | 59 + .../components/variables/text_variable_spec.js | 59 - .../components/variables_section_spec.js | 63 +- spec/frontend/monitoring/fixture_data.js | 40 +- spec/frontend/monitoring/graph_data.js | 164 +++ spec/frontend/monitoring/mock_data.js | 572 +++----- .../monitoring/pages/dashboard_page_spec.js | 36 +- spec/frontend/monitoring/router_spec.js | 81 ++ spec/frontend/monitoring/store/actions_spec.js | 1510 +++++++++++--------- spec/frontend/monitoring/store/getters_spec.js | 40 +- spec/frontend/monitoring/store/mutations_spec.js | 154 +- spec/frontend/monitoring/store/utils_spec.js | 297 +++- .../monitoring/store/variable_mapping_spec.js | 263 +++- spec/frontend/monitoring/store_utils.js | 32 +- spec/frontend/monitoring/utils_spec.js | 55 +- .../frontend/namespace_storage_limit_alert_spec.js | 36 - .../components/multiline_comment_utils_spec.js | 64 +- .../frontend/notes/components/note_actions_spec.js | 64 +- spec/frontend/notes/components/note_form_spec.js | 18 + .../notes/components/noteable_note_spec.js | 71 +- .../notes/mixins/discussion_navigation_spec.js | 6 + spec/frontend/notes/old_notes_spec.js | 44 +- spec/frontend/notes/stores/actions_spec.js | 133 +- spec/frontend/notes/stores/mutation_spec.js | 34 + spec/frontend/pager_spec.js | 7 +- .../jobs/index/components/stop_jobs_modal_spec.js | 5 +- .../labels/components/promote_label_modal_spec.js | 5 +- .../components/delete_milestone_modal_spec.js | 5 +- .../components/promote_milestone_modal_spec.js | 5 +- .../new/components/fork_groups_list_item_spec.js | 78 + .../forks/new/components/fork_groups_list_spec.js | 133 ++ .../pages/projects/graphs/code_coverage_spec.js | 6 +- spec/frontend/pages/projects/graphs/mock_data.js | 91 +- .../components/interval_pattern_input_spec.js | 121 +- spec/frontend/persistent_user_callout_spec.js | 66 + spec/frontend/pipelines/blank_state_spec.js | 2 +- .../dag/__snapshots__/dag_graph_spec.js.snap | 44 +- .../components/dag/dag_annotations_spec.js | 112 ++ .../pipelines/components/dag/dag_graph_spec.js | 4 +- spec/frontend/pipelines/components/dag/dag_spec.js | 171 ++- .../frontend/pipelines/components/dag/mock_data.js | 80 ++ .../components/pipelines_filtered_search_spec.js | 2 +- spec/frontend/pipelines/empty_state_spec.js | 2 +- spec/frontend/pipelines/graph/job_item_spec.js | 13 +- .../pipelines/graph/linked_pipeline_spec.js | 28 +- .../pipelines/graph/linked_pipelines_mock_data.js | 21 + spec/frontend/pipelines/nav_controls_spec.js | 2 +- spec/frontend/pipelines/pipeline_triggerer_spec.js | 2 +- spec/frontend/pipelines/pipeline_url_spec.js | 116 +- spec/frontend/pipelines/pipelines_actions_spec.js | 2 +- .../frontend/pipelines/pipelines_artifacts_spec.js | 2 +- spec/frontend/pipelines/pipelines_spec.js | 14 +- .../frontend/pipelines/pipelines_table_row_spec.js | 2 +- spec/frontend/pipelines/pipelines_table_spec.js | 2 +- spec/frontend/pipelines/stage_spec.js | 2 +- .../pipelines/test_reports/stores/actions_spec.js | 109 +- .../pipelines/test_reports/stores/getters_spec.js | 15 +- .../test_reports/stores/mutations_spec.js | 34 +- .../pipelines/test_reports/test_reports_spec.js | 71 +- .../test_reports/test_suite_table_spec.js | 11 +- .../pipelines/test_reports/test_summary_spec.js | 2 +- spec/frontend/pipelines/time_ago_spec.js | 2 +- .../tokens/pipeline_branch_name_token_spec.js | 2 +- .../pipelines/tokens/pipeline_status_token_spec.js | 2 +- .../tokens/pipeline_tag_name_token_spec.js | 2 +- .../tokens/pipeline_trigger_author_token_spec.js | 2 +- spec/frontend/polyfills/element_spec.js | 46 - .../projects/commits/store/actions_spec.js | 4 +- .../__snapshots__/remove_modal_spec.js.snap | 126 ++ .../projects/components/remove_modal_spec.js | 62 + .../pipelines_area_chart_spec.js.snap | 2 +- spec/frontend/projects/project_new_spec.js | 3 +- .../components/service_desk_root_spec.js | 226 +++ .../components/service_desk_setting_spec.js | 234 +++ .../services/service_desk_service_spec.js | 129 ++ spec/frontend/ref/components/ref_selector_spec.js | 532 +++++++ spec/frontend/ref/stores/actions_spec.js | 180 +++ spec/frontend/ref/stores/getters_spec.js | 36 + spec/frontend/ref/stores/mutations_spec.js | 274 ++++ .../explorer/components/delete_button_spec.js | 73 + .../components/details_page/details_row_spec.js | 43 + .../components/details_page/empty_tags_state.js | 43 - .../details_page/empty_tags_state_spec.js | 43 + .../components/details_page/tags_list_row_spec.js | 330 +++++ .../components/details_page/tags_list_spec.js | 146 ++ .../components/details_page/tags_table_spec.js | 286 ---- .../registry/explorer/components/list_item_spec.js | 156 ++ .../__snapshots__/group_empty_state_spec.js.snap | 5 +- .../__snapshots__/project_empty_state_spec.js.snap | 95 +- .../components/list_page/image_list_row_spec.js | 35 +- spec/frontend/registry/explorer/mock_data.js | 10 +- .../registry/explorer/pages/details_spec.js | 53 +- spec/frontend/registry/explorer/stubs.js | 17 +- .../registry_settings_app_spec.js.snap | 18 - .../components/registry_settings_app_spec.js | 11 +- .../settings/components/settings_form_spec.js | 69 +- .../expiration_policy_fields_spec.js.snap | 20 +- .../components/expiration_policy_fields_spec.js | 75 +- spec/frontend/releases/components/app_new_spec.js | 26 + .../components/release_block_assets_spec.js | 32 +- .../components/codequality_issue_body_spec.js | 62 + .../grouped_codequality_reports_app_spec.js | 146 ++ .../reports/codequality_report/mock_data.js | 90 ++ .../codequality_report/store/actions_spec.js | 151 ++ .../codequality_report/store/getters_spec.js | 95 ++ .../codequality_report/store/mutations_spec.js | 80 ++ .../store/utils/codequality_comparison_spec.js | 139 ++ .../components/grouped_test_reports_app_spec.js | 45 +- .../reports/components/report_section_spec.js | 102 +- .../reports/components/summary_row_spec.js | 43 +- .../table/__snapshots__/row_spec.js.snap | 55 + .../repository/components/table/index_spec.js | 15 +- .../repository/components/table/row_spec.js | 16 + .../repository/components/web_ide_link_spec.js | 51 + spec/frontend/repository/utils/dom_spec.js | 3 +- spec/frontend/search_autocomplete_spec.js | 284 ++++ .../__snapshots__/self_monitor_form_spec.js.snap | 4 +- .../components/self_monitor_form_spec.js | 3 +- .../confidential_issue_sidebar_spec.js.snap | 6 + .../sidebar/confidential/edit_form_buttons_spec.js | 130 +- .../sidebar/confidential/edit_form_spec.js | 2 + .../sidebar/confidential_issue_sidebar_spec.js | 28 +- spec/frontend/snippets/components/edit_spec.js | 179 +-- spec/frontend/snippets/components/show_spec.js | 35 +- .../snippets/components/snippet_blob_edit_spec.js | 137 +- .../snippets/components/snippet_blob_view_spec.js | 38 +- .../snippets/components/snippet_header_spec.js | 19 +- .../components/edit_area_spec.js | 41 +- spec/frontend/static_site_editor/mock_data.js | 7 + .../services/parse_source_file_spec.js | 92 +- .../services/submit_content_changes_spec.js | 48 +- .../components/approvals/approvals_spec.js | 391 +++++ .../approvals/approvals_summary_optional_spec.js | 57 + .../components/approvals/approvals_summary_spec.js | 93 ++ .../components/mr_widget_author_spec.js | 54 +- .../mr_widget_expandable_section_spec.js | 65 + .../components/mr_widget_header_spec.js | 35 + .../components/mr_widget_pipeline_spec.js | 300 ++-- .../components/mr_widget_suggest_pipeline_spec.js | 86 +- .../components/mr_widget_terraform_plan_spec.js | 107 -- .../components/pipeline_tour_mock_data.js | 7 + .../states/mr_widget_auto_merge_enabled_spec.js | 9 + .../components/states/mr_widget_checking_spec.js | 2 +- .../states/mr_widget_pipeline_tour_spec.js | 143 -- .../states/mr_widget_ready_to_merge_spec.js | 44 +- .../states/mr_widget_squash_before_merge_spec.js | 42 + .../components/states/pipeline_tour_mock_data.js | 10 - .../components/terraform/mock_data.js | 31 + .../mr_widget_terraform_container_spec.js | 172 +++ .../components/terraform/terraform_plan_spec.js | 95 ++ spec/frontend/vue_mr_widget/mock_data.js | 15 +- .../vue_mr_widget/mr_widget_options_spec.js | 6 + .../vue_mr_widget/stores/get_state_key_spec.js | 8 +- .../__snapshots__/awards_list_spec.js.snap | 63 +- .../resizable_chart_container_spec.js.snap | 21 - .../vue_shared/components/file_icon_spec.js | 27 +- .../filtered_search_bar_root_spec.js | 60 +- .../components/filtered_search_bar/mock_data.js | 23 + .../tokens/author_token_spec.js | 23 +- .../vue_shared/components/gl_modal_vuex_spec.js | 28 + .../issue/__snapshots__/issue_warning_spec.js.snap | 62 - .../components/issue/issue_assignees_spec.js | 2 +- .../components/issue/issue_milestone_spec.js | 2 +- .../components/issue/issue_warning_spec.js | 105 -- .../components/issue/related_issuable_item_spec.js | 3 +- .../components/issue/related_issuable_mock_data.js | 18 +- .../markdown/suggestion_diff_header_spec.js | 25 +- .../__snapshots__/noteable_warning_spec.js.snap | 58 + .../components/notes/noteable_warning_spec.js | 196 +++ .../project_selector/project_list_item_spec.js | 10 + .../components/remove_member_modal_spec.js | 65 + .../resizable_chart_container_spec.js.snap | 21 + .../__snapshots__/skeleton_loader_spec.js.snap | 324 +++++ .../resizable_chart_container_spec.js | 63 + .../resizable_chart/skeleton_loader_spec.js | 55 + .../components/resizable_chart_container_spec.js | 63 - .../rich_content_editor/editor_service_spec.js | 62 +- .../modals/add_image/add_image_modal_spec.js | 76 + .../modals/add_image/upload_image_tab_spec.js | 41 + .../modals/add_image_modal_spec.js | 41 - .../rich_content_editor_spec.js | 43 +- .../services/build_custom_renderer_spec.js | 29 + .../build_html_to_markdown_renderer_spec.js | 50 + .../renderers/build_uneditable_token_spec.js | 88 ++ .../services/renderers/mock_data.js | 58 + .../renderers/render_embedded_ruby_spec.js | 30 + .../render_font_awesome_html_inline_spec.js | 33 + .../services/renderers/render_html_block_spec.js | 38 + .../render_identifier_instance_text_spec.js | 55 + .../renderers/render_identifier_paragraph_spec.js | 65 + .../renderers/render_kramdown_list_spec.js | 55 + .../renderers/render_kramdown_text_spec.js | 30 + .../sidebar/labels_select/dropdown_button_spec.js | 3 +- .../labels_select/dropdown_search_input_spec.js | 6 +- .../labels_select_vue/dropdown_button_spec.js | 62 +- .../dropdown_contents_labels_view_spec.js | 14 + .../labels_select_vue/labels_select_root_spec.js | 29 +- .../labels_select_vue/store/getters_spec.js | 21 +- .../components/user_popover/user_popover_spec.js | 46 +- spec/frontend/wikis_spec.js | 30 + 434 files changed, 25968 insertions(+), 7363 deletions(-) create mode 100644 spec/frontend/__mocks__/document-register-element/index.js create mode 100644 spec/frontend/alert_management/components/alert_management_empty_state_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_management_list_spec.js create mode 100644 spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js create mode 100644 spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_management_system_note_spec.js create mode 100644 spec/frontend/alert_management/components/alert_management_table_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js create mode 100644 spec/frontend/alert_management/components/alert_metrics_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_sidebar_spec.js delete mode 100644 spec/frontend/alert_management/components/alert_sidebar_status_spec.js create mode 100644 spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js create mode 100644 spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js create mode 100644 spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js create mode 100644 spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js create mode 100644 spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap create mode 100644 spec/frontend/alert_settings/alert_settings_form_spec.js delete mode 100644 spec/frontend/behaviors/gl_emoji/unicode_support_map_spec.js create mode 100644 spec/frontend/behaviors/gl_emoji_spec.js create mode 100644 spec/frontend/blob_edit/edit_blob_spec.js delete mode 100644 spec/frontend/ci_variable_list/components/ci_key_field_spec.js create mode 100644 spec/frontend/clusters_list/components/ancestor_notice_spec.js create mode 100644 spec/frontend/clusters_list/store/mutations_spec.js create mode 100644 spec/frontend/design_management_new/components/__snapshots__/design_note_pin_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/__snapshots__/design_presentation_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/__snapshots__/design_scaler_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/__snapshots__/image_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/delete_button_spec.js create mode 100644 spec/frontend/design_management_new/components/design_note_pin_spec.js create mode 100644 spec/frontend/design_management_new/components/design_notes/__snapshots__/design_note_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/design_notes/__snapshots__/design_reply_form_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js create mode 100644 spec/frontend/design_management_new/components/design_notes/design_note_spec.js create mode 100644 spec/frontend/design_management_new/components/design_notes/design_reply_form_spec.js create mode 100644 spec/frontend/design_management_new/components/design_notes/toggle_replies_widget_spec.js create mode 100644 spec/frontend/design_management_new/components/design_overlay_spec.js create mode 100644 spec/frontend/design_management_new/components/design_presentation_spec.js create mode 100644 spec/frontend/design_management_new/components/design_scaler_spec.js create mode 100644 spec/frontend/design_management_new/components/design_sidebar_spec.js create mode 100644 spec/frontend/design_management_new/components/image_spec.js create mode 100644 spec/frontend/design_management_new/components/list/__snapshots__/item_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/list/item_spec.js create mode 100644 spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_button_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/toolbar/index_spec.js create mode 100644 spec/frontend/design_management_new/components/toolbar/pagination_button_spec.js create mode 100644 spec/frontend/design_management_new/components/toolbar/pagination_spec.js create mode 100644 spec/frontend/design_management_new/components/upload/__snapshots__/button_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/upload/__snapshots__/design_version_dropdown_spec.js.snap create mode 100644 spec/frontend/design_management_new/components/upload/button_spec.js create mode 100644 spec/frontend/design_management_new/components/upload/design_dropzone_spec.js create mode 100644 spec/frontend/design_management_new/components/upload/design_version_dropdown_spec.js create mode 100644 spec/frontend/design_management_new/components/upload/mock_data/all_versions.js create mode 100644 spec/frontend/design_management_new/mock_data/all_versions.js create mode 100644 spec/frontend/design_management_new/mock_data/design.js create mode 100644 spec/frontend/design_management_new/mock_data/designs.js create mode 100644 spec/frontend/design_management_new/mock_data/no_designs.js create mode 100644 spec/frontend/design_management_new/mock_data/notes.js create mode 100644 spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap create mode 100644 spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap create mode 100644 spec/frontend/design_management_new/pages/design/index_spec.js create mode 100644 spec/frontend/design_management_new/pages/index_spec.js create mode 100644 spec/frontend/design_management_new/router_spec.js create mode 100644 spec/frontend/design_management_new/utils/cache_update_spec.js create mode 100644 spec/frontend/design_management_new/utils/design_management_utils_spec.js create mode 100644 spec/frontend/design_management_new/utils/error_messages_spec.js create mode 100644 spec/frontend/design_management_new/utils/tracking_spec.js create mode 100644 spec/frontend/editor/editor_markdown_ext_spec.js create mode 100644 spec/frontend/emoji/emoji_spec.js create mode 100644 spec/frontend/emoji/support/unicode_support_map_spec.js delete mode 100644 spec/frontend/emoji_spec.js create mode 100644 spec/frontend/fixtures/emojis.rb delete mode 100644 spec/frontend/fixtures/static/global_search_input.html create mode 100644 spec/frontend/fixtures/static/search_autocomplete.html create mode 100644 spec/frontend/fixtures/tags.rb delete mode 100644 spec/frontend/global_search_input_spec.js create mode 100644 spec/frontend/helpers/fake_request_animation_frame.js create mode 100644 spec/frontend/helpers/init_vue_mr_page_helper.js create mode 100644 spec/frontend/helpers/wait_using_real_timer.js create mode 100644 spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap create mode 100644 spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap create mode 100644 spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap create mode 100644 spec/frontend/incidents_settings/components/alerts_form_spec.js create mode 100644 spec/frontend/incidents_settings/components/incidents_settings_service_spec.js create mode 100644 spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js create mode 100644 spec/frontend/incidents_settings/components/pagerduty_form_spec.js create mode 100644 spec/frontend/integrations/edit/components/jira_issues_fields_spec.js create mode 100644 spec/frontend/integrations/edit/mock_data.js create mode 100644 spec/frontend/integrations/edit/store/actions_spec.js create mode 100644 spec/frontend/integrations/edit/store/getters_spec.js create mode 100644 spec/frontend/integrations/edit/store/mutations_spec.js create mode 100644 spec/frontend/integrations/edit/store/state_spec.js create mode 100644 spec/frontend/issue_show/components/issuable_header_warnings_spec.js create mode 100644 spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap delete mode 100644 spec/frontend/jobs/components/job_log_spec.js create mode 100644 spec/frontend/monitoring/components/create_dashboard_modal_spec.js create mode 100644 spec/frontend/monitoring/components/dashboard_header_spec.js create mode 100644 spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js create mode 100644 spec/frontend/monitoring/components/refresh_button_spec.js delete mode 100644 spec/frontend/monitoring/components/variables/custom_variable_spec.js create mode 100644 spec/frontend/monitoring/components/variables/dropdown_field_spec.js create mode 100644 spec/frontend/monitoring/components/variables/text_field_spec.js delete mode 100644 spec/frontend/monitoring/components/variables/text_variable_spec.js create mode 100644 spec/frontend/monitoring/graph_data.js create mode 100644 spec/frontend/monitoring/router_spec.js delete mode 100644 spec/frontend/namespace_storage_limit_alert_spec.js create mode 100644 spec/frontend/pages/projects/forks/new/components/fork_groups_list_item_spec.js create mode 100644 spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js create mode 100644 spec/frontend/pipelines/components/dag/dag_annotations_spec.js delete mode 100644 spec/frontend/polyfills/element_spec.js create mode 100644 spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap create mode 100644 spec/frontend/projects/components/remove_modal_spec.js create mode 100644 spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js create mode 100644 spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js create mode 100644 spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js create mode 100644 spec/frontend/ref/components/ref_selector_spec.js create mode 100644 spec/frontend/ref/stores/actions_spec.js create mode 100644 spec/frontend/ref/stores/getters_spec.js create mode 100644 spec/frontend/ref/stores/mutations_spec.js create mode 100644 spec/frontend/registry/explorer/components/delete_button_spec.js create mode 100644 spec/frontend/registry/explorer/components/details_page/details_row_spec.js delete mode 100644 spec/frontend/registry/explorer/components/details_page/empty_tags_state.js create mode 100644 spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js create mode 100644 spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js create mode 100644 spec/frontend/registry/explorer/components/details_page/tags_list_spec.js delete mode 100644 spec/frontend/registry/explorer/components/details_page/tags_table_spec.js create mode 100644 spec/frontend/registry/explorer/components/list_item_spec.js create mode 100644 spec/frontend/releases/components/app_new_spec.js create mode 100644 spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js create mode 100644 spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js create mode 100644 spec/frontend/reports/codequality_report/mock_data.js create mode 100644 spec/frontend/reports/codequality_report/store/actions_spec.js create mode 100644 spec/frontend/reports/codequality_report/store/getters_spec.js create mode 100644 spec/frontend/reports/codequality_report/store/mutations_spec.js create mode 100644 spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js create mode 100644 spec/frontend/repository/components/web_ide_link_spec.js create mode 100644 spec/frontend/search_autocomplete_spec.js create mode 100644 spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js create mode 100644 spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js create mode 100644 spec/frontend/vue_mr_widget/components/approvals/approvals_summary_spec.js create mode 100644 spec/frontend/vue_mr_widget/components/mr_widget_expandable_section_spec.js delete mode 100644 spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js create mode 100644 spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js delete mode 100644 spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_tour_spec.js delete mode 100644 spec/frontend/vue_mr_widget/components/states/pipeline_tour_mock_data.js create mode 100644 spec/frontend/vue_mr_widget/components/terraform/mock_data.js create mode 100644 spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js create mode 100644 spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js delete mode 100644 spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap delete mode 100644 spec/frontend/vue_shared/components/issue/__snapshots__/issue_warning_spec.js.snap delete mode 100644 spec/frontend/vue_shared/components/issue/issue_warning_spec.js create mode 100644 spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/notes/noteable_warning_spec.js create mode 100644 spec/frontend/vue_shared/components/remove_member_modal_spec.js create mode 100644 spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js create mode 100644 spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js delete mode 100644 spec/frontend/vue_shared/components/resizable_chart_container_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/modals/add_image/upload_image_tab_spec.js delete mode 100644 spec/frontend/vue_shared/components/rich_content_editor/modals/add_image_modal_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/build_custom_renderer_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/mock_data.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_font_awesome_html_inline_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_html_block_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_instance_text_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js create mode 100644 spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js (limited to 'spec/frontend') diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js index d65fab80d3b..ea36f1dabaf 100644 --- a/spec/frontend/__mocks__/@gitlab/ui.js +++ b/spec/frontend/__mocks__/@gitlab/ui.js @@ -7,22 +7,23 @@ export * from '@gitlab/ui'; * * This mock decouples those tests from the implementation, removing the need to set * them up specially just for these tooltips. + * + * Mocking the modules using the full file path allows the mocks to take effect + * when the modules are imported in this project (`gitlab`) **and** when they + * are imported internally in `@gitlab/ui`. */ -export const GlTooltipDirective = { + +jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({ bind() {}, -}; +})); -export const GlTooltip = { +jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({ render(h) { return h('div', this.$attrs, this.$slots.default); }, -}; - -export const GlPopoverDirective = { - bind() {}, -}; +})); -export const GlPopover = { +jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({ props: { cssClasses: { type: Array, @@ -33,4 +34,4 @@ export const GlPopover = { render(h) { return h('div', this.$attrs, Object.keys(this.$slots).map(s => this.$slots[s])); }, -}; +})); diff --git a/spec/frontend/__mocks__/document-register-element/index.js b/spec/frontend/__mocks__/document-register-element/index.js new file mode 100644 index 00000000000..2d1ec238274 --- /dev/null +++ b/spec/frontend/__mocks__/document-register-element/index.js @@ -0,0 +1 @@ +export default () => {}; diff --git a/spec/frontend/__mocks__/monaco-editor/index.js b/spec/frontend/__mocks__/monaco-editor/index.js index 7c53cfb5174..b9602d69b74 100644 --- a/spec/frontend/__mocks__/monaco-editor/index.js +++ b/spec/frontend/__mocks__/monaco-editor/index.js @@ -8,9 +8,11 @@ import 'monaco-editor/esm/vs/language/css/monaco.contribution'; import 'monaco-editor/esm/vs/language/json/monaco.contribution'; import 'monaco-editor/esm/vs/language/html/monaco.contribution'; import 'monaco-editor/esm/vs/basic-languages/monaco.contribution'; +import 'monaco-yaml/esm/monaco.contribution'; // This language starts trying to spin up web workers which obviously breaks in Jest environment jest.mock('monaco-editor/esm/vs/language/typescript/tsMode'); +jest.mock('monaco-yaml/esm/yamlMode'); export * from 'monaco-editor/esm/vs/editor/editor.api'; export default global.monaco; diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js index 14e45a4f563..daa730d3b9f 100644 --- a/spec/frontend/alert_management/components/alert_management_detail_spec.js +++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js @@ -3,7 +3,7 @@ import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import AlertDetails from '~/alert_management/components/alert_details.vue'; -import createIssueQuery from '~/alert_management/graphql/mutations/create_issue_from_alert.graphql'; +import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql'; import { joinPaths } from '~/lib/utils/url_utility'; import { trackAlertsDetailsViewsOptions, @@ -19,18 +19,20 @@ describe('AlertDetails', () => { let mock; const projectPath = 'root/alerts'; const projectIssuesPath = 'root/alerts/-/issues'; + const projectId = '1'; const findDetailsTable = () => wrapper.find(GlTable); function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) { wrapper = mountMethod(AlertDetails, { - propsData: { + provide: { alertId: 'alertId', projectPath, projectIssuesPath, + projectId, }, data() { - return { alert: { ...mockAlert }, ...data }; + return { alert: { ...mockAlert }, sidebarStatus: false, ...data }; }, mocks: { $apollo: { @@ -39,6 +41,7 @@ describe('AlertDetails', () => { alert: { loading, }, + sidebarStatus: {}, }, }, }, @@ -52,9 +55,7 @@ describe('AlertDetails', () => { afterEach(() => { if (wrapper) { - if (wrapper) { - wrapper.destroy(); - } + wrapper.destroy(); } mock.restore(); }); @@ -133,7 +134,7 @@ describe('AlertDetails', () => { it('should display "View issue" button that links the issue page when issue exists', () => { const issueIid = '3'; mountComponent({ - data: { alert: { ...mockAlert, issueIid } }, + data: { alert: { ...mockAlert, issueIid }, sidebarStatus: false }, }); expect(findViewIssueBtn().exists()).toBe(true); expect(findViewIssueBtn().attributes('href')).toBe(joinPaths(projectIssuesPath, issueIid)); @@ -146,8 +147,11 @@ describe('AlertDetails', () => { mountMethod: mount, data: { alert: { ...mockAlert, issueIid } }, }); - expect(findViewIssueBtn().exists()).toBe(false); - expect(findCreateIssueBtn().exists()).toBe(true); + + return wrapper.vm.$nextTick().then(() => { + expect(findViewIssueBtn().exists()).toBe(false); + expect(findCreateIssueBtn().exists()).toBe(true); + }); }); it('calls `$apollo.mutate` with `createIssueQuery`', () => { @@ -158,7 +162,7 @@ describe('AlertDetails', () => { findCreateIssueBtn().trigger('click'); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: createIssueQuery, + mutation: createIssueMutation, variables: { iid: mockAlert.iid, projectPath, @@ -208,6 +212,13 @@ describe('AlertDetails', () => { expect(wrapper.find(GlAlert).exists()).toBe(true); }); + it('renders html-errors correctly', () => { + mountComponent({ + data: { errored: true, sidebarErrorMessage: '' }, + }); + expect(wrapper.find('[data-testid="htmlError"]').exists()).toBe(true); + }); + it('does not display an error when dismissed', () => { mountComponent({ data: { errored: true, isErrorDismissed: true } }); expect(wrapper.find(GlAlert).exists()).toBe(false); diff --git a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js new file mode 100644 index 00000000000..0d1214211d3 --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlEmptyState } from '@gitlab/ui'; +import AlertManagementEmptyState from '~/alert_management/components/alert_management_empty_state.vue'; + +describe('AlertManagementEmptyState', () => { + let wrapper; + + function mountComponent({ + props = { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + }, + stubs = {}, + } = {}) { + wrapper = shallowMount(AlertManagementEmptyState, { + propsData: { + enableAlertManagementPath: '/link', + emptyAlertSvgPath: 'illustration/path', + ...props, + }, + stubs, + }); + } + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + const EmptyState = () => wrapper.find(GlEmptyState); + + describe('Empty state', () => { + it('shows empty state', () => { + expect(EmptyState().exists()).toBe(true); + }); + + it('show OpsGenie integration state when OpsGenie mcv is true', () => { + mountComponent({ + props: { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + opsgenieMvcEnabled: true, + opsgenieMvcTargetUrl: 'https://opsgenie-url.com', + }, + }); + expect(EmptyState().props('title')).toBe('Opsgenie is enabled'); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js deleted file mode 100644 index 0154e5fa112..00000000000 --- a/spec/frontend/alert_management/components/alert_management_list_spec.js +++ /dev/null @@ -1,489 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { - GlEmptyState, - GlTable, - GlAlert, - GlLoadingIcon, - GlDropdown, - GlDropdownItem, - GlIcon, - GlTabs, - GlTab, - GlBadge, - GlPagination, -} from '@gitlab/ui'; -import { visitUrl } from '~/lib/utils/url_utility'; -import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; -import createFlash from '~/flash'; -import AlertManagementList from '~/alert_management/components/alert_management_list.vue'; -import { - ALERTS_STATUS_TABS, - trackAlertListViewsOptions, - trackAlertStatusUpdateOptions, -} from '~/alert_management/constants'; -import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql'; -import mockAlerts from '../mocks/alerts.json'; -import Tracking from '~/tracking'; - -jest.mock('~/flash'); - -jest.mock('~/lib/utils/url_utility', () => ({ - visitUrl: jest.fn().mockName('visitUrlMock'), - joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, -})); - -describe('AlertManagementList', () => { - let wrapper; - - const findAlertsTable = () => wrapper.find(GlTable); - const findAlerts = () => wrapper.findAll('table tbody tr'); - const findAlert = () => wrapper.find(GlAlert); - const findLoader = () => wrapper.find(GlLoadingIcon); - const findStatusDropdown = () => wrapper.find(GlDropdown); - const findStatusFilterTabs = () => wrapper.findAll(GlTab); - const findStatusTabs = () => wrapper.find(GlTabs); - const findStatusFilterBadge = () => wrapper.findAll(GlBadge); - const findDateFields = () => wrapper.findAll(TimeAgo); - const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem); - const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]'); - const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]'); - const findSeverityColumnHeader = () => wrapper.findAll('th').at(0); - const findPagination = () => wrapper.find(GlPagination); - const alertsCount = { - open: 14, - triggered: 10, - acknowledged: 6, - resolved: 1, - all: 16, - }; - - function mountComponent({ - props = { - alertManagementEnabled: false, - userCanEnableAlertManagement: false, - }, - data = {}, - loading = false, - stubs = {}, - } = {}) { - wrapper = mount(AlertManagementList, { - propsData: { - projectPath: 'gitlab-org/gitlab', - enableAlertManagementPath: '/link', - emptyAlertSvgPath: 'illustration/path', - ...props, - }, - data() { - return data; - }, - mocks: { - $apollo: { - mutate: jest.fn(), - query: jest.fn(), - queries: { - alerts: { - loading, - }, - }, - }, - }, - stubs, - }); - } - - beforeEach(() => { - mountComponent(); - }); - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - describe('Empty state', () => { - it('shows empty state', () => { - expect(wrapper.find(GlEmptyState).exists()).toBe(true); - }); - }); - - describe('Status Filter Tabs', () => { - beforeEach(() => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: mockAlerts, alertsCount }, - loading: false, - stubs: { - GlTab: true, - }, - }); - }); - - it('should display filter tabs with alerts count badge for each status', () => { - const tabs = findStatusFilterTabs().wrappers; - const badges = findStatusFilterBadge(); - - tabs.forEach((tab, i) => { - const status = ALERTS_STATUS_TABS[i].status.toLowerCase(); - expect(tab.text()).toContain(ALERTS_STATUS_TABS[i].title); - expect(badges.at(i).text()).toContain(alertsCount[status]); - }); - }); - }); - - describe('Alerts table', () => { - it('loading state', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: {}, alertsCount: null }, - loading: true, - }); - expect(findAlertsTable().exists()).toBe(true); - expect(findLoader().exists()).toBe(true); - expect( - findAlerts() - .at(0) - .classes(), - ).not.toContain('gl-hover-bg-blue-50'); - }); - - it('error state', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { errors: ['error'] }, alertsCount: null, errored: true }, - loading: false, - }); - expect(findAlertsTable().exists()).toBe(true); - expect(findAlertsTable().text()).toContain('No alerts to display'); - expect(findLoader().exists()).toBe(false); - expect(findAlert().props().variant).toBe('danger'); - expect( - findAlerts() - .at(0) - .classes(), - ).not.toContain('gl-hover-bg-blue-50'); - }); - - it('empty state', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, errored: false }, - loading: false, - }); - expect(findAlertsTable().exists()).toBe(true); - expect(findAlertsTable().text()).toContain('No alerts to display'); - expect(findLoader().exists()).toBe(false); - expect(findAlert().props().variant).toBe('info'); - expect( - findAlerts() - .at(0) - .classes(), - ).not.toContain('gl-hover-bg-blue-50'); - }); - - it('has data state', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, - loading: false, - }); - expect(findLoader().exists()).toBe(false); - expect(findAlertsTable().exists()).toBe(true); - expect(findAlerts()).toHaveLength(mockAlerts.length); - expect( - findAlerts() - .at(0) - .classes(), - ).toContain('gl-hover-bg-blue-50'); - }); - - it('displays status dropdown', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, - loading: false, - }); - expect(findStatusDropdown().exists()).toBe(true); - }); - - it('shows correct severity icons', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, - loading: false, - }); - - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlTable).exists()).toBe(true); - expect( - findAlertsTable() - .find(GlIcon) - .classes('icon-critical'), - ).toBe(true); - }); - }); - - it('renders severity text', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, - loading: false, - }); - - expect( - findSeverityFields() - .at(0) - .text(), - ).toBe('Critical'); - }); - - it('renders Unassigned when no assignee(s) present', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, - loading: false, - }); - - expect( - findAssignees() - .at(0) - .text(), - ).toBe('Unassigned'); - }); - - it('renders username(s) when assignee(s) present', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, - loading: false, - }); - - expect( - findAssignees() - .at(1) - .text(), - ).toBe(mockAlerts[1].assignees.nodes[0].username); - }); - - it('navigates to the detail page when alert row is clicked', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, - loading: false, - }); - - findAlerts() - .at(0) - .trigger('click'); - expect(visitUrl).toHaveBeenCalledWith('/1527542/details'); - }); - - describe('handle date fields', () => { - it('should display time ago dates when values provided', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { - alerts: { - list: [ - { - iid: 1, - status: 'acknowledged', - startedAt: '2020-03-17T23:18:14.996Z', - endedAt: '2020-04-17T23:18:14.996Z', - severity: 'high', - assignees: { nodes: [] }, - }, - ], - }, - alertsCount, - errored: false, - }, - loading: false, - }); - expect(findDateFields().length).toBe(2); - }); - - it('should not display time ago dates when values not provided', () => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { - alerts: [ - { - iid: 1, - status: 'acknowledged', - startedAt: null, - endedAt: null, - severity: 'high', - }, - ], - alertsCount, - errored: false, - }, - loading: false, - }); - expect(findDateFields().exists()).toBe(false); - }); - }); - }); - - describe('sorting the alert list by column', () => { - beforeEach(() => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { - alerts: { list: mockAlerts }, - errored: false, - sort: 'STARTED_AT_DESC', - alertsCount, - }, - loading: false, - stubs: { GlTable }, - }); - }); - - it('updates sort with new direction and column key', () => { - findSeverityColumnHeader().trigger('click'); - - expect(wrapper.vm.$data.sort).toBe('SEVERITY_DESC'); - - findSeverityColumnHeader().trigger('click'); - - expect(wrapper.vm.$data.sort).toBe('SEVERITY_ASC'); - }); - }); - - describe('updating the alert status', () => { - const iid = '1527542'; - const mockUpdatedMutationResult = { - data: { - updateAlertStatus: { - errors: [], - alert: { - iid, - status: 'acknowledged', - }, - }, - }, - }; - - beforeEach(() => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, - loading: false, - }); - }); - - it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); - findFirstStatusOption().vm.$emit('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: updateAlertStatus, - variables: { - iid, - status: 'TRIGGERED', - projectPath: 'gitlab-org/gitlab', - }, - }); - }); - - it('calls `createFlash` when request fails', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); - findFirstStatusOption().vm.$emit('click'); - - setImmediate(() => { - expect(createFlash).toHaveBeenCalledWith( - 'There was an error while updating the status of the alert. Please try again.', - ); - }); - }); - }); - - describe('Snowplow tracking', () => { - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts }, alertsCount }, - loading: false, - }); - }); - - it('should track alert list page views', () => { - const { category, action } = trackAlertListViewsOptions; - expect(Tracking.event).toHaveBeenCalledWith(category, action); - }); - - it('should track alert status updates', () => { - Tracking.event.mockClear(); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); - findFirstStatusOption().vm.$emit('click'); - const status = findFirstStatusOption().text(); - setImmediate(() => { - const { category, action, label } = trackAlertStatusUpdateOptions; - expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status }); - }); - }); - }); - - describe('Pagination', () => { - beforeEach(() => { - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, errored: false }, - loading: false, - }); - }); - - it('does NOT show pagination control when list is smaller than default page size', () => { - findStatusTabs().vm.$emit('input', 3); - wrapper.vm.$nextTick(() => { - expect(findPagination().exists()).toBe(false); - }); - }); - - it('shows pagination control when list is larger than default page size', () => { - findStatusTabs().vm.$emit('input', 0); - wrapper.vm.$nextTick(() => { - expect(findPagination().exists()).toBe(true); - }); - }); - - describe('prevPage', () => { - it('returns prevPage number', () => { - findPagination().vm.$emit('input', 3); - - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.prevPage).toBe(2); - }); - }); - - it('returns 0 when it is the first page', () => { - findPagination().vm.$emit('input', 1); - - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.prevPage).toBe(0); - }); - }); - }); - - describe('nextPage', () => { - it('returns nextPage number', () => { - findPagination().vm.$emit('input', 1); - - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.nextPage).toBe(2); - }); - }); - - it('returns `null` when currentPage is already last page', () => { - findStatusTabs().vm.$emit('input', 3); - findPagination().vm.$emit('input', 1); - return wrapper.vm.$nextTick(() => { - expect(wrapper.vm.nextPage).toBeNull(); - }); - }); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js new file mode 100644 index 00000000000..4644406c037 --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js @@ -0,0 +1,57 @@ +import { shallowMount } from '@vue/test-utils'; +import AlertManagementList from '~/alert_management/components/alert_management_list_wrapper.vue'; +import { trackAlertListViewsOptions } from '~/alert_management/constants'; +import mockAlerts from '../mocks/alerts.json'; +import Tracking from '~/tracking'; + +describe('AlertManagementList', () => { + let wrapper; + + function mountComponent({ + props = { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + }, + data = {}, + stubs = {}, + } = {}) { + wrapper = shallowMount(AlertManagementList, { + propsData: { + projectPath: 'gitlab-org/gitlab', + enableAlertManagementPath: '/link', + populatingAlertsHelpUrl: '/help/help-page.md#populating-alert-data', + emptyAlertSvgPath: 'illustration/path', + ...props, + }, + data() { + return data; + }, + stubs, + }); + } + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('Snowplow tracking', () => { + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts } }, + }); + }); + + it('should track alert list page views', () => { + const { category, action } = trackAlertListViewsOptions; + expect(Tracking.event).toHaveBeenCalledWith(category, action); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js new file mode 100644 index 00000000000..fe08cf2c10a --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js @@ -0,0 +1,76 @@ +import { mount } from '@vue/test-utils'; +import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue'; +import AlertMarkTodo from '~/alert_management/graphql/mutations/alert_todo_create.graphql'; +import mockAlerts from '../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar To Do', () => { + let wrapper; + + function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { + wrapper = mount(SidebarTodo, { + propsData: { + alert: { ...mockAlert }, + ...data, + sidebarCollapsed, + projectPath: 'projectPath', + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('updating the alert to do', () => { + const mockUpdatedMutationResult = { + data: { + updateAlertTodo: { + errors: [], + alert: {}, + }, + }, + }; + + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('renders a button for adding a To Do', () => { + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find('[data-testid="alert-todo-button"]').text()).toBe('Add a To Do'); + }); + }); + + it('calls `$apollo.mutate` with `AlertMarkTodo` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + + return wrapper.vm.$nextTick().then(() => { + wrapper.find('button').trigger('click'); + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: AlertMarkTodo, + variables: { + iid: '1527542', + projectPath: 'projectPath', + }, + }); + }); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_management_system_note_spec.js b/spec/frontend/alert_management/components/alert_management_system_note_spec.js deleted file mode 100644 index 87dc36cc7cb..00000000000 --- a/spec/frontend/alert_management/components/alert_management_system_note_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import SystemNote from '~/alert_management/components/system_notes/system_note.vue'; -import mockAlerts from '../mocks/alerts.json'; - -const mockAlert = mockAlerts[1]; - -describe('Alert Details System Note', () => { - let wrapper; - - function mountComponent({ stubs = {} } = {}) { - wrapper = shallowMount(SystemNote, { - propsData: { - note: { ...mockAlert.notes.nodes[0] }, - }, - stubs, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - describe('System notes', () => { - beforeEach(() => { - mountComponent({}); - }); - - it('renders the correct system note', () => { - expect(wrapper.find('.note-wrapper').attributes('id')).toBe('note_1628'); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js new file mode 100644 index 00000000000..f316126432e --- /dev/null +++ b/spec/frontend/alert_management/components/alert_management_table_spec.js @@ -0,0 +1,590 @@ +import { mount } from '@vue/test-utils'; +import { + GlTable, + GlAlert, + GlLoadingIcon, + GlDropdown, + GlDropdownItem, + GlIcon, + GlTabs, + GlTab, + GlBadge, + GlPagination, + GlSearchBoxByType, +} from '@gitlab/ui'; +import { visitUrl } from '~/lib/utils/url_utility'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import AlertManagementTable from '~/alert_management/components/alert_management_table.vue'; +import { ALERTS_STATUS_TABS, trackAlertStatusUpdateOptions } from '~/alert_management/constants'; +import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql'; +import mockAlerts from '../mocks/alerts.json'; +import Tracking from '~/tracking'; + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn().mockName('visitUrlMock'), + joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, +})); + +describe('AlertManagementTable', () => { + let wrapper; + + const findAlertsTable = () => wrapper.find(GlTable); + const findAlerts = () => wrapper.findAll('table tbody tr'); + const findAlert = () => wrapper.find(GlAlert); + const findLoader = () => wrapper.find(GlLoadingIcon); + const findStatusDropdown = () => wrapper.find(GlDropdown); + const findStatusFilterTabs = () => wrapper.findAll(GlTab); + const findStatusTabs = () => wrapper.find(GlTabs); + const findStatusFilterBadge = () => wrapper.findAll(GlBadge); + const findDateFields = () => wrapper.findAll(TimeAgo); + const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem); + const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]'); + const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]'); + const findSeverityColumnHeader = () => wrapper.findAll('th').at(0); + const findPagination = () => wrapper.find(GlPagination); + const findSearch = () => wrapper.find(GlSearchBoxByType); + const findIssueFields = () => wrapper.findAll('[data-testid="issueField"]'); + const alertsCount = { + open: 14, + triggered: 10, + acknowledged: 6, + resolved: 1, + all: 16, + }; + + function mountComponent({ + props = { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + }, + data = {}, + loading = false, + stubs = {}, + } = {}) { + wrapper = mount(AlertManagementTable, { + propsData: { + projectPath: 'gitlab-org/gitlab', + populatingAlertsHelpUrl: '/help/help-page.md#populating-alert-data', + ...props, + }, + data() { + return data; + }, + mocks: { + $apollo: { + mutate: jest.fn(), + query: jest.fn(), + queries: { + alerts: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + beforeEach(() => { + mountComponent({ data: { alerts: mockAlerts, alertsCount } }); + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('Status Filter Tabs', () => { + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: mockAlerts, alertsCount }, + loading: false, + stubs: { + GlTab: true, + }, + }); + }); + + it('should display filter tabs with alerts count badge for each status', () => { + const tabs = findStatusFilterTabs().wrappers; + const badges = findStatusFilterBadge(); + + tabs.forEach((tab, i) => { + const status = ALERTS_STATUS_TABS[i].status.toLowerCase(); + expect(tab.text()).toContain(ALERTS_STATUS_TABS[i].title); + expect(badges.at(i).text()).toContain(alertsCount[status]); + }); + }); + }); + + describe('Alerts table', () => { + it('loading state', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: {}, alertsCount: null }, + loading: true, + }); + expect(findAlertsTable().exists()).toBe(true); + expect(findLoader().exists()).toBe(true); + expect( + findAlerts() + .at(0) + .classes(), + ).not.toContain('gl-hover-bg-blue-50'); + }); + + it('error state', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { errors: ['error'] }, alertsCount: null, errored: true }, + loading: false, + }); + expect(findAlertsTable().exists()).toBe(true); + expect(findAlertsTable().text()).toContain('No alerts to display'); + expect(findLoader().exists()).toBe(false); + expect(findAlert().props().variant).toBe('danger'); + expect( + findAlerts() + .at(0) + .classes(), + ).not.toContain('gl-hover-bg-blue-50'); + }); + + it('empty state', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, errored: false }, + loading: false, + }); + expect(findAlertsTable().exists()).toBe(true); + expect(findAlertsTable().text()).toContain('No alerts to display'); + expect(findLoader().exists()).toBe(false); + expect(findAlert().props().variant).toBe('info'); + expect( + findAlerts() + .at(0) + .classes(), + ).not.toContain('gl-hover-bg-blue-50'); + }); + + it('has data state', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + expect(findLoader().exists()).toBe(false); + expect(findAlertsTable().exists()).toBe(true); + expect(findAlerts()).toHaveLength(mockAlerts.length); + expect( + findAlerts() + .at(0) + .classes(), + ).toContain('gl-hover-bg-blue-50'); + }); + + it('displays status dropdown', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + expect(findStatusDropdown().exists()).toBe(true); + }); + + it('does not display a dropdown status header', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + expect(findStatusDropdown().contains('.dropdown-title')).toBe(false); + }); + + it('shows correct severity icons', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(GlTable).exists()).toBe(true); + expect( + findAlertsTable() + .find(GlIcon) + .classes('icon-critical'), + ).toBe(true); + }); + }); + + it('renders severity text', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + + expect( + findSeverityFields() + .at(0) + .text(), + ).toBe('Critical'); + }); + + it('renders Unassigned when no assignee(s) present', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + + expect( + findAssignees() + .at(0) + .text(), + ).toBe('Unassigned'); + }); + + it('renders username(s) when assignee(s) present', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + + expect( + findAssignees() + .at(1) + .text(), + ).toBe(mockAlerts[1].assignees.nodes[0].username); + }); + + it('navigates to the detail page when alert row is clicked', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + + findAlerts() + .at(0) + .trigger('click'); + expect(visitUrl).toHaveBeenCalledWith('/1527542/details'); + }); + + describe('alert issue links', () => { + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + }); + + it('shows "None" when no link exists', () => { + expect( + findIssueFields() + .at(0) + .text(), + ).toBe('None'); + }); + + it('renders a link when one exists', () => { + expect( + findIssueFields() + .at(1) + .text(), + ).toBe('#1'); + expect( + findIssueFields() + .at(1) + .attributes('href'), + ).toBe('/gitlab-org/gitlab/-/issues/1'); + }); + }); + + describe('handle date fields', () => { + it('should display time ago dates when values provided', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { + alerts: { + list: [ + { + iid: 1, + status: 'acknowledged', + startedAt: '2020-03-17T23:18:14.996Z', + severity: 'high', + assignees: { nodes: [] }, + }, + ], + }, + alertsCount, + errored: false, + }, + loading: false, + }); + expect(findDateFields().length).toBe(1); + }); + + it('should not display time ago dates when values not provided', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { + alerts: [ + { + iid: 1, + status: 'acknowledged', + startedAt: null, + severity: 'high', + }, + ], + alertsCount, + errored: false, + }, + loading: false, + }); + expect(findDateFields().exists()).toBe(false); + }); + + describe('New Alert indicator', () => { + const oldAlert = mockAlerts[0]; + + const newAlert = { ...oldAlert, isNew: true }; + + it('should highlight the row when alert is new', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: [newAlert] }, alertsCount, errored: false }, + loading: false, + }); + + expect( + findAlerts() + .at(0) + .classes(), + ).toContain('new-alert'); + }); + + it('should not highlight the row when alert is not new', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: [oldAlert] }, alertsCount, errored: false }, + loading: false, + }); + + expect( + findAlerts() + .at(0) + .classes(), + ).not.toContain('new-alert'); + }); + }); + }); + }); + + describe('sorting the alert list by column', () => { + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { + alerts: { list: mockAlerts }, + errored: false, + sort: 'STARTED_AT_DESC', + alertsCount, + }, + loading: false, + stubs: { GlTable }, + }); + }); + + it('updates sort with new direction and column key', () => { + findSeverityColumnHeader().trigger('click'); + + expect(wrapper.vm.$data.sort).toBe('SEVERITY_DESC'); + + findSeverityColumnHeader().trigger('click'); + + expect(wrapper.vm.$data.sort).toBe('SEVERITY_ASC'); + }); + }); + + describe('updating the alert status', () => { + const iid = '1527542'; + const mockUpdatedMutationResult = { + data: { + updateAlertStatus: { + errors: [], + alert: { + iid, + status: 'acknowledged', + }, + }, + }, + }; + + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + }); + + it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + findFirstStatusOption().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateAlertStatus, + variables: { + iid, + status: 'TRIGGERED', + projectPath: 'gitlab-org/gitlab', + }, + }); + }); + + it('shows an error when request fails', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + findFirstStatusOption().vm.$emit('click'); + wrapper.setData({ + errored: true, + }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.find('[data-testid="alert-error"]').exists()).toBe(true); + }); + }); + + it('shows an error when response includes HTML errors', () => { + const mockUpdatedMutationErrorResult = { + data: { + updateAlertStatus: { + errors: [''], + alert: { + iid, + status: 'acknowledged', + }, + }, + }, + }; + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationErrorResult); + findFirstStatusOption().vm.$emit('click'); + wrapper.setData({ errored: true }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.contains('[data-testid="alert-error"]')).toBe(true); + expect(wrapper.contains('[data-testid="htmlError"]')).toBe(true); + }); + }); + }); + + describe('Snowplow tracking', () => { + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount }, + loading: false, + }); + }); + + it('should track alert status updates', () => { + Tracking.event.mockClear(); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); + findFirstStatusOption().vm.$emit('click'); + const status = findFirstStatusOption().text(); + setImmediate(() => { + const { category, action, label } = trackAlertStatusUpdateOptions; + expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status }); + }); + }); + }); + + describe('Pagination', () => { + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, errored: false }, + loading: false, + }); + }); + + it('does NOT show pagination control when list is smaller than default page size', () => { + findStatusTabs().vm.$emit('input', 3); + return wrapper.vm.$nextTick(() => { + expect(findPagination().exists()).toBe(false); + }); + }); + + it('shows pagination control when list is larger than default page size', () => { + findStatusTabs().vm.$emit('input', 0); + return wrapper.vm.$nextTick(() => { + expect(findPagination().exists()).toBe(true); + }); + }); + + describe('prevPage', () => { + it('returns prevPage number', () => { + findPagination().vm.$emit('input', 3); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.prevPage).toBe(2); + }); + }); + + it('returns 0 when it is the first page', () => { + findPagination().vm.$emit('input', 1); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.prevPage).toBe(0); + }); + }); + }); + + describe('nextPage', () => { + it('returns nextPage number', () => { + findPagination().vm.$emit('input', 1); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.nextPage).toBe(2); + }); + }); + + it('returns `null` when currentPage is already last page', () => { + findStatusTabs().vm.$emit('input', 3); + findPagination().vm.$emit('input', 1); + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.nextPage).toBeNull(); + }); + }); + }); + }); + + describe('Search', () => { + beforeEach(() => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + }); + + it('renders the search component', () => { + expect(findSearch().exists()).toBe(true); + }); + + it('sets the `searchTerm` graphql variable', () => { + const SEARCH_TERM = 'Simple Alert'; + + findSearch().vm.$emit('input', SEARCH_TERM); + + expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js deleted file mode 100644 index 5dbd83dbdac..00000000000 --- a/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import { GlDropdownItem } from '@gitlab/ui'; -import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue'; -import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue'; -import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.graphql'; -import mockAlerts from '../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('Alert Details Sidebar Assignees', () => { - let wrapper; - let mock; - - function mountComponent({ - data, - users = [], - isDropdownSearching = false, - sidebarCollapsed = true, - loading = false, - stubs = {}, - } = {}) { - wrapper = shallowMount(SidebarAssignees, { - data() { - return { - users, - isDropdownSearching, - }; - }, - propsData: { - alert: { ...mockAlert }, - ...data, - sidebarCollapsed, - projectPath: 'projectPath', - }, - mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - }, - }, - }, - stubs, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - mock.restore(); - }); - - describe('updating the alert status', () => { - const mockUpdatedMutationResult = { - data: { - updateAlertStatus: { - errors: [], - alert: { - assigneeUsernames: ['root'], - }, - }, - }, - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - const path = '/autocomplete/users.json'; - const users = [ - { - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - id: 1, - name: 'User 1', - username: 'root', - }, - { - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - id: 2, - name: 'User 2', - username: 'not-root', - }, - ]; - - mock.onGet(path).replyOnce(200, users); - mountComponent({ - data: { alert: mockAlert }, - sidebarCollapsed: false, - loading: false, - users, - stubs: { - SidebarAssignee, - }, - }); - }); - - it('renders a unassigned option', () => { - wrapper.setData({ isDropdownSearching: false }); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find(GlDropdownItem).text()).toBe('Unassigned'); - }); - }); - - it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); - wrapper.setData({ isDropdownSearching: false }); - - return wrapper.vm.$nextTick().then(() => { - wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: AlertSetAssignees, - variables: { - iid: '1527542', - assigneeUsernames: ['root'], - projectPath: 'projectPath', - }, - }); - }); - }); - - it('stops updating and cancels loading when the request fails', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); - wrapper.vm.updateAlertAssignees('root'); - expect(wrapper.find('[data-testid="unassigned-users"]').text()).toBe('assign yourself'); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/alert_metrics_spec.js b/spec/frontend/alert_management/components/alert_metrics_spec.js new file mode 100644 index 00000000000..c188363ddc2 --- /dev/null +++ b/spec/frontend/alert_management/components/alert_metrics_spec.js @@ -0,0 +1,67 @@ +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import AlertMetrics from '~/alert_management/components/alert_metrics.vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; + +jest.mock('~/monitoring/stores', () => ({ + monitoringDashboard: {}, +})); + +const mockEmbedName = 'MetricsEmbedStub'; + +jest.mock('~/monitoring/components/embeds/metric_embed.vue', () => ({ + name: mockEmbedName, + render(h) { + return h('div'); + }, +})); + +describe('Alert Metrics', () => { + let wrapper; + const mock = new MockAdapter(axios); + + function mountComponent({ props } = {}) { + wrapper = shallowMount(AlertMetrics, { + propsData: { + ...props, + }, + stubs: { + MetricEmbed: true, + }, + }); + } + + const findChart = () => wrapper.find({ name: mockEmbedName }); + const findEmptyState = () => wrapper.find({ ref: 'emptyState' }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + afterAll(() => { + mock.restore(); + }); + + describe('Empty state', () => { + it('should display a message when metrics dashboard url is not provided ', () => { + mountComponent(); + expect(findChart().exists()).toBe(false); + expect(findEmptyState().text()).toBe("Metrics weren't available in the alerts payload."); + }); + }); + + describe('Chart', () => { + it('should be rendered when dashboard url is provided', async () => { + mountComponent({ props: { dashboardUrl: 'metrics.url' } }); + + await waitForPromises(); + await wrapper.vm.$nextTick(); + + expect(findEmptyState().exists()).toBe(false); + expect(findChart().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_sidebar_spec.js b/spec/frontend/alert_management/components/alert_sidebar_spec.js deleted file mode 100644 index 80c4d9e0650..00000000000 --- a/spec/frontend/alert_management/components/alert_sidebar_spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import { shallowMount, mount } from '@vue/test-utils'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import AlertSidebar from '~/alert_management/components/alert_sidebar.vue'; -import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue'; -import mockAlerts from '../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('Alert Details Sidebar', () => { - let wrapper; - let mock; - - function mountComponent({ - sidebarCollapsed = true, - mountMethod = shallowMount, - stubs = {}, - alert = {}, - } = {}) { - wrapper = mountMethod(AlertSidebar, { - propsData: { - alert, - sidebarCollapsed, - projectPath: 'projectPath', - }, - stubs, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - mock.restore(); - }); - - describe('the sidebar renders', () => { - beforeEach(() => { - mock = new MockAdapter(axios); - mountComponent(); - }); - - it('open as default', () => { - expect(wrapper.props('sidebarCollapsed')).toBe(true); - }); - - it('should render side bar assignee dropdown', () => { - mountComponent({ - mountMethod: mount, - alert: mockAlert, - }); - expect(wrapper.find(SidebarAssignees).exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/alert_sidebar_status_spec.js b/spec/frontend/alert_management/components/alert_sidebar_status_spec.js deleted file mode 100644 index 94643966a43..00000000000 --- a/spec/frontend/alert_management/components/alert_sidebar_status_spec.js +++ /dev/null @@ -1,107 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; -import { trackAlertStatusUpdateOptions } from '~/alert_management/constants'; -import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue'; -import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql'; -import Tracking from '~/tracking'; -import mockAlerts from '../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('Alert Details Sidebar Status', () => { - let wrapper; - const findStatusDropdownItem = () => wrapper.find(GlDropdownItem); - const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon); - - function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { - wrapper = shallowMount(AlertSidebarStatus, { - propsData: { - alert: { ...mockAlert }, - ...data, - sidebarCollapsed, - projectPath: 'projectPath', - }, - mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - }, - }, - }, - stubs, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - describe('updating the alert status', () => { - const mockUpdatedMutationResult = { - data: { - updateAlertStatus: { - errors: [], - alert: { - status: 'acknowledged', - }, - }, - }, - }; - - beforeEach(() => { - mountComponent({ - data: { alert: mockAlert }, - sidebarCollapsed: false, - loading: false, - }); - }); - - it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); - findStatusDropdownItem().vm.$emit('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: updateAlertStatus, - variables: { - iid: '1527542', - status: 'TRIGGERED', - projectPath: 'projectPath', - }, - }); - }); - - it('stops updating when the request fails', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); - findStatusDropdownItem().vm.$emit('click'); - expect(findStatusLoadingIcon().exists()).toBe(false); - expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered'); - }); - }); - - describe('Snowplow tracking', () => { - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alert: mockAlert }, - loading: false, - }); - }); - - it('should track alert status updates', () => { - Tracking.event.mockClear(); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); - findStatusDropdownItem().vm.$emit('click'); - const status = findStatusDropdownItem().text(); - setImmediate(() => { - const { category, action, label } = trackAlertStatusUpdateOptions; - expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status }); - }); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js new file mode 100644 index 00000000000..db086782424 --- /dev/null +++ b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js @@ -0,0 +1,154 @@ +import { shallowMount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { GlDropdownItem } from '@gitlab/ui'; +import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue'; +import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue'; +import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql'; +import mockAlerts from '../../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar Assignees', () => { + let wrapper; + let mock; + + function mountComponent({ + data, + users = [], + isDropdownSearching = false, + sidebarCollapsed = true, + loading = false, + stubs = {}, + } = {}) { + wrapper = shallowMount(SidebarAssignees, { + data() { + return { + users, + isDropdownSearching, + }; + }, + propsData: { + alert: { ...mockAlert }, + ...data, + sidebarCollapsed, + projectPath: 'projectPath', + projectId: '1', + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + mock.restore(); + }); + + describe('updating the alert status', () => { + const mockUpdatedMutationResult = { + data: { + alertSetAssignees: { + errors: [], + alert: { + assigneeUsernames: ['root'], + }, + }, + }, + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + const path = '/-/autocomplete/users.json'; + const users = [ + { + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 1, + name: 'User 1', + username: 'root', + }, + { + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: 2, + name: 'User 2', + username: 'not-root', + }, + ]; + + mock.onGet(path).replyOnce(200, users); + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + users, + stubs: { + SidebarAssignee, + }, + }); + }); + + it('renders a unassigned option', () => { + wrapper.setData({ isDropdownSearching: false }); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(GlDropdownItem).text()).toBe('Unassigned'); + }); + }); + + it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + wrapper.setData({ isDropdownSearching: false }); + + return wrapper.vm.$nextTick().then(() => { + wrapper.find(SidebarAssignee).vm.$emit('update-alert-assignees', 'root'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: AlertSetAssignees, + variables: { + iid: '1527542', + assigneeUsernames: ['root'], + projectPath: 'projectPath', + }, + }); + }); + }); + + it('shows an error when request contains error messages', () => { + wrapper.setData({ isDropdownSearching: false }); + const errorMutationResult = { + data: { + alertSetAssignees: { + errors: ['There was a problem for sure.'], + alert: {}, + }, + }, + }; + + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult); + + return wrapper.vm.$nextTick().then(() => { + const SideBarAssigneeItem = wrapper.findAll(SidebarAssignee).at(0); + SideBarAssigneeItem.vm.$emit('click'); + expect(wrapper.emitted('alert-refresh')).toBeUndefined(); + }); + }); + + it('stops updating and cancels loading when the request fails', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + wrapper.vm.updateAlertAssignees('root'); + expect(wrapper.find('[data-testid="unassigned-users"]').text()).toBe('assign yourself'); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js b/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js new file mode 100644 index 00000000000..5235ae63fee --- /dev/null +++ b/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js @@ -0,0 +1,64 @@ +import { shallowMount, mount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import AlertSidebar from '~/alert_management/components/alert_sidebar.vue'; +import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue'; +import mockAlerts from '../../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar', () => { + let wrapper; + let mock; + + function mountComponent({ mountMethod = shallowMount, stubs = {}, alert = {} } = {}) { + wrapper = mountMethod(AlertSidebar, { + data() { + return { + sidebarStatus: false, + }; + }, + propsData: { + alert, + }, + provide: { + projectPath: 'projectPath', + projectId: '1', + }, + stubs, + mocks: { + $apollo: { + queries: { + sidebarStatus: {}, + }, + }, + }, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + mock.restore(); + }); + + describe('the sidebar renders', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + mountComponent(); + }); + + it('open as default', () => { + expect(wrapper.classes('right-sidebar-expanded')).toBe(true); + }); + + it('should render side bar assignee dropdown', () => { + mountComponent({ + mountMethod: mount, + alert: mockAlert, + }); + expect(wrapper.find(SidebarAssignees).exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js new file mode 100644 index 00000000000..c2eaf540e9c --- /dev/null +++ b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js @@ -0,0 +1,129 @@ +import { mount } from '@vue/test-utils'; +import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; +import { trackAlertStatusUpdateOptions } from '~/alert_management/constants'; +import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue'; +import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql'; +import Tracking from '~/tracking'; +import mockAlerts from '../../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar Status', () => { + let wrapper; + const findStatusDropdown = () => wrapper.find(GlDropdown); + const findStatusDropdownItem = () => wrapper.find(GlDropdownItem); + const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon); + + function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { + wrapper = mount(AlertSidebarStatus, { + propsData: { + alert: { ...mockAlert }, + ...data, + sidebarCollapsed, + projectPath: 'projectPath', + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('Alert Sidebar Dropdown Status', () => { + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('displays status dropdown', () => { + expect(findStatusDropdown().exists()).toBe(true); + }); + + it('displays the dropdown status header', () => { + expect(findStatusDropdown().contains('.dropdown-title')).toBe(true); + }); + + describe('updating the alert status', () => { + const mockUpdatedMutationResult = { + data: { + updateAlertStatus: { + errors: [], + alert: { + status: 'acknowledged', + }, + }, + }, + }; + + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + findStatusDropdownItem().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateAlertStatus, + variables: { + iid: '1527542', + status: 'TRIGGERED', + projectPath: 'projectPath', + }, + }); + }); + + it('stops updating when the request fails', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + findStatusDropdownItem().vm.$emit('click'); + expect(findStatusLoadingIcon().exists()).toBe(false); + expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered'); + }); + }); + + describe('Snowplow tracking', () => { + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alert: mockAlert }, + loading: false, + }); + }); + + it('should track alert status updates', () => { + Tracking.event.mockClear(); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); + findStatusDropdownItem().vm.$emit('click'); + const status = findStatusDropdownItem().text(); + setImmediate(() => { + const { category, action, label } = trackAlertStatusUpdateOptions; + expect(Tracking.event).toHaveBeenCalledWith(category, action, { + label, + property: status, + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js b/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js new file mode 100644 index 00000000000..8dd663e55d9 --- /dev/null +++ b/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js @@ -0,0 +1,38 @@ +import { shallowMount } from '@vue/test-utils'; +import SystemNote from '~/alert_management/components/system_notes/system_note.vue'; +import mockAlerts from '../../mocks/alerts.json'; + +const mockAlert = mockAlerts[1]; + +describe('Alert Details System Note', () => { + let wrapper; + + function mountComponent({ stubs = {} } = {}) { + wrapper = shallowMount(SystemNote, { + propsData: { + note: { ...mockAlert.notes.nodes[0] }, + }, + stubs, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('System notes', () => { + beforeEach(() => { + mountComponent({}); + }); + + it('renders the correct system note', () => { + const noteId = wrapper.find('.note-wrapper').attributes('id'); + const iconRoute = wrapper.find('use').attributes('href'); + + expect(noteId).toBe('note_1628'); + expect(iconRoute.includes('user')).toBe(true); + }); + }); +}); diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json index 312d1756790..f63019d1e5c 100644 --- a/spec/frontend/alert_management/mocks/alerts.json +++ b/spec/frontend/alert_management/mocks/alerts.json @@ -20,6 +20,7 @@ "endedAt": "2020-04-17T23:18:14.996Z", "status": "ACKNOWLEDGED", "assignees": { "nodes": [{ "username": "root" }] }, + "issueIid": "1", "notes": { "nodes": [ { @@ -32,7 +33,8 @@ "name": "Administrator", "username": "root", "webUrl": "http://192.168.1.4:3000/root" - } + }, + "systemNoteIconName": "user" } ] } diff --git a/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap b/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap new file mode 100644 index 00000000000..1f5c3a80fbb --- /dev/null +++ b/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AlertsSettingsForm with default values renders the initial template 1`] = ` +"
+ +
+

+ +

+

+ +

+
+ + + + + + + + + + + + + + + + Reset key + + Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in. + + + + + + Test alert payload +
+ + Save changes + + + Cancel + +
+
+
" +`; diff --git a/spec/frontend/alert_settings/alert_settings_form_spec.js b/spec/frontend/alert_settings/alert_settings_form_spec.js new file mode 100644 index 00000000000..5a04d768645 --- /dev/null +++ b/spec/frontend/alert_settings/alert_settings_form_spec.js @@ -0,0 +1,233 @@ +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { shallowMount } from '@vue/test-utils'; +import { GlModal, GlAlert } from '@gitlab/ui'; +import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue'; +import ToggleButton from '~/vue_shared/components/toggle_button.vue'; + +const PROMETHEUS_URL = '/prometheus/alerts/notify.json'; +const GENERIC_URL = '/alerts/notify.json'; +const KEY = 'abcedfg123'; +const INVALID_URL = 'http://invalid'; +const ACTIVATED = false; + +const defaultProps = { + generic: { + initialAuthorizationKey: KEY, + formPath: INVALID_URL, + url: GENERIC_URL, + alertsSetupUrl: INVALID_URL, + alertsUsageUrl: INVALID_URL, + activated: ACTIVATED, + }, + prometheus: { + prometheusAuthorizationKey: KEY, + prometheusFormPath: INVALID_URL, + prometheusUrl: PROMETHEUS_URL, + activated: ACTIVATED, + }, + opsgenie: { + opsgenieMvcIsAvailable: true, + formPath: INVALID_URL, + activated: ACTIVATED, + opsgenieMvcTargetUrl: GENERIC_URL, + }, +}; + +describe('AlertsSettingsForm', () => { + let wrapper; + let mockAxios; + + const createComponent = (props = defaultProps, { methods } = {}, data) => { + wrapper = shallowMount(AlertsSettingsForm, { + data() { + return { ...data }; + }, + propsData: { + ...defaultProps, + ...props, + }, + methods, + }); + }; + + const findSelect = () => wrapper.find('[data-testid="alert-settings-select"]'); + const findJsonInput = () => wrapper.find('#alert-json'); + const findUrl = () => wrapper.find('#url'); + const findAuthorizationKey = () => wrapper.find('#authorization-key'); + const findApiUrl = () => wrapper.find('#api-url'); + + beforeEach(() => { + mockAxios = new MockAdapter(axios); + setFixtures(` +
+ + +
`); + }); + + afterEach(() => { + wrapper.destroy(); + mockAxios.restore(); + }); + + describe('with default values', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the initial template', () => { + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('reset key', () => { + it('triggers resetKey method', () => { + const resetGenericKey = jest.fn(); + const methods = { resetGenericKey }; + createComponent(defaultProps, { methods }); + + wrapper.find(GlModal).vm.$emit('ok'); + + expect(resetGenericKey).toHaveBeenCalled(); + }); + + it('updates the authorization key on success', () => { + const formPath = 'some/path'; + mockAxios.onPut(formPath, { service: { token: '' } }).replyOnce(200, { token: 'newToken' }); + createComponent({ generic: { ...defaultProps.generic, formPath } }); + + return wrapper.vm.resetGenericKey().then(() => { + expect(findAuthorizationKey().attributes('value')).toBe('newToken'); + }); + }); + + it('shows a alert message on error', () => { + const formPath = 'some/path'; + mockAxios.onPut(formPath).replyOnce(404); + + createComponent({ generic: { ...defaultProps.generic, formPath } }); + + return wrapper.vm.resetGenericKey().then(() => { + expect(wrapper.find(GlAlert).exists()).toBe(true); + }); + }); + }); + + describe('activate toggle', () => { + it('triggers toggleActivated method', () => { + const toggleService = jest.fn(); + const methods = { toggleService }; + createComponent(defaultProps, { methods }); + + wrapper.find(ToggleButton).vm.$emit('change', true); + + expect(toggleService).toHaveBeenCalled(); + }); + + describe('error is encountered', () => { + beforeEach(() => { + const formPath = 'some/path'; + mockAxios.onPut(formPath).replyOnce(500); + }); + + it('restores previous value', () => { + createComponent({ generic: { ...defaultProps.generic, initialActivated: false } }); + return wrapper.vm.resetGenericKey().then(() => { + expect(wrapper.find(ToggleButton).props('value')).toBe(false); + }); + }); + }); + }); + + describe('prometheus is active', () => { + beforeEach(() => { + createComponent( + { prometheus: { ...defaultProps.prometheus, prometheusIsActivated: true } }, + {}, + { + selectedEndpoint: 'prometheus', + }, + ); + }); + + it('renders a valid "select"', () => { + expect(findSelect().exists()).toBe(true); + }); + + it('shows the API URL input', () => { + expect(findApiUrl().exists()).toBe(true); + }); + + it('shows the correct default API URL', () => { + expect(findUrl().attributes('value')).toBe(PROMETHEUS_URL); + }); + }); + + describe('opsgenie is active', () => { + beforeEach(() => { + createComponent( + { opsgenie: { ...defaultProps.opsgenie, opsgenieMvcActivated: true } }, + {}, + { + selectedEndpoint: 'opsgenie', + }, + ); + }); + + it('shows a input for the opsgenie target URL', () => { + expect(findApiUrl().exists()).toBe(true); + expect(findSelect().attributes('value')).toBe('opsgenie'); + }); + }); + + describe('trigger test alert', () => { + beforeEach(() => { + createComponent({ generic: { ...defaultProps.generic, initialActivated: true } }, {}, true); + }); + + it('should enable the JSON input', () => { + expect(findJsonInput().exists()).toBe(true); + expect(findJsonInput().props('value')).toBe(null); + }); + + it('should validate JSON input', () => { + createComponent({ generic: { ...defaultProps.generic } }, true, { + testAlertJson: '{ "value": "test" }', + }); + + findJsonInput().vm.$emit('change'); + return wrapper.vm.$nextTick().then(() => { + expect(findJsonInput().attributes('state')).toBe('true'); + }); + }); + + describe('alert service is toggled', () => { + it('should show a info alert if successful', () => { + const formPath = 'some/path'; + const toggleService = true; + mockAxios.onPut(formPath).replyOnce(200); + + createComponent({ generic: { ...defaultProps.generic, formPath } }); + + return wrapper.vm.toggleActivated(toggleService).then(() => { + expect(wrapper.find(GlAlert).attributes('variant')).toBe('info'); + }); + }); + + it('should show a error alert if failed', () => { + const formPath = 'some/path'; + const toggleService = true; + mockAxios.onPut(formPath).replyOnce(422, { + errors: 'Error message to display', + }); + + createComponent({ generic: { ...defaultProps.generic, formPath } }); + + return wrapper.vm.toggleActivated(toggleService).then(() => { + expect(wrapper.find(GlAlert).attributes('variant')).toBe('danger'); + }); + }); + }); + }); +}); diff --git a/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js b/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js index c7c15c8fd44..610f9d6b9bd 100644 --- a/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js +++ b/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js @@ -15,6 +15,7 @@ const defaultProps = { alertsSetupUrl: 'http://invalid', alertsUsageUrl: 'http://invalid', initialActivated: false, + isDisabled: false, }; describe('AlertsServiceForm', () => { @@ -166,4 +167,17 @@ describe('AlertsServiceForm', () => { }); }); }); + + describe('form is disabled', () => { + beforeEach(() => { + createComponent({ isDisabled: true }); + }); + + it('cannot be toggled', () => { + wrapper.find(ToggleButton).vm.$emit('change'); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.find(ToggleButton).props('disabledInput')).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index c1a23d441b3..c94637e04af 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -46,6 +46,77 @@ describe('Api', () => { }); }); + describe('packages', () => { + const projectId = 'project_a'; + const packageId = 'package_b'; + const apiResponse = [{ id: 1, name: 'foo' }]; + + describe('groupPackages', () => { + const groupId = 'group_a'; + + it('fetch all group packages', () => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/packages`; + jest.spyOn(axios, 'get'); + mock.onGet(expectedUrl).replyOnce(200, apiResponse); + + return Api.groupPackages(groupId).then(({ data }) => { + expect(data).toEqual(apiResponse); + expect(axios.get).toHaveBeenCalledWith(expectedUrl, {}); + }); + }); + }); + + describe('projectPackages', () => { + it('fetch all project packages', () => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages`; + jest.spyOn(axios, 'get'); + mock.onGet(expectedUrl).replyOnce(200, apiResponse); + + return Api.projectPackages(projectId).then(({ data }) => { + expect(data).toEqual(apiResponse); + expect(axios.get).toHaveBeenCalledWith(expectedUrl, {}); + }); + }); + }); + + describe('buildProjectPackageUrl', () => { + it('returns the right url', () => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages/${packageId}`; + const url = Api.buildProjectPackageUrl(projectId, packageId); + expect(url).toEqual(expectedUrl); + }); + }); + + describe('projectPackage', () => { + it('fetch package details', () => { + const expectedUrl = `foo`; + jest.spyOn(Api, 'buildProjectPackageUrl').mockReturnValue(expectedUrl); + jest.spyOn(axios, 'get'); + mock.onGet(expectedUrl).replyOnce(200, apiResponse); + + return Api.projectPackage(projectId, packageId).then(({ data }) => { + expect(data).toEqual(apiResponse); + expect(axios.get).toHaveBeenCalledWith(expectedUrl); + }); + }); + }); + + describe('deleteProjectPackage', () => { + it('delete a package', () => { + const expectedUrl = `foo`; + + jest.spyOn(Api, 'buildProjectPackageUrl').mockReturnValue(expectedUrl); + jest.spyOn(axios, 'delete'); + mock.onDelete(expectedUrl).replyOnce(200, true); + + return Api.deleteProjectPackage(projectId, packageId).then(({ data }) => { + expect(data).toEqual(true); + expect(axios.delete).toHaveBeenCalledWith(expectedUrl); + }); + }); + }); + }); + describe('group', () => { it('fetches a group', done => { const groupId = '123456'; @@ -366,6 +437,30 @@ describe('Api', () => { }); }); + describe('commit', () => { + const projectId = 'user/project'; + const sha = 'abcd0123'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent( + projectId, + )}/repository/commits/${sha}`; + + it('fetches a single commit', () => { + mock.onGet(expectedUrl).reply(200, { id: sha }); + + return Api.commit(projectId, sha).then(({ data: commit }) => { + expect(commit.id).toBe(sha); + }); + }); + + it('fetches a single commit without stats', () => { + mock.onGet(expectedUrl, { params: { stats: false } }).reply(200, { id: sha }); + + return Api.commit(projectId, sha, { stats: false }).then(({ data: commit }) => { + expect(commit.id).toBe(sha); + }); + }); + }); + describe('issueTemplate', () => { it('fetches an issue template', done => { const namespace = 'some namespace'; diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js index 754f0702b84..6cfbc6024af 100644 --- a/spec/frontend/awards_handler_spec.js +++ b/spec/frontend/awards_handler_spec.js @@ -1,63 +1,61 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import loadAwardsHandler from '~/awards_handler'; -import '~/lib/utils/common_utils'; -import waitForPromises from './helpers/wait_for_promises'; +import { setTestTimeout } from './helpers/timeout'; +import { EMOJI_VERSION } from '~/emoji'; +import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame'; window.gl = window.gl || {}; window.gon = window.gon || {}; -let openAndWaitForEmojiMenu; +let mock; let awardsHandler = null; const urlRoot = gon.relative_url_root; -const lazyAssert = (done, assertFn) => { - jest.runOnlyPendingTimers(); - waitForPromises() - .then(() => { - assertFn(); - done(); - }) - .catch(e => { - throw e; - }); -}; - describe('AwardsHandler', () => { + useFakeRequestAnimationFrame(); + + const emojiData = getJSONFixture('emojis/emojis.json'); preloadFixtures('snippets/show.html'); - beforeEach(done => { - loadFixtures('snippets/show.html'); - loadAwardsHandler(true) - .then(obj => { - awardsHandler = obj; - jest.spyOn(awardsHandler, 'postEmoji').mockImplementation((button, url, emoji, cb) => cb()); - done(); - }) - .catch(done.fail); - - let isEmojiMenuBuilt = false; - openAndWaitForEmojiMenu = () => { - return new Promise(resolve => { - if (isEmojiMenuBuilt) { - resolve(); - } else { - $('.js-add-award') - .eq(0) - .click(); - const $menu = $('.emoji-menu'); - $menu.one('build-emoji-menu-finish', () => { - isEmojiMenuBuilt = true; - resolve(); - }); - } + + const openAndWaitForEmojiMenu = (sel = '.js-add-award') => { + $(sel) + .eq(0) + .click(); + + jest.advanceTimersByTime(200); + + const $menu = $('.emoji-menu'); + + return new Promise(resolve => { + $menu.one('build-emoji-menu-finish', () => { + resolve(); }); - }; + }); + }; + + beforeEach(async () => { + // These tests have had some timeout issues + // https://gitlab.com/gitlab-org/gitlab/-/issues/221086 + setTestTimeout(6000); + + mock = new MockAdapter(axios); + mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData); + + loadFixtures('snippets/show.html'); + + awardsHandler = await loadAwardsHandler(true); + jest.spyOn(awardsHandler, 'postEmoji').mockImplementation((button, url, emoji, cb) => cb()); }); afterEach(() => { // restore original url root value gon.relative_url_root = urlRoot; + mock.restore(); + // Undo what we did to the shared $('body').removeAttr('data-page'); @@ -65,55 +63,45 @@ describe('AwardsHandler', () => { }); describe('::showEmojiMenu', () => { - it('should show emoji menu when Add emoji button clicked', done => { - $('.js-add-award') - .eq(0) - .click(); - lazyAssert(done, () => { - const $emojiMenu = $('.emoji-menu'); + it('should show emoji menu when Add emoji button clicked', async () => { + await openAndWaitForEmojiMenu(); - expect($emojiMenu.length).toBe(1); - expect($emojiMenu.hasClass('is-visible')).toBe(true); - expect($emojiMenu.find('.js-emoji-menu-search').length).toBe(1); - expect($('.js-awards-block.current').length).toBe(1); - }); + const $emojiMenu = $('.emoji-menu'); + + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(true); + expect($emojiMenu.find('.js-emoji-menu-search').length).toBe(1); + expect($('.js-awards-block.current').length).toBe(1); }); - it('should also show emoji menu for the smiley icon in notes', done => { - $('.js-add-award.note-action-button').click(); - lazyAssert(done, () => { - const $emojiMenu = $('.emoji-menu'); + it('should also show emoji menu for the smiley icon in notes', async () => { + await openAndWaitForEmojiMenu('.js-add-award.note-action-button'); - expect($emojiMenu.length).toBe(1); - }); + const $emojiMenu = $('.emoji-menu'); + + expect($emojiMenu.length).toBe(1); }); - it('should remove emoji menu when body is clicked', done => { - $('.js-add-award') - .eq(0) - .click(); - lazyAssert(done, () => { - const $emojiMenu = $('.emoji-menu'); - $('body').click(); + it('should remove emoji menu when body is clicked', async () => { + await openAndWaitForEmojiMenu(); - expect($emojiMenu.length).toBe(1); - expect($emojiMenu.hasClass('is-visible')).toBe(false); - expect($('.js-awards-block.current').length).toBe(0); - }); + const $emojiMenu = $('.emoji-menu'); + $('body').click(); + + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(false); + expect($('.js-awards-block.current').length).toBe(0); }); - it('should not remove emoji menu when search is clicked', done => { - $('.js-add-award') - .eq(0) - .click(); - lazyAssert(done, () => { - const $emojiMenu = $('.emoji-menu'); - $('.emoji-search').click(); + it('should not remove emoji menu when search is clicked', async () => { + await openAndWaitForEmojiMenu(); - expect($emojiMenu.length).toBe(1); - expect($emojiMenu.hasClass('is-visible')).toBe(true); - expect($('.js-awards-block.current').length).toBe(1); - }); + const $emojiMenu = $('.emoji-menu'); + $('.emoji-search').click(); + + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(true); + expect($('.js-awards-block.current').length).toBe(1); }); }); @@ -261,48 +249,39 @@ describe('AwardsHandler', () => { }); describe('::searchEmojis', () => { - it('should filter the emoji', done => { - openAndWaitForEmojiMenu() - .then(() => { - expect($('[data-name=angel]').is(':visible')).toBe(true); - expect($('[data-name=anger]').is(':visible')).toBe(true); - awardsHandler.searchEmojis('ali'); - - expect($('[data-name=angel]').is(':visible')).toBe(false); - expect($('[data-name=anger]').is(':visible')).toBe(false); - expect($('[data-name=alien]').is(':visible')).toBe(true); - expect($('.js-emoji-menu-search').val()).toBe('ali'); - }) - .then(done) - .catch(err => { - done.fail(`Failed to open and build emoji menu: ${err.message}`); - }); + it('should filter the emoji', async () => { + await openAndWaitForEmojiMenu(); + + expect($('[data-name=angel]').is(':visible')).toBe(true); + expect($('[data-name=anger]').is(':visible')).toBe(true); + awardsHandler.searchEmojis('ali'); + + expect($('[data-name=angel]').is(':visible')).toBe(false); + expect($('[data-name=anger]').is(':visible')).toBe(false); + expect($('[data-name=alien]').is(':visible')).toBe(true); + expect($('.js-emoji-menu-search').val()).toBe('ali'); }); - it('should clear the search when searching for nothing', done => { - openAndWaitForEmojiMenu() - .then(() => { - awardsHandler.searchEmojis('ali'); - - expect($('[data-name=angel]').is(':visible')).toBe(false); - expect($('[data-name=anger]').is(':visible')).toBe(false); - expect($('[data-name=alien]').is(':visible')).toBe(true); - awardsHandler.searchEmojis(''); - - expect($('[data-name=angel]').is(':visible')).toBe(true); - expect($('[data-name=anger]').is(':visible')).toBe(true); - expect($('[data-name=alien]').is(':visible')).toBe(true); - expect($('.js-emoji-menu-search').val()).toBe(''); - }) - .then(done) - .catch(err => { - done.fail(`Failed to open and build emoji menu: ${err.message}`); - }); + it('should clear the search when searching for nothing', async () => { + await openAndWaitForEmojiMenu(); + + awardsHandler.searchEmojis('ali'); + + expect($('[data-name=angel]').is(':visible')).toBe(false); + expect($('[data-name=anger]').is(':visible')).toBe(false); + expect($('[data-name=alien]').is(':visible')).toBe(true); + awardsHandler.searchEmojis(''); + + expect($('[data-name=angel]').is(':visible')).toBe(true); + expect($('[data-name=anger]').is(':visible')).toBe(true); + expect($('[data-name=alien]').is(':visible')).toBe(true); + expect($('.js-emoji-menu-search').val()).toBe(''); }); }); describe('emoji menu', () => { const emojiSelector = '[data-name="sunglasses"]'; + const openEmojiMenuAndAddEmoji = () => { return openAndWaitForEmojiMenu().then(() => { const $menu = $('.emoji-menu'); @@ -318,32 +297,23 @@ describe('AwardsHandler', () => { }); }; - it('should add selected emoji to awards block', done => { - openEmojiMenuAndAddEmoji() - .then(done) - .catch(err => { - done.fail(`Failed to open and build emoji menu: ${err.message}`); - }); + it('should add selected emoji to awards block', async () => { + await openEmojiMenuAndAddEmoji(); }); - it('should remove already selected emoji', done => { - openEmojiMenuAndAddEmoji() - .then(() => { - $('.js-add-award') - .eq(0) - .click(); - const $block = $('.js-awards-block'); - const $emoji = $('.emoji-menu').find( - `.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`, - ); - $emoji.click(); - - expect($block.find(emojiSelector).length).toBe(0); - }) - .then(done) - .catch(err => { - done.fail(`Failed to open and build emoji menu: ${err.message}`); - }); + it('should remove already selected emoji', async () => { + await openEmojiMenuAndAddEmoji(); + + $('.js-add-award') + .eq(0) + .click(); + const $block = $('.js-awards-block'); + const $emoji = $('.emoji-menu').find( + `.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`, + ); + $emoji.click(); + + expect($block.find(emojiSelector).length).toBe(0); }); }); @@ -353,37 +323,27 @@ describe('AwardsHandler', () => { Cookies.set('frequently_used_emojis', ''); }); - it('shouldn\'t have any "Frequently used" heading if no frequently used emojis', done => { - return openAndWaitForEmojiMenu() - .then(() => { - const emojiMenu = document.querySelector('.emoji-menu'); - Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => { - expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used'); - }); - }) - .then(done) - .catch(err => { - done.fail(`Failed to open and build emoji menu: ${err.message}`); - }); + it('shouldn\'t have any "Frequently used" heading if no frequently used emojis', async () => { + await openAndWaitForEmojiMenu(); + + const emojiMenu = document.querySelector('.emoji-menu'); + Array.prototype.forEach.call(emojiMenu.querySelectorAll('.emoji-menu-title'), title => { + expect(title.textContent.trim().toLowerCase()).not.toBe('frequently used'); + }); }); - it('should have any frequently used section when there are frequently used emojis', done => { + it('should have any frequently used section when there are frequently used emojis', async () => { awardsHandler.addEmojiToFrequentlyUsedList('8ball'); - return openAndWaitForEmojiMenu() - .then(() => { - const emojiMenu = document.querySelector('.emoji-menu'); - const hasFrequentlyUsedHeading = Array.prototype.some.call( - emojiMenu.querySelectorAll('.emoji-menu-title'), - title => title.textContent.trim().toLowerCase() === 'frequently used', - ); - - expect(hasFrequentlyUsedHeading).toBe(true); - }) - .then(done) - .catch(err => { - done.fail(`Failed to open and build emoji menu: ${err.message}`); - }); + await openAndWaitForEmojiMenu(); + + const emojiMenu = document.querySelector('.emoji-menu'); + const hasFrequentlyUsedHeading = Array.prototype.some.call( + emojiMenu.querySelectorAll('.emoji-menu-title'), + title => title.textContent.trim().toLowerCase() === 'frequently used', + ); + + expect(hasFrequentlyUsedHeading).toBe(true); }); it('should disregard invalid frequently used emoji that are being attempted to be added', () => { 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 2ec114d026a..4bac6d4e3dc 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 @@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/batch_comments/stores/modules/batch_comments/actions'; import axios from '~/lib/utils/axios_utils'; +import { TEST_HOST } from 'jest/helpers/test_constants'; describe('Batch comments store actions', () => { let res = {}; @@ -33,7 +34,7 @@ describe('Batch comments store actions', () => { testAction( actions.addDraftToDiscussion, - { endpoint: gl.TEST_HOST, data: 'test' }, + { endpoint: TEST_HOST, data: 'test' }, null, [{ type: 'ADD_NEW_DRAFT', payload: res }], [], @@ -46,7 +47,7 @@ describe('Batch comments store actions', () => { testAction( actions.addDraftToDiscussion, - { endpoint: gl.TEST_HOST, data: 'test' }, + { endpoint: TEST_HOST, data: 'test' }, null, [], [], @@ -62,7 +63,7 @@ describe('Batch comments store actions', () => { testAction( actions.createNewDraft, - { endpoint: gl.TEST_HOST, data: 'test' }, + { endpoint: TEST_HOST, data: 'test' }, null, [{ type: 'ADD_NEW_DRAFT', payload: res }], [], @@ -73,14 +74,7 @@ describe('Batch comments store actions', () => { it('does not commit ADD_NEW_DRAFT if errors returned', done => { mock.onAny().reply(500); - testAction( - actions.createNewDraft, - { endpoint: gl.TEST_HOST, data: 'test' }, - null, - [], - [], - done, - ); + testAction(actions.createNewDraft, { endpoint: TEST_HOST, data: 'test' }, null, [], [], done); }); }); @@ -90,7 +84,7 @@ describe('Batch comments store actions', () => { beforeEach(() => { getters = { getNotesData: { - draftsDiscardPath: gl.TEST_HOST, + draftsDiscardPath: TEST_HOST, }, }; }); @@ -137,7 +131,7 @@ describe('Batch comments store actions', () => { beforeEach(() => { getters = { getNotesData: { - draftsPath: gl.TEST_HOST, + draftsPath: TEST_HOST, }, }; }); @@ -171,7 +165,7 @@ describe('Batch comments store actions', () => { dispatch = jest.fn(); commit = jest.fn(); getters = { - getNotesData: { draftsPublishPath: gl.TEST_HOST, discussionsPath: gl.TEST_HOST }, + getNotesData: { draftsPublishPath: TEST_HOST, discussionsPath: TEST_HOST }, }; rootGetters = { discussionsStructuredByLineCode: 'discussions' }; }); @@ -208,7 +202,7 @@ describe('Batch comments store actions', () => { describe('discardReview', () => { it('commits mutations', done => { const getters = { - getNotesData: { draftsDiscardPath: gl.TEST_HOST }, + getNotesData: { draftsDiscardPath: TEST_HOST }, }; const commit = jest.fn(); mock.onAny().reply(200); @@ -225,7 +219,7 @@ describe('Batch comments store actions', () => { it('commits error mutations', done => { const getters = { - getNotesData: { draftsDiscardPath: gl.TEST_HOST }, + getNotesData: { draftsDiscardPath: TEST_HOST }, }; const commit = jest.fn(); mock.onAny().reply(500); @@ -247,7 +241,7 @@ describe('Batch comments store actions', () => { beforeEach(() => { getters = { getNotesData: { - draftsPath: gl.TEST_HOST, + draftsPath: TEST_HOST, }, }; }); diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js index cf96ac488a8..33af9bc135e 100644 --- a/spec/frontend/behaviors/copy_as_gfm_spec.js +++ b/spec/frontend/behaviors/copy_as_gfm_spec.js @@ -1,3 +1,4 @@ +import * as commonUtils from '~/lib/utils/common_utils'; import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm'; describe('CopyAsGFM', () => { @@ -27,7 +28,7 @@ describe('CopyAsGFM', () => { } it('wraps pasted code when not already in code tags', () => { - jest.spyOn(window.gl.utils, 'insertText').mockImplementation((el, textFunc) => { + jest.spyOn(commonUtils, 'insertText').mockImplementation((el, textFunc) => { const insertedText = textFunc('This is code: ', ''); expect(insertedText).toEqual('`code`'); @@ -37,7 +38,7 @@ describe('CopyAsGFM', () => { }); it('does not wrap pasted code when already in code tags', () => { - jest.spyOn(window.gl.utils, 'insertText').mockImplementation((el, textFunc) => { + jest.spyOn(commonUtils, 'insertText').mockImplementation((el, textFunc) => { const insertedText = textFunc('This is code: `', '`'); expect(insertedText).toEqual('code'); diff --git a/spec/frontend/behaviors/gl_emoji/unicode_support_map_spec.js b/spec/frontend/behaviors/gl_emoji/unicode_support_map_spec.js deleted file mode 100644 index aaee9c30cac..00000000000 --- a/spec/frontend/behaviors/gl_emoji/unicode_support_map_spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import getUnicodeSupportMap from '~/emoji/support/unicode_support_map'; -import AccessorUtilities from '~/lib/utils/accessor'; -import { useLocalStorageSpy } from 'helpers/local_storage_helper'; - -describe('Unicode Support Map', () => { - useLocalStorageSpy(); - describe('getUnicodeSupportMap', () => { - const stringSupportMap = 'stringSupportMap'; - - beforeEach(() => { - jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockImplementation(() => {}); - jest.spyOn(JSON, 'parse').mockImplementation(() => {}); - jest.spyOn(JSON, 'stringify').mockReturnValue(stringSupportMap); - }); - - describe('if isLocalStorageAvailable is `true`', () => { - beforeEach(() => { - jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(true); - - getUnicodeSupportMap(); - }); - - it('should call .getItem and .setItem', () => { - const getArgs = window.localStorage.getItem.mock.calls; - const setArgs = window.localStorage.setItem.mock.calls; - - expect(getArgs[0][0]).toBe('gl-emoji-version'); - expect(getArgs[1][0]).toBe('gl-emoji-user-agent'); - - expect(setArgs[0][0]).toBe('gl-emoji-version'); - expect(setArgs[0][1]).toBe('0.2.0'); - expect(setArgs[1][0]).toBe('gl-emoji-user-agent'); - expect(setArgs[1][1]).toBe(navigator.userAgent); - expect(setArgs[2][0]).toBe('gl-emoji-unicode-support-map'); - expect(setArgs[2][1]).toBe(stringSupportMap); - }); - }); - - describe('if isLocalStorageAvailable is `false`', () => { - beforeEach(() => { - jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(false); - - getUnicodeSupportMap(); - }); - - it('should not call .getItem or .setItem', () => { - expect(window.localStorage.getItem.mock.calls.length).toBe(1); - expect(window.localStorage.setItem).not.toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js new file mode 100644 index 00000000000..7ea0bafc328 --- /dev/null +++ b/spec/frontend/behaviors/gl_emoji_spec.js @@ -0,0 +1,110 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { initEmojiMap, EMOJI_VERSION } from '~/emoji'; +import installGlEmojiElement from '~/behaviors/gl_emoji'; + +import * as EmojiUnicodeSupport from '~/emoji/support'; +import waitForPromises from 'jest/helpers/wait_for_promises'; + +jest.mock('~/emoji/support'); + +describe('gl_emoji', () => { + let mock; + const emojiData = getJSONFixture('emojis/emojis.json'); + + beforeAll(() => { + jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true); + installGlEmojiElement(); + }); + + function markupToDomElement(markup) { + const div = document.createElement('div'); + div.innerHTML = markup; + document.body.appendChild(div); + + return div.firstElementChild; + } + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData); + + return initEmojiMap().catch(() => {}); + }); + + afterEach(() => { + mock.restore(); + + 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:', + ], + ])('%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('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; + + expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null); + expect(window.gon.emoji_sprites_css_added).toBeFalsy(); + + markupToDomElement( + '', + ); + await waitForPromises(); + + expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe( + '', + ); + expect(window.gon.emoji_sprites_css_added).toBe(true); + }); +}); diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js index 6391a544985..baedbf5771a 100644 --- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js +++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -46,7 +46,7 @@ describe('ShortcutsIssuable', () => { }); describe('replyWithSelectedText', () => { - // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. + // Stub getSelectedFragment to return a node with the provided HTML. const stubSelection = (html, invalidNode) => { getSelectedFragment.mockImplementation(() => { const documentFragment = document.createDocumentFragment(); diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap index 005b2c5da1c..0f5b3cd3f5e 100644 --- a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap +++ b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap @@ -8,6 +8,7 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `