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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/clean_html_element_serializer.js142
-rw-r--r--spec/frontend/__helpers__/dom_shims/get_client_rects.js3
-rw-r--r--spec/frontend/__helpers__/html_string_serializer.js11
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper.js121
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper_spec.js46
-rw-r--r--spec/frontend/access_tokens/components/access_token_table_app_spec.js4
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap4
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js80
-rw-r--r--spec/frontend/admin/abuse_report/components/activity_events_list_spec.js30
-rw-r--r--spec/frontend/admin/abuse_report/components/activity_history_item_spec.js (renamed from spec/frontend/admin/abuse_report/components/history_items_spec.js)20
-rw-r--r--spec/frontend/admin/abuse_report/components/labels_select_spec.js297
-rw-r--r--spec/frontend/admin/abuse_report/components/report_actions_spec.js27
-rw-r--r--spec/frontend/admin/abuse_report/components/report_details_spec.js74
-rw-r--r--spec/frontend/admin/abuse_report/components/report_header_spec.js55
-rw-r--r--spec/frontend/admin/abuse_report/components/reported_content_spec.js11
-rw-r--r--spec/frontend/admin/abuse_report/components/user_details_spec.js62
-rw-r--r--spec/frontend/admin/abuse_report/mock_data.js88
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js14
-rw-r--r--spec/frontend/admin/abuse_reports/mock_data.js3
-rw-r--r--spec/frontend/admin/applications/components/__snapshots__/delete_application_spec.js.snap1
-rw-r--r--spec/frontend/admin/topics/components/__snapshots__/remove_avatar_spec.js.snap1
-rw-r--r--spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap9
-rw-r--r--spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap2
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js2
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap32
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/__snapshots__/total_time_spec.js.snap46
-rw-r--r--spec/frontend/api/application_settings_api_spec.js45
-rw-r--r--spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap74
-rw-r--r--spec/frontend/avatar_helper_spec.js110
-rw-r--r--spec/frontend/behaviors/markdown/paste_markdown_table_spec.js6
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap6
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap10
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap34
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js61
-rw-r--r--spec/frontend/blob/components/mock_data.js15
-rw-r--r--spec/frontend/blob/line_highlighter_spec.js9
-rw-r--r--spec/frontend/blob/openapi/index_spec.js31
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js1
-rw-r--r--spec/frontend/boards/components/board_card_spec.js1
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js12
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js7
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js8
-rw-r--r--spec/frontend/boards/mock_data.js5
-rw-r--r--spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap51
-rw-r--r--spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap8
-rw-r--r--spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js445
-rw-r--r--spec/frontend/ci/admin/jobs_table/components/cancel_jobs_modal_spec.js (renamed from spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js)2
-rw-r--r--spec/frontend/ci/admin/jobs_table/components/cancel_jobs_spec.js54
-rw-r--r--spec/frontend/ci/admin/jobs_table/components/cells/project_cell_spec.js (renamed from spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js)4
-rw-r--r--spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js (renamed from spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js)6
-rw-r--r--spec/frontend/ci/admin/jobs_table/components/jobs_skeleton_loader_spec.js (renamed from spec/frontend/pages/admin/jobs/components/jobs_skeleton_loader_spec.js)2
-rw-r--r--spec/frontend/ci/admin/jobs_table/graphql/cache_config_spec.js (renamed from spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js)4
-rw-r--r--spec/frontend/ci/artifacts/components/feedback_banner_spec.js59
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js12
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js161
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js41
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js338
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js15
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js5
-rw-r--r--spec/frontend/ci/common/pipelines_table_spec.js (renamed from spec/frontend/pipelines/pipelines_table_spec.js)18
-rw-r--r--spec/frontend/ci/common/private/job_links_layer_spec.js (renamed from spec/frontend/pipelines/graph_shared/links_layer_spec.js)6
-rw-r--r--spec/frontend/ci/common/private/jobs_filtered_search/jobs_filtered_search_spec.js123
-rw-r--r--spec/frontend/ci/common/private/jobs_filtered_search/tokens/job_status_token_spec.js (renamed from spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js)2
-rw-r--r--spec/frontend/ci/common/private/jobs_filtered_search/utils_spec.js22
-rw-r--r--spec/frontend/ci/job_details/components/empty_state_spec.js (renamed from spec/frontend/jobs/components/job/empty_state_spec.js)6
-rw-r--r--spec/frontend/ci/job_details/components/environments_block_spec.js (renamed from spec/frontend/jobs/components/job/environments_block_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/erased_block_spec.js (renamed from spec/frontend/jobs/components/job/erased_block_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/job_header_spec.js (renamed from spec/frontend/vue_shared/components/header_ci_component_spec.js)41
-rw-r--r--spec/frontend/ci/job_details/components/job_log_controllers_spec.js (renamed from spec/frontend/jobs/components/job/job_log_controllers_spec.js)8
-rw-r--r--spec/frontend/ci/job_details/components/log/collapsible_section_spec.js (renamed from spec/frontend/jobs/components/log/collapsible_section_spec.js)26
-rw-r--r--spec/frontend/ci/job_details/components/log/duration_badge_spec.js (renamed from spec/frontend/jobs/components/log/duration_badge_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/log/line_header_spec.js (renamed from spec/frontend/jobs/components/log/line_header_spec.js)42
-rw-r--r--spec/frontend/ci/job_details/components/log/line_number_spec.js (renamed from spec/frontend/jobs/components/log/line_number_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/log/line_spec.js (renamed from spec/frontend/jobs/components/log/line_spec.js)17
-rw-r--r--spec/frontend/ci/job_details/components/log/log_spec.js (renamed from spec/frontend/jobs/components/log/log_spec.js)35
-rw-r--r--spec/frontend/ci/job_details/components/log/mock_data.js (renamed from spec/frontend/jobs/components/log/mock_data.js)0
-rw-r--r--spec/frontend/ci/job_details/components/manual_variables_form_spec.js (renamed from spec/frontend/jobs/components/job/manual_variables_form_spec.js)12
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js (renamed from spec/frontend/jobs/components/job/artifacts_block_spec.js)6
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/commit_block_spec.js (renamed from spec/frontend/jobs/components/job/commit_block_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/external_links_block_spec.js49
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/job_container_item_spec.js (renamed from spec/frontend/jobs/components/job/job_container_item_spec.js)4
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/job_retry_forward_deployment_modal_spec.js (renamed from spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js)17
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/job_sidebar_retry_button_spec.js (renamed from spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js)6
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/jobs_container_spec.js (renamed from spec/frontend/jobs/components/job/jobs_container_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/sidebar_detail_row_spec.js (renamed from spec/frontend/jobs/components/job/sidebar_detail_row_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js (renamed from spec/frontend/jobs/components/job/sidebar_header_spec.js)22
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js (renamed from spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js)11
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/sidebar_spec.js (renamed from spec/frontend/jobs/components/job/sidebar_spec.js)84
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js (renamed from spec/frontend/jobs/components/job/stages_dropdown_spec.js)11
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/trigger_block_spec.js (renamed from spec/frontend/jobs/components/job/trigger_block_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/stuck_block_spec.js (renamed from spec/frontend/jobs/components/job/stuck_block_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/components/unmet_prerequisites_block_spec.js (renamed from spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/job_app_spec.js (renamed from spec/frontend/jobs/components/job/job_app_spec.js)24
-rw-r--r--spec/frontend/ci/job_details/mock_data.js (renamed from spec/frontend/jobs/components/job/mock_data.js)0
-rw-r--r--spec/frontend/ci/job_details/store/actions_spec.js (renamed from spec/frontend/jobs/store/actions_spec.js)6
-rw-r--r--spec/frontend/ci/job_details/store/getters_spec.js (renamed from spec/frontend/jobs/store/getters_spec.js)4
-rw-r--r--spec/frontend/ci/job_details/store/helpers.js (renamed from spec/frontend/jobs/store/helpers.js)2
-rw-r--r--spec/frontend/ci/job_details/store/mutations_spec.js (renamed from spec/frontend/jobs/store/mutations_spec.js)6
-rw-r--r--spec/frontend/ci/job_details/store/utils_spec.js (renamed from spec/frontend/jobs/store/utils_spec.js)2
-rw-r--r--spec/frontend/ci/job_details/utils_spec.js265
-rw-r--r--spec/frontend/ci/jobs_mock_data.js (renamed from spec/frontend/jobs/mock_data.js)1
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/actions_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells/actions_cell_spec.js)14
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells/duration_cell_spec.js)2
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells/job_cell_spec.js)4
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/pipeline_cell_spec.js (renamed from spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js)2
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js (renamed from spec/frontend/jobs/components/table/jobs_table_empty_state_spec.js)2
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_spec.js (renamed from spec/frontend/jobs/components/table/jobs_table_spec.js)19
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_tabs_spec.js (renamed from spec/frontend/jobs/components/table/jobs_table_tabs_spec.js)4
-rw-r--r--spec/frontend/ci/jobs_page/graphql/cache_config_spec.js (renamed from spec/frontend/jobs/components/table/graphql/cache_config_spec.js)4
-rw-r--r--spec/frontend/ci/jobs_page/job_page_app_spec.js (renamed from spec/frontend/jobs/components/table/job_table_app_spec.js)16
-rw-r--r--spec/frontend/ci/merge_requests/components/pipelines_table_wrapper_spec.js117
-rw-r--r--spec/frontend/ci/merge_requests/mock_data.js30
-rw-r--r--spec/frontend/ci/mixins/delayed_job_mixin_spec.js (renamed from spec/frontend/jobs/mixins/delayed_job_mixin_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/dag/components/__snapshots__/dag_graph_spec.js.snap743
-rw-r--r--spec/frontend/ci/pipeline_details/dag/components/dag_annotations_spec.js (renamed from spec/frontend/pipelines/components/dag/dag_annotations_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_details/dag/components/dag_graph_spec.js (renamed from spec/frontend/pipelines/components/dag/dag_graph_spec.js)12
-rw-r--r--spec/frontend/ci/pipeline_details/dag/dag_spec.js (renamed from spec/frontend/pipelines/components/dag/dag_spec.js)10
-rw-r--r--spec/frontend/ci/pipeline_details/dag/mock_data.js (renamed from spec/frontend/pipelines/components/dag/mock_data.js)0
-rw-r--r--spec/frontend/ci/pipeline_details/dag/utils/drawing_utils_spec.js (renamed from spec/frontend/pipelines/components/dag/drawing_utils_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/__snapshots__/links_inner_spec.js.snap110
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/action_component_spec.js (renamed from spec/frontend/pipelines/graph/action_component_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js (renamed from spec/frontend/pipelines/graph/graph_component_spec.js)18
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/graph_view_selector_spec.js (renamed from spec/frontend/pipelines/graph/graph_view_selector_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_group_dropdown_spec.js (renamed from spec/frontend/pipelines/graph/job_group_dropdown_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js (renamed from spec/frontend/pipelines/graph/job_item_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js (renamed from spec/frontend/pipelines/graph/job_name_component_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js (renamed from spec/frontend/pipelines/graph/linked_pipeline_spec.js)8
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/linked_pipelines_column_spec.js (renamed from spec/frontend/pipelines/graph/linked_pipelines_column_spec.js)16
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/linked_pipelines_mock_data.js (renamed from spec/frontend/pipelines/graph/linked_pipelines_mock_data.js)0
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/links_inner_spec.js (renamed from spec/frontend/pipelines/graph_shared/links_inner_spec.js)8
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/stage_column_component_spec.js (renamed from spec/frontend/pipelines/graph/stage_column_component_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_details/graph/graph_component_wrapper_spec.js (renamed from spec/frontend/pipelines/graph/graph_component_wrapper_spec.js)18
-rw-r--r--spec/frontend/ci/pipeline_details/graph/mock_data.js (renamed from spec/frontend/pipelines/graph/mock_data.js)8
-rw-r--r--spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js (renamed from spec/frontend/pipelines/pipeline_details_header_spec.js)14
-rw-r--r--spec/frontend/ci/pipeline_details/jobs/components/failed_jobs_table_spec.js (renamed from spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_details/jobs/failed_jobs_app_spec.js (renamed from spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js)8
-rw-r--r--spec/frontend/ci/pipeline_details/jobs/jobs_app_spec.js (renamed from spec/frontend/pipelines/components/jobs/jobs_app_spec.js)8
-rw-r--r--spec/frontend/ci/pipeline_details/linked_pipelines_mock.json (renamed from spec/frontend/pipelines/linked_pipelines_mock.json)0
-rw-r--r--spec/frontend/ci/pipeline_details/mock_data.js (renamed from spec/frontend/pipelines/mock_data.js)102
-rw-r--r--spec/frontend/ci/pipeline_details/pipeline_tabs_spec.js (renamed from spec/frontend/pipelines/pipeline_tabs_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_details/pipelines_store_spec.js (renamed from spec/frontend/pipelines/pipelines_store_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/tabs/pipeline_tabs_spec.js (renamed from spec/frontend/pipelines/components/pipeline_tabs_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/empty_state_spec.js (renamed from spec/frontend/pipelines/test_reports/empty_state_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/mock_data.js (renamed from spec/frontend/pipelines/test_reports/mock_data.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/stores/actions_spec.js (renamed from spec/frontend/pipelines/test_reports/stores/actions_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/stores/getters_spec.js (renamed from spec/frontend/pipelines/test_reports/stores/getters_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/stores/mutations_spec.js (renamed from spec/frontend/pipelines/test_reports/stores/mutations_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/stores/utils_spec.js (renamed from spec/frontend/pipelines/test_reports/stores/utils_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/test_case_details_spec.js (renamed from spec/frontend/pipelines/test_reports/test_case_details_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/test_reports_spec.js (renamed from spec/frontend/pipelines/test_reports/test_reports_spec.js)10
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/test_suite_table_spec.js (renamed from spec/frontend/pipelines/test_reports/test_suite_table_spec.js)10
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/test_summary_spec.js (renamed from spec/frontend/pipelines/test_reports/test_summary_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_details/test_reports/test_summary_table_spec.js (renamed from spec/frontend/pipelines/test_reports/test_summary_table_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_details/utils/index_spec.js (renamed from spec/frontend/pipelines/pipeline_graph/utils_spec.js)8
-rw-r--r--spec/frontend/ci/pipeline_details/utils/parsing_utils_spec.js (renamed from spec/frontend/pipelines/utils_spec.js)14
-rw-r--r--spec/frontend/ci/pipeline_details/utils/unwrapping_utils_spec.js (renamed from spec/frontend/pipelines/unwrapping_utils_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/graph/mock_data.js (renamed from spec/frontend/pipelines/pipeline_graph/mock_data.js)0
-rw-r--r--spec/frontend/ci/pipeline_editor/components/graph/pipeline_graph_spec.js (renamed from spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js)10
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js13
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js2
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/job_item_spec.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js)6
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mock_data.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js)0
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/mock_data.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js)102
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/pipeline_mini_graph_spec.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js)8
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/pipeline_stage_spec.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/pipeline_stages_spec.js (renamed from spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js11
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js36
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js42
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js2
-rw-r--r--spec/frontend/ci/pipelines_page/components/empty_state/ci_templates_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js)2
-rw-r--r--spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js)4
-rw-r--r--spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js (renamed from spec/frontend/pipelines/empty_state_spec.js)6
-rw-r--r--spec/frontend/ci/pipelines_page/components/empty_state/pipelines_ci_templates_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js)4
-rw-r--r--spec/frontend/ci/pipelines_page/components/failure_widget/failed_job_details_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js)6
-rw-r--r--spec/frontend/ci/pipelines_page/components/failure_widget/failed_jobs_list_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js)8
-rw-r--r--spec/frontend/ci/pipelines_page/components/failure_widget/mock.js (renamed from spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js)0
-rw-r--r--spec/frontend/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js)4
-rw-r--r--spec/frontend/ci/pipelines_page/components/failure_widget/utils_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/failure_widget/utils_spec.js)5
-rw-r--r--spec/frontend/ci/pipelines_page/components/nav_controls_spec.js (renamed from spec/frontend/pipelines/nav_controls_spec.js)2
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js (renamed from spec/frontend/pipelines/pipeline_labels_spec.js)4
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_multi_actions_spec.js (renamed from spec/frontend/pipelines/pipeline_multi_actions_spec.js)86
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js (renamed from spec/frontend/pipelines/pipeline_operations_spec.js)8
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js (renamed from spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js)4
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js (renamed from spec/frontend/pipelines/pipeline_triggerer_spec.js)2
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_url_spec.js (renamed from spec/frontend/pipelines/pipeline_url_spec.js)10
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipelines_artifacts_spec.js (renamed from spec/frontend/pipelines/pipelines_artifacts_spec.js)2
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipelines_filtered_search_spec.js (renamed from spec/frontend/pipelines/components/pipelines_filtered_search_spec.js)6
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipelines_manual_actions_spec.js (renamed from spec/frontend/pipelines/pipelines_manual_actions_spec.js)6
-rw-r--r--spec/frontend/ci/pipelines_page/components/time_ago_spec.js (renamed from spec/frontend/pipelines/time_ago_spec.js)2
-rw-r--r--spec/frontend/ci/pipelines_page/pipelines_spec.js (renamed from spec/frontend/pipelines/pipelines_spec.js)15
-rw-r--r--spec/frontend/ci/pipelines_page/tokens/pipeline_branch_name_token_spec.js (renamed from spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js)4
-rw-r--r--spec/frontend/ci/pipelines_page/tokens/pipeline_source_token_spec.js (renamed from spec/frontend/pipelines/tokens/pipeline_source_token_spec.js)4
-rw-r--r--spec/frontend/ci/pipelines_page/tokens/pipeline_status_token_spec.js (renamed from spec/frontend/pipelines/tokens/pipeline_status_token_spec.js)2
-rw-r--r--spec/frontend/ci/pipelines_page/tokens/pipeline_tag_name_token_spec.js (renamed from spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js)4
-rw-r--r--spec/frontend/ci/pipelines_page/tokens/pipeline_trigger_author_token_spec.js (renamed from spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js)4
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap7
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/runner_create_form_spec.js1
-rw-r--r--spec/frontend/ci/runner/components/runner_form_fields_spec.js3
-rw-r--r--spec/frontend/ci/runner/components/runner_managers_table_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js2
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap101
-rw-r--r--spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap19
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap103
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap24
-rw-r--r--spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap47
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js8
-rw-r--r--spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js (renamed from spec/frontend/commit/pipelines/pipelines_table_spec.js)6
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap26
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap20
-rw-r--r--spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap54
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js71
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js2
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_created_spec.js18
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_destroyed_spec.js32
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_updated_spec.js31
-rw-r--r--spec/frontend/contribution_events/components/contribution_events_spec.js8
-rw-r--r--spec/frontend/contribution_events/components/target_link_spec.js2
-rw-r--r--spec/frontend/contribution_events/utils.js95
-rw-r--r--spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap20
-rw-r--r--spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap58
-rw-r--r--spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap30
-rw-r--r--spec/frontend/design_management/components/__snapshots__/image_spec.js.snap18
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap12
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap36
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap66
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js4
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap19
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap10
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap65
-rw-r--r--spec/frontend/diffs/components/app_spec.js23
-rw-r--r--spec/frontend/diffs/components/diff_inline_findings_item_spec.js51
-rw-r--r--spec/frontend/diffs/components/diff_inline_findings_spec.js6
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js3
-rw-r--r--spec/frontend/diffs/components/inline_findings_spec.js6
-rw-r--r--spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap31
-rw-r--r--spec/frontend/diffs/mock_data/inline_findings.js60
-rw-r--r--spec/frontend/drawio/drawio_editor_spec.js5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml11
-rw-r--r--spec/frontend/emoji/index_spec.js17
-rw-r--r--spec/frontend/environments/edit_environment_spec.js20
-rw-r--r--spec/frontend/environments/environment_form_spec.js47
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js45
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js80
-rw-r--r--spec/frontend/feature_flags/components/strategy_spec.js13
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js2
-rw-r--r--spec/frontend/fixtures/abuse_reports.rb28
-rw-r--r--spec/frontend/fixtures/issues.rb2
-rw-r--r--spec/frontend/fixtures/jobs.rb10
-rw-r--r--spec/frontend/fixtures/pipeline_header.rb2
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb29
-rw-r--r--spec/frontend/fixtures/pipelines.rb2
-rw-r--r--spec/frontend/fixtures/snippet.rb4
-rw-r--r--spec/frontend/groups/components/empty_states/groups_dashboard_empty_state_spec.js29
-rw-r--r--spec/frontend/groups/components/empty_states/groups_explore_empty_state_spec.js27
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js168
-rw-r--r--spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap4
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js3
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js2
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js2
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap14
-rw-r--r--spec/frontend/integrations/index/mock_data.js6
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js145
-rw-r--r--spec/frontend/invite_members/mock_data/api_responses.js6
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js14
-rw-r--r--spec/frontend/invite_members/utils/member_utils_spec.js22
-rw-r--r--spec/frontend/issuable/components/csv_export_modal_spec.js2
-rw-r--r--spec/frontend/issuable/components/issuable_header_warnings_spec.js105
-rw-r--r--spec/frontend/issuable/components/status_badge_spec.js43
-rw-r--r--spec/frontend/issuable/components/status_box_spec.js50
-rw-r--r--spec/frontend/issuable/popover/components/issue_popover_spec.js6
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js1
-rw-r--r--spec/frontend/issues/dashboard/mock_data.js1
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js125
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js1
-rw-r--r--spec/frontend/issues/list/mock_data.js1
-rw-r--r--spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap10
-rw-r--r--spec/frontend/issues/service_desk/components/empty_state_with_any_issues_spec.js (renamed from spec/frontend/service_desk/components/empty_state_with_any_issues_spec.js)4
-rw-r--r--spec/frontend/issues/service_desk/components/empty_state_without_any_issues_spec.js (renamed from spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js)8
-rw-r--r--spec/frontend/issues/service_desk/components/info_banner_spec.js (renamed from spec/frontend/service_desk/components/info_banner_spec.js)4
-rw-r--r--spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js717
-rw-r--r--spec/frontend/issues/service_desk/mock_data.js (renamed from spec/frontend/service_desk/mock_data.js)17
-rw-r--r--spec/frontend/issues/show/components/app_spec.js154
-rw-r--r--spec/frontend/issues/show/components/sticky_header_spec.js135
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js52
-rw-r--r--spec/frontend/issues/show/issue_spec.js43
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js3
-rw-r--r--spec/frontend/issues/show/store_spec.js39
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js146
-rw-r--r--spec/frontend/jira_connect/branches/mock_data.js15
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap13
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap86
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js71
-rw-r--r--spec/frontend/jobs/components/filtered_search/utils_spec.js19
-rw-r--r--spec/frontend/lib/utils/array_utility_spec.js36
-rw-r--r--spec/frontend/lib/utils/breadcrumbs_spec.js84
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js39
-rw-r--r--spec/frontend/lib/utils/datetime_range_spec.js382
-rw-r--r--spec/frontend/lib/utils/secret_detection_spec.js1
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js17
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js10
-rw-r--r--spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap26
-rw-r--r--spec/frontend/merge_request_tabs_spec.js16
-rw-r--r--spec/frontend/merge_requests/components/compare_app_spec.js54
-rw-r--r--spec/frontend/merge_requests/components/header_metadata_spec.js93
-rw-r--r--spec/frontend/nav/components/top_nav_new_dropdown_spec.js3
-rw-r--r--spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap40
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js5
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js1
-rw-r--r--spec/frontend/notes/stores/actions_spec.js174
-rw-r--r--spec/frontend/observability/client_spec.js129
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/app_spec.js19
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js88
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js88
-rw-r--r--spec/frontend/organizations/groups_and_projects/mock_data.js252
-rw-r--r--spec/frontend/organizations/shared/components/groups_view_spec.js146
-rw-r--r--spec/frontend/organizations/shared/components/projects_view_spec.js146
-rw-r--r--spec/frontend/organizations/shared/utils_spec.js (renamed from spec/frontend/organizations/groups_and_projects/utils_spec.js)14
-rw-r--r--spec/frontend/organizations/show/components/app_spec.js49
-rw-r--r--spec/frontend/organizations/show/components/association_count_card_spec.js48
-rw-r--r--spec/frontend/organizations/show/components/association_counts_spec.js61
-rw-r--r--spec/frontend/organizations/show/components/groups_and_projects_spec.js106
-rw-r--r--spec/frontend/organizations/show/components/organization_avatar_spec.js64
-rw-r--r--spec/frontend/organizations/show/utils_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap6
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap18
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js71
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/utils_spec.js25
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap11
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/terraform_installation_spec.js.snap4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap25
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap42
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap11
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap67
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap112
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap47
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js20
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js105
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js21
-rw-r--r--spec/frontend/packages_and_registries/package_registry/utils_spec.js52
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap6
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap24
-rw-r--r--spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js4
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js48
-rw-r--r--spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js9
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js105
-rw-r--r--spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js6
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js92
-rw-r--r--spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap230
-rw-r--r--spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap30
-rw-r--r--spec/frontend/pipelines/notification/mock_data.js33
-rw-r--r--spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap210
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js9
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js136
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap3
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap1
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap8
-rw-r--r--spec/frontend/projects/settings/access_dropdown_spec.js204
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js55
-rw-r--r--spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js13
-rw-r--r--spec/frontend/projects/settings_service_desk/components/custom_email_wrapper_spec.js21
-rw-r--r--spec/frontend/protected_branches/protected_branch_create_spec.js51
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js2
-rw-r--r--spec/frontend/protected_tags/mock_data.js18
-rw-r--r--spec/frontend/protected_tags/protected_tag_edit_spec.js113
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap48
-rw-r--r--spec/frontend/releases/components/__snapshots__/issuable_stats_spec.js.snap67
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js6
-rw-r--r--spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap18
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap44
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js81
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap114
-rw-r--r--spec/frontend/repository/mock_data.js11
-rw-r--r--spec/frontend/search/mock_data.js2
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js117
-rw-r--r--spec/frontend/search/sidebar/components/blobs_filters_spec.js85
-rw-r--r--spec/frontend/search/sidebar/components/commits_filters_spec.js28
-rw-r--r--spec/frontend/search/sidebar/components/issues_filters_spec.js98
-rw-r--r--spec/frontend/search/sidebar/components/merge_requests_filters_spec.js123
-rw-r--r--spec/frontend/search/sidebar/components/notes_filters_spec.js28
-rw-r--r--spec/frontend/search/sidebar/components/projects_filters_spec.js (renamed from spec/frontend/search/sidebar/components/projects_filters_specs.js)2
-rw-r--r--spec/frontend/search/sidebar/components/small_screen_drawer_navigation_spec.js68
-rw-r--r--spec/frontend/search/store/actions_spec.js16
-rw-r--r--spec/frontend/security_configuration/components/continuous_vulnerability_scan_spec.js124
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js18
-rw-r--r--spec/frontend/security_configuration/utils_spec.js64
-rw-r--r--spec/frontend/sentry/index_spec.js104
-rw-r--r--spec/frontend/sentry/init_sentry_spec.js177
-rw-r--r--spec/frontend/sentry/legacy_index_spec.js6
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js103
-rw-r--r--spec/frontend/service_desk/components/service_desk_list_app_spec.js376
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js2
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js86
-rw-r--r--spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js7
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js27
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js37
-rw-r--r--spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap4
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js6
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap4
-rw-r--r--spec/frontend/sidebar/mock_data.js80
-rw-r--r--spec/frontend/silent_mode_settings/components/app_spec.js133
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap3
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap26
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap25
-rw-r--r--spec/frontend/snippets/components/embed_dropdown_spec.js48
-rw-r--r--spec/frontend/super_sidebar/components/context_header_spec.js50
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_spec.js302
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js39
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js21
-rw-r--r--spec/frontend/super_sidebar/components/flyout_menu_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/frequent_items_list_spec.js85
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap27
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js42
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js17
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js16
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js63
-rw-r--r--spec/frontend/super_sidebar/components/global_search/mock_data.js44
-rw-r--r--spec/frontend/super_sidebar/components/global_search/utils_spec.js88
-rw-r--r--spec/frontend/super_sidebar/components/groups_list_spec.js90
-rw-r--r--spec/frontend/super_sidebar/components/items_list_spec.js63
-rw-r--r--spec/frontend/super_sidebar/components/menu_section_spec.js36
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js97
-rw-r--r--spec/frontend/super_sidebar/components/pinned_section_spec.js29
-rw-r--r--spec/frontend/super_sidebar/components/projects_list_spec.js85
-rw-r--r--spec/frontend/super_sidebar/components/search_results_spec.js69
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_hover_peek_behavior_spec.js213
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_menu_spec.js69
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js25
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js111
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js23
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js20
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js21
-rw-r--r--spec/frontend/super_sidebar/mock_data.js46
-rw-r--r--spec/frontend/super_sidebar/mocks.js24
-rw-r--r--spec/frontend/super_sidebar/utils_spec.js78
-rw-r--r--spec/frontend/time_tracking/components/timelogs_app_spec.js25
-rw-r--r--spec/frontend/tracing/components/tracing_details_spec.js103
-rw-r--r--spec/frontend/tracing/components/tracing_empty_state_spec.js39
-rw-r--r--spec/frontend/tracing/components/tracing_list_filtered_search_spec.js38
-rw-r--r--spec/frontend/tracing/components/tracing_list_spec.js216
-rw-r--r--spec/frontend/tracing/components/tracing_table_list_spec.js77
-rw-r--r--spec/frontend/tracing/details_index_spec.js42
-rw-r--r--spec/frontend/tracing/filters_spec.js141
-rw-r--r--spec/frontend/tracing/list_index_spec.js37
-rw-r--r--spec/frontend/tracking/dispatch_snowplow_event_spec.js76
-rw-r--r--spec/frontend/tracking/internal_events_spec.js147
-rw-r--r--spec/frontend/tracking/mock_data.js17
-rw-r--r--spec/frontend/tracking/tracking_initialization_spec.js29
-rw-r--r--spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js29
-rw-r--r--spec/frontend/usage_quotas/storage/components/usage_graph_spec.js125
-rw-r--r--spec/frontend/user_lists/components/user_list_spec.js2
-rw-r--r--spec/frontend/users_select/test_helper.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/components/action_buttons_spec.js (renamed from spec/frontend/vue_merge_request_widget/components/action_buttons.js)26
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap18
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap154
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/app_spec.js45
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js72
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js31
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js16
-rw-r--r--spec/frontend/vue_merge_request_widget/mock_data.js168
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js811
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap32
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap6
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap59
-rw-r--r--spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap6
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap29
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js77
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/code_block_highlighted_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/code_block_spec.js56
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js42
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js62
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js190
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js326
-rw-r--r--spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap12
-rw-r--r--spec/frontend/vue_shared/components/entity_select/utils_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js16
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/groups_list/groups_list_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/groups_list/mock_data.js6
-rw-r--r--spec/frontend/vue_shared/components/list_actions/list_actions_spec.js135
-rw-r--r--spec/frontend/vue_shared/components/markdown/__snapshots__/suggestion_diff_spec.js.snap3
-rw-r--r--spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_view_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap13
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap18
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap15
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_system_note_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap14
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap6
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap12
-rw-r--r--spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap15
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap13
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js117
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap328
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js28
-rw-r--r--spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap30
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js2
-rw-r--r--spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js33
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js12
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/mock_data.js1
-rw-r--r--spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js20
-rw-r--r--spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js64
-rw-r--r--spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap168
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap40
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap12
-rw-r--r--spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js27
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js14
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js7
-rw-r--r--spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js14
-rw-r--r--spec/frontend/work_items/components/shared/work_item_links_menu_spec.js8
-rw-r--r--spec/frontend/work_items/components/shared/work_item_token_input_spec.js81
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js21
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js84
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js50
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js12
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap29
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js41
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js93
-rw-r--r--spec/frontend/work_items/components/work_item_state_badge_spec.js5
-rw-r--r--spec/frontend/work_items/list/components/work_items_list_app_spec.js18
-rw-r--r--spec/frontend/work_items/mock_data.js192
-rw-r--r--spec/frontend/work_items/utils_spec.js13
565 files changed, 12661 insertions, 11043 deletions
diff --git a/spec/frontend/__helpers__/clean_html_element_serializer.js b/spec/frontend/__helpers__/clean_html_element_serializer.js
new file mode 100644
index 00000000000..d787f5126ec
--- /dev/null
+++ b/spec/frontend/__helpers__/clean_html_element_serializer.js
@@ -0,0 +1,142 @@
+// slot-scope attribute is a result of Vue.js 3 stubs being serialized in slot context, drop it
+// modelModifiers are result of Vue.js 3 model modifiers handling and should not be in snapshot
+const ATTRIBUTES_TO_REMOVE = ['slot-scope', 'modelmodifiers'];
+// Taken from https://github.com/vuejs/vue/blob/72aed6a149b94b5b929fb47370a7a6d4cb7491c5/src/platforms/web/util/attrs.ts#L37-L44
+const BOOLEAN_ATTRIBUTES = new Set(
+ (
+ 'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' +
+ 'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' +
+ 'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' +
+ 'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' +
+ 'required,reversed,scoped,seamless,selected,sortable,' +
+ 'truespeed,typemustmatch,visible'
+ ).split(','),
+);
+
+function sortClassesAlphabetically(node) {
+ // Make classes render in alphabetical order for both Vue2 and Vue3
+ if (node.hasAttribute('class')) {
+ const classes = node.getAttribute('class');
+ if (classes === '') {
+ node.removeAttribute('class');
+ } else {
+ node.setAttribute('class', Array.from(node.classList).sort().join(' '));
+ }
+ }
+}
+
+const TRANSITION_VALUES_TO_REMOVE = [
+ { attributeName: 'css', defaultValue: 'true' },
+ { attributeName: 'persisted', defaultValue: 'true' },
+];
+function removeInternalPropsLeakingToTransitionStub(node) {
+ TRANSITION_VALUES_TO_REMOVE.forEach((hash) => {
+ if (node.getAttribute(hash.attributeName) === hash.defaultValue) {
+ node.removeAttribute(hash.attributeName);
+ }
+ });
+}
+
+function normalizeText(node) {
+ const newText = node.textContent.trim();
+ const textWithoutNewLines = newText.replace(/\n/g, '');
+ const textWithoutDeepSpace = textWithoutNewLines.replace(/(?<=\S)\s+/g, ' ');
+ // eslint-disable-next-line no-param-reassign
+ node.textContent = textWithoutDeepSpace;
+}
+
+const visited = new WeakSet();
+
+// Lovingly borrowed from https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#whitespace_helper_functions
+function isAllWhitespace(node) {
+ return !/[^\t\n\r ]/.test(node.textContent);
+}
+
+function isIgnorable(node) {
+ return (
+ node.nodeType === Node.COMMENT_NODE || // A comment node
+ (node.nodeType === Node.TEXT_NODE && isAllWhitespace(node))
+ ); // a text node, all ws
+}
+
+const REFERENCE_ATTRIBUTES = ['aria-controls', 'aria-labelledby', 'for'];
+function updateIdTags(root) {
+ const elementsWithIds = [...(root.id ? [root] : []), ...root.querySelectorAll('[id]')];
+
+ const referenceSelector = REFERENCE_ATTRIBUTES.map((attr) => `[${attr}]`).join(',');
+ const elementsWithReference = [
+ ...(root.matches(referenceSelector) ? [root] : []),
+ ...root.querySelectorAll(REFERENCE_ATTRIBUTES.map((attr) => `[${attr}]`).join(',')),
+ ];
+
+ elementsWithReference.forEach((el) => {
+ REFERENCE_ATTRIBUTES.filter((attr) => el.getAttribute(attr)).forEach((target) => {
+ const index = elementsWithIds.findIndex((t) => t.id === el.getAttribute(target));
+ if (index !== -1) {
+ el.setAttribute(target, `reference-${index}`);
+ }
+ });
+ });
+
+ elementsWithIds.forEach((el, index) => {
+ el.setAttribute('id', `reference-${index}`);
+ });
+}
+
+export function test(received) {
+ return received instanceof Element && !visited.has(received);
+}
+
+export function serialize(received, config, indentation, depth, refs, printer) {
+ // Explicitly set empty string values of img.src to `null` as Vue3 does
+ // We need to do this before `clone`, otherwise src prop diff will be lost
+ received.querySelectorAll('img').forEach((img) => img.setAttribute('src', img.src || null));
+
+ const clone = received.cloneNode(true);
+
+ updateIdTags(clone);
+ visited.add(clone);
+
+ const iterator = document.createNodeIterator(
+ clone,
+ // eslint-disable-next-line no-bitwise
+ NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
+ );
+ const ignorableNodes = [];
+
+ for (let currentNode = iterator.nextNode(); currentNode; currentNode = iterator.nextNode()) {
+ if (isIgnorable(currentNode)) {
+ ignorableNodes.push(currentNode);
+ } else {
+ if (currentNode instanceof Element) {
+ ATTRIBUTES_TO_REMOVE.forEach((attr) => currentNode.removeAttribute(attr));
+
+ if (!currentNode.tagName.includes('-')) {
+ // We want to normalize boolean attributes rendering only on native tags
+ BOOLEAN_ATTRIBUTES.forEach((attr) => {
+ if (currentNode.hasAttribute(attr) && currentNode.getAttribute(attr) === attr) {
+ currentNode.setAttribute(attr, '');
+ }
+ });
+ }
+
+ sortClassesAlphabetically(currentNode);
+
+ if (currentNode.tagName === 'TRANSITION-STUB') {
+ removeInternalPropsLeakingToTransitionStub(currentNode);
+ }
+ }
+
+ if (currentNode.nodeType === Node.TEXT_NODE) {
+ normalizeText(currentNode);
+ }
+
+ currentNode.normalize();
+ visited.add(currentNode);
+ }
+ }
+
+ ignorableNodes.forEach((x) => x.remove());
+
+ return printer(clone, config, indentation, depth, refs);
+}
diff --git a/spec/frontend/__helpers__/dom_shims/get_client_rects.js b/spec/frontend/__helpers__/dom_shims/get_client_rects.js
index 7ba60dd7936..0ec3525f0ef 100644
--- a/spec/frontend/__helpers__/dom_shims/get_client_rects.js
+++ b/spec/frontend/__helpers__/dom_shims/get_client_rects.js
@@ -1,7 +1,8 @@
function hasHiddenStyle(node) {
if (!node.style) {
return false;
- } else if (node.style.display === 'none' || node.style.visibility === 'hidden') {
+ }
+ if (node.style.display === 'none' || node.style.visibility === 'hidden') {
return true;
}
diff --git a/spec/frontend/__helpers__/html_string_serializer.js b/spec/frontend/__helpers__/html_string_serializer.js
new file mode 100644
index 00000000000..99f4acd0e97
--- /dev/null
+++ b/spec/frontend/__helpers__/html_string_serializer.js
@@ -0,0 +1,11 @@
+export function test(received) {
+ return received && typeof received === 'string' && received.startsWith('<');
+}
+
+export function serialize(received, config, indentation, depth, refs, printer) {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(received, 'text/html');
+ const el = doc.body.firstElementChild;
+
+ return printer(el, config, indentation, depth, refs);
+}
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js
index c144a256dce..20a79fc4d2f 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper.js
@@ -1,6 +1,13 @@
import * as testingLibrary from '@testing-library/dom';
-import { createWrapper, WrapperArray, ErrorWrapper, mount, shallowMount } from '@vue/test-utils';
-import { isArray, upperFirst } from 'lodash';
+import {
+ createWrapper,
+ Wrapper, // eslint-disable-line no-unused-vars
+ ErrorWrapper,
+ mount,
+ shallowMount,
+ WrapperArray,
+} from '@vue/test-utils';
+import { compose } from 'lodash/fp';
const vNodeContainsText = (vnode, text) =>
(vnode.text && vnode.text.includes(text)) ||
@@ -14,7 +21,7 @@ const vNodeContainsText = (vnode, text) =>
*
* @param {HTMLElement} element
* @param {Object} options
- * @returns VTU wrapper
+ * @returns {Wrapper} VTU wrapper
*/
const createWrapperFromElement = (element, options) =>
// eslint-disable-next-line no-underscore-dangle
@@ -52,19 +59,84 @@ export const waitForMutation = (store, expectedMutationType) =>
});
});
+/**
+ * Query function type
+ * @callback FindFunction
+ * @param text
+ * @returns {Wrapper}
+ */
+
+/**
+ * Query all function type
+ * @callback FindAllFunction
+ * @param text
+ * @returns {WrapperArray}
+ */
+
+/**
+ * Query find with options functions type
+ * @callback FindWithOptionsFunction
+ * @param text
+ * @param options
+ * @returns {Wrapper}
+ */
+
+/**
+ * Query find all with options functions type
+ * @callback FindAllWithOptionsFunction
+ * @param text
+ * @param options
+ * @returns {WrapperArray}
+ */
+
+/**
+ * Extended Wrapper queries
+ * @typedef { {
+ * findByTestId: FindFunction,
+ * findAllByTestId: FindAllFunction,
+ * findComponentByTestId: FindFunction,
+ * findAllComponentsByTestId: FindAllFunction,
+ * findByRole: FindWithOptionsFunction,
+ * findAllByRole: FindAllWithOptionsFunction,
+ * findByLabelText: FindWithOptionsFunction,
+ * findAllByLabelText: FindAllWithOptionsFunction,
+ * findByPlaceholderText: FindWithOptionsFunction,
+ * findAllByPlaceholderText: FindAllWithOptionsFunction,
+ * findByText: FindWithOptionsFunction,
+ * findAllByText: FindAllWithOptionsFunction,
+ * findByDisplayValue: FindWithOptionsFunction,
+ * findAllByDisplayValue: FindAllWithOptionsFunction,
+ * findByAltText: FindWithOptionsFunction,
+ * findAllByAltText: FindAllWithOptionsFunction,
+ * findByTitle: FindWithOptionsFunction,
+ * findAllByTitle: FindAllWithOptionsFunction
+ * } } ExtendedQueries
+ */
+
+/**
+ * Extended Wrapper
+ * @typedef {(Wrapper & ExtendedQueries)} ExtendedWrapper
+ */
+
+/**
+ * Creates a Wrapper {@link https://v1.test-utils.vuejs.org/api/wrapper/} with
+ * Additional Queries {@link https://testing-library.com/docs/queries/about}.
+ * @param { Wrapper } wrapper
+ * @returns { ExtendedWrapper }
+ */
export const extendedWrapper = (wrapper) => {
// https://testing-library.com/docs/queries/about
const AVAILABLE_QUERIES = [
- 'byRole',
- 'byLabelText',
- 'byPlaceholderText',
- 'byText',
- 'byDisplayValue',
- 'byAltText',
- 'byTitle',
+ 'ByRole',
+ 'ByLabelText',
+ 'ByPlaceholderText',
+ 'ByText',
+ 'ByDisplayValue',
+ 'ByAltText',
+ 'ByTitle',
];
- if (isArray(wrapper) || !wrapper?.find) {
+ if (Array.isArray(wrapper) || !wrapper?.find) {
// eslint-disable-next-line no-console
console.warn(
'[vue-test-utils-helper]: you are trying to extend an object that is not a VueWrapper.',
@@ -74,11 +146,13 @@ export const extendedWrapper = (wrapper) => {
return Object.defineProperties(wrapper, {
findByTestId: {
+ /** @this { Wrapper } */
value(id) {
return this.find(`[data-testid="${id}"]`);
},
},
findAllByTestId: {
+ /** @this { Wrapper } */
value(id) {
return this.findAll(`[data-testid="${id}"]`);
},
@@ -88,6 +162,7 @@ export const extendedWrapper = (wrapper) => {
* with CSS selectors: https://v1.test-utils.vuejs.org/api/wrapper/#findcomponent
*/
findComponentByTestId: {
+ /** @this { Wrapper } */
value(id) {
return this.findComponent(`[data-testid="${id}"]`);
},
@@ -97,6 +172,7 @@ export const extendedWrapper = (wrapper) => {
* with CSS selectors: https://v1.test-utils.vuejs.org/api/wrapper/#findallcomponents
*/
findAllComponentsByTestId: {
+ /** @this { Wrapper } */
value(id) {
return this.findAllComponents(`[data-testid="${id}"]`);
},
@@ -105,13 +181,10 @@ export const extendedWrapper = (wrapper) => {
...AVAILABLE_QUERIES.reduce((accumulator, query) => {
return {
...accumulator,
- [`find${upperFirst(query)}`]: {
+ [`find${query}`]: {
+ /** @this { Wrapper } */
value(text, options = {}) {
- const elements = testingLibrary[`queryAll${upperFirst(query)}`](
- wrapper.element,
- text,
- options,
- );
+ const elements = testingLibrary[`queryAll${query}`](this.element, text, options);
// Element not found, return an `ErrorWrapper`
if (!elements.length) {
@@ -126,13 +199,10 @@ export const extendedWrapper = (wrapper) => {
...AVAILABLE_QUERIES.reduce((accumulator, query) => {
return {
...accumulator,
- [`findAll${upperFirst(query)}`]: {
+ [`findAll${query}`]: {
+ /** @this { Wrapper } */
value(text, options = {}) {
- const elements = testingLibrary[`queryAll${upperFirst(query)}`](
- wrapper.element,
- text,
- options,
- );
+ const elements = testingLibrary[`queryAll${query}`](this.element, text, options);
const wrappers = elements.map((element) => {
const elementWrapper = createWrapperFromElement(element, this.options);
@@ -152,6 +222,5 @@ export const extendedWrapper = (wrapper) => {
});
};
-export const shallowMountExtended = (...args) => extendedWrapper(shallowMount(...args));
-
-export const mountExtended = (...args) => extendedWrapper(mount(...args));
+export const shallowMountExtended = compose(extendedWrapper, shallowMount);
+export const mountExtended = compose(extendedWrapper, mount);
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
index 2f69a2348d9..c137561154d 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
@@ -374,34 +374,34 @@ describe('Vue test utils helpers', () => {
});
});
- describe.each`
- mountExtendedFunction | expectedMountFunction
- ${shallowMountExtended} | ${'shallowMount'}
- ${mountExtended} | ${'mount'}
- `('$mountExtendedFunction', ({ mountExtendedFunction, expectedMountFunction }) => {
- const FakeComponent = jest.fn();
- const options = {
- propsData: {
- foo: 'bar',
- },
- };
-
- beforeEach(() => {
- const mockWrapper = { find: jest.fn() };
- jest.spyOn(vtu, expectedMountFunction).mockImplementation(() => mockWrapper);
+ describe('mount extended functions', () => {
+ // eslint-disable-next-line vue/one-component-per-file
+ const FakeChildComponent = Vue.component('FakeChildComponent', {
+ template: '<div>Bar <div data-testid="fake-id"/></div>',
});
- it(`calls \`${expectedMountFunction}\` with passed arguments`, () => {
- mountExtendedFunction(FakeComponent, options);
-
- expect(vtu[expectedMountFunction]).toHaveBeenCalledWith(FakeComponent, options);
+ // eslint-disable-next-line vue/one-component-per-file
+ const FakeComponent = Vue.component('FakeComponent', {
+ components: {
+ FakeChildComponent,
+ },
+ template: '<div>Foo <fake-child-component data-testid="fake-id" /></div>',
});
- it('returns extended wrapper', () => {
- const result = mountExtendedFunction(FakeComponent, options);
+ describe('mountExtended', () => {
+ it('mounts component and provides extended queries', () => {
+ const wrapper = mountExtended(FakeComponent);
+ expect(wrapper.text()).toBe('Foo Bar');
+ expect(wrapper.findAllByTestId('fake-id').length).toBe(2);
+ });
+ });
- expect(result).toHaveProperty('find');
- expect(result).toHaveProperty('findByTestId');
+ describe('shallowMountExtended', () => {
+ it('shallow mounts component and provides extended queries', () => {
+ const wrapper = shallowMountExtended(FakeComponent);
+ expect(wrapper.text()).toBe('Foo');
+ expect(wrapper.findAllByTestId('fake-id').length).toBe(1);
+ });
});
});
});
diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
index 5236f38dc35..ae767f8b3f5 100644
--- a/spec/frontend/access_tokens/components/access_token_table_app_spec.js
+++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js
@@ -157,9 +157,9 @@ describe('~/access_tokens/components/access_token_table_app', () => {
href: '/-/profile/personal_access_tokens/1/revoke',
'data-confirm': sprintf(
__(
- 'Are you sure you want to revoke this %{accessTokenType}? This action cannot be undone.',
+ 'Are you sure you want to revoke the %{accessTokenType} "%{tokenName}"? This action cannot be undone.',
),
- { accessTokenType },
+ { accessTokenType, tokenName: 'a' },
),
});
expect(button.props('category')).toBe('tertiary');
diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
index ddeab3e3b62..fca17f948f8 100644
--- a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
+++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
@@ -24,7 +24,6 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
<gl-tab-stub
titlelinkclass=""
>
-
<div
class="gl-mt-3"
>
@@ -38,7 +37,6 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
searchtextoptionlabel="Search for this text"
value=""
/>
-
<review-tab-container-stub
commits=""
emptylisttext="Your search didn't match any commits. Try a different query."
@@ -46,11 +44,9 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
/>
</div>
</gl-tab-stub>
-
<gl-tab-stub
titlelinkclass=""
>
-
<review-tab-container-stub
commits=""
emptylisttext="Commits you select appear here. Go to the first tab and select commits to add to this merge request."
diff --git a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
index e519684bbc5..4340699a7ed 100644
--- a/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
@@ -1,28 +1,46 @@
-import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import AbuseReportApp from '~/admin/abuse_report/components/abuse_report_app.vue';
import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
import UserDetails from '~/admin/abuse_report/components/user_details.vue';
+import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
import ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
-import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
+import ActivityEventsList from '~/admin/abuse_report/components/activity_events_list.vue';
+import ActivityHistoryItem from '~/admin/abuse_report/components/activity_history_item.vue';
import { SUCCESS_ALERT } from '~/admin/abuse_report/constants';
import { mockAbuseReport } from '../mock_data';
describe('AbuseReportApp', () => {
let wrapper;
+ const { similarOpenReports } = mockAbuseReport.user;
+
const findAlert = () => wrapper.findComponent(GlAlert);
const findReportHeader = () => wrapper.findComponent(ReportHeader);
const findUserDetails = () => wrapper.findComponent(UserDetails);
- const findReportedContent = () => wrapper.findComponent(ReportedContent);
- const findHistoryItems = () => wrapper.findComponent(HistoryItems);
- const createComponent = (props = {}) => {
- wrapper = shallowMount(AbuseReportApp, {
+ const findReportedContent = () => wrapper.findByTestId('reported-content');
+ const findReportedContentForSimilarReports = () =>
+ wrapper.findAllByTestId('reported-content-similar-open-reports');
+ const firstReportedContentForSimilarReports = () =>
+ findReportedContentForSimilarReports().at(0).findComponent(ReportedContent);
+
+ const findActivityList = () => wrapper.findComponent(ActivityEventsList);
+ const findActivityItem = () => wrapper.findByTestId('activity');
+ const findActivityForSimilarReports = () =>
+ wrapper.findAllByTestId('activity-similar-open-reports');
+ const firstActivityForSimilarReports = () =>
+ findActivityForSimilarReports().at(0).findComponent(ActivityHistoryItem);
+
+ const findReportDetails = () => wrapper.findComponent(ReportDetails);
+
+ const createComponent = (props = {}, provide = {}) => {
+ wrapper = shallowMountExtended(AbuseReportApp, {
propsData: {
abuseReport: mockAbuseReport,
...props,
},
+ provide,
});
};
@@ -64,7 +82,7 @@ describe('AbuseReportApp', () => {
});
});
- describe('ReportHeader', () => {
+ describe('Report header', () => {
it('renders ReportHeader', () => {
expect(findReportHeader().props('user')).toBe(mockAbuseReport.user);
expect(findReportHeader().props('report')).toBe(mockAbuseReport.report);
@@ -83,7 +101,7 @@ describe('AbuseReportApp', () => {
});
});
- describe('UserDetails', () => {
+ describe('User Details', () => {
it('renders UserDetails', () => {
expect(findUserDetails().props('user')).toBe(mockAbuseReport.user);
});
@@ -101,13 +119,47 @@ describe('AbuseReportApp', () => {
});
});
- it('renders ReportedContent', () => {
- expect(findReportedContent().props('report')).toBe(mockAbuseReport.report);
- expect(findReportedContent().props('reporter')).toBe(mockAbuseReport.reporter);
+ describe('Reported Content', () => {
+ it('renders ReportedContent', () => {
+ expect(findReportedContent().props('report')).toBe(mockAbuseReport.report);
+ });
+
+ it('renders similar abuse reports', () => {
+ expect(findReportedContentForSimilarReports()).toHaveLength(similarOpenReports.length);
+ expect(firstReportedContentForSimilarReports().props('report')).toBe(similarOpenReports[0]);
+ });
});
- it('renders HistoryItems', () => {
- expect(findHistoryItems().props('report')).toBe(mockAbuseReport.report);
- expect(findHistoryItems().props('reporter')).toBe(mockAbuseReport.reporter);
+ describe('ReportDetails', () => {
+ describe('when abuseReportLabels feature flag is enabled', () => {
+ it('renders ReportDetails', () => {
+ createComponent({}, { glFeatures: { abuseReportLabels: true } });
+
+ expect(findReportDetails().props('reportId')).toBe(mockAbuseReport.report.globalId);
+ });
+ });
+
+ describe('when abuseReportLabels feature flag is disabled', () => {
+ it('does not render ReportDetails', () => {
+ createComponent({}, { glFeatures: { abuseReportLabels: false } });
+
+ expect(findReportDetails().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Activity', () => {
+ it('renders the activity events list', () => {
+ expect(findActivityList().exists()).toBe(true);
+ });
+
+ it('renders activity item for abuse report', () => {
+ expect(findActivityItem().props('report')).toBe(mockAbuseReport.report);
+ });
+
+ it('renders activity items for similar abuse reports', () => {
+ expect(findActivityForSimilarReports()).toHaveLength(similarOpenReports.length);
+ expect(firstActivityForSimilarReports().props('report')).toBe(similarOpenReports[0]);
+ });
});
});
diff --git a/spec/frontend/admin/abuse_report/components/activity_events_list_spec.js b/spec/frontend/admin/abuse_report/components/activity_events_list_spec.js
new file mode 100644
index 00000000000..cd1120d2db4
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/activity_events_list_spec.js
@@ -0,0 +1,30 @@
+import { shallowMount } from '@vue/test-utils';
+import ActivityEventsList from '~/admin/abuse_report/components/activity_events_list.vue';
+
+describe('ActivityEventsList', () => {
+ let wrapper;
+
+ const mockSlotContent = 'Test slot content';
+
+ const findActivityEventsList = () => wrapper.findComponent(ActivityEventsList);
+
+ const createComponent = () => {
+ wrapper = shallowMount(ActivityEventsList, {
+ slots: {
+ 'history-items': mockSlotContent,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders activity title', () => {
+ expect(findActivityEventsList().text()).toContain('Activity');
+ });
+
+ it('renders history-items slot', () => {
+ expect(findActivityEventsList().text()).toContain(mockSlotContent);
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/history_items_spec.js b/spec/frontend/admin/abuse_report/components/activity_history_item_spec.js
index 86e994fdc57..3f430b0143e 100644
--- a/spec/frontend/admin/abuse_report/components/history_items_spec.js
+++ b/spec/frontend/admin/abuse_report/components/activity_history_item_spec.js
@@ -1,25 +1,23 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { sprintf } from '~/locale';
-import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
+import AcitivityHistoryItem from '~/admin/abuse_report/components/activity_history_item.vue';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { HISTORY_ITEMS_I18N } from '~/admin/abuse_report/constants';
import { mockAbuseReport } from '../mock_data';
-describe('HistoryItems', () => {
+describe('AcitivityHistoryItem', () => {
let wrapper;
- const { report, reporter } = mockAbuseReport;
+ const { report } = mockAbuseReport;
const findHistoryItem = () => wrapper.findComponent(HistoryItem);
const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
const createComponent = (props = {}) => {
- wrapper = shallowMount(HistoryItems, {
+ wrapper = shallowMount(AcitivityHistoryItem, {
propsData: {
report,
- reporter,
...props,
},
stubs: {
@@ -38,8 +36,8 @@ describe('HistoryItems', () => {
describe('rendering the title', () => {
it('renders the reporters name and the category', () => {
- const title = sprintf(HISTORY_ITEMS_I18N.reportedByForCategory, {
- name: reporter.name,
+ const title = sprintf('Reported by %{name} for %{category}.', {
+ name: report.reporter.name,
category: report.category,
});
expect(findHistoryItem().text()).toContain(title);
@@ -47,12 +45,12 @@ describe('HistoryItems', () => {
describe('when the reporter is not defined', () => {
beforeEach(() => {
- createComponent({ reporter: undefined });
+ createComponent({ report: { ...report, reporter: undefined } });
});
it('renders the `No user found` as the reporters name and the category', () => {
- const title = sprintf(HISTORY_ITEMS_I18N.reportedByForCategory, {
- name: HISTORY_ITEMS_I18N.deletedReporter,
+ const title = sprintf('Reported by %{name} for %{category}.', {
+ name: 'No user found',
category: report.category,
});
expect(findHistoryItem().text()).toContain(title);
diff --git a/spec/frontend/admin/abuse_report/components/labels_select_spec.js b/spec/frontend/admin/abuse_report/components/labels_select_spec.js
new file mode 100644
index 00000000000..a22dcc18e10
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/labels_select_spec.js
@@ -0,0 +1,297 @@
+import MockAdapter from 'axios-mock-adapter';
+import { GlButton, GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
+import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import labelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
+import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
+import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
+import { createAlert } from '~/alert';
+import { mockLabelsQueryResponse, mockLabel1, mockLabel2 } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('Labels select component', () => {
+ let mock;
+ let wrapper;
+ let fakeApollo;
+
+ const selectedText = () => wrapper.findByTestId('selected-labels').text();
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEditButton = () => wrapper.findComponent(GlButton);
+ const findDropdown = () => wrapper.findComponent(DropdownWidget);
+ const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
+ const findDropdownValue = () => wrapper.findComponent(DropdownValue);
+ const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
+ const findDropdownFooter = () => wrapper.findComponent(DropdownFooter);
+
+ const labelsQueryHandlerSuccess = jest.fn().mockResolvedValue(mockLabelsQueryResponse);
+ const labelsQueryHandlerFailure = jest.fn().mockRejectedValue(new Error());
+
+ const updatePath = '/admin/abuse_reports/1';
+ const listPath = '/admin/abuse_reports';
+
+ async function openLabelsDropdown() {
+ findEditButton().vm.$emit('click');
+ await waitForPromises();
+ }
+
+ const selectLabel = (label) => {
+ findDropdown().vm.$emit('set-option', label);
+ nextTick();
+ };
+
+ const createComponent = ({ props = {}, labelsQueryHandler = labelsQueryHandlerSuccess } = {}) => {
+ fakeApollo = createMockApollo([[labelsQuery, labelsQueryHandler]]);
+ wrapper = shallowMountExtended(LabelsSelect, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ report: { labels: [] },
+ canEdit: true,
+ ...props,
+ },
+ provide: {
+ updatePath,
+ listPath,
+ },
+ stubs: {
+ GlDropdown,
+ GlDropdownItem,
+ DropdownWidget: stubComponent(DropdownWidget, {
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ methods: { showDropdown: jest.fn() },
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ fakeApollo = null;
+ mock.restore();
+ });
+
+ describe('initial load', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays loading icon', () => {
+ expect(findLoadingIcon().exists()).toEqual(true);
+ });
+
+ it('disables edit button', () => {
+ expect(findEditButton().props('disabled')).toEqual(true);
+ });
+
+ describe('after initial load', () => {
+ beforeEach(() => {
+ wrapper.setProps({ report: { labels: [mockLabel1] } });
+ });
+
+ it('does not display loading icon', () => {
+ expect(findLoadingIcon().exists()).toEqual(false);
+ });
+
+ it('enables edit button', () => {
+ expect(findEditButton().props('disabled')).toEqual(false);
+ });
+
+ it('renders fetched DropdownValue with the correct props', () => {
+ const component = findDropdownValue();
+ expect(component.isVisible()).toBe(true);
+ expect(component.props('selectedLabels')).toEqual([mockLabel1]);
+ expect(component.props('labelsFilterBasePath')).toBe(listPath);
+ });
+ });
+ });
+
+ describe('when there are no selected labels', () => {
+ it('displays "None"', () => {
+ createComponent();
+
+ expect(selectedText()).toContain('None');
+ });
+ });
+
+ describe('when there are selected labels', () => {
+ beforeEach(() => {
+ createComponent({ props: { report: { labels: [mockLabel1, mockLabel2] } } });
+
+ mock.onPut(updatePath).reply(HTTP_STATUS_OK, {});
+ jest.spyOn(axios, 'put');
+ });
+
+ it('renders selected labels in DropdownValue', () => {
+ expect(findDropdownValue().isVisible()).toBe(true);
+ expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1, mockLabel2]);
+ });
+
+ it('selected labels can be removed', async () => {
+ findDropdownValue().vm.$emit('onLabelRemove', mockLabel1.id);
+ await nextTick();
+
+ expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel2]);
+ expect(axios.put).toHaveBeenCalledWith(updatePath, {
+ label_ids: [mockLabel2.id],
+ });
+ });
+ });
+
+ describe('when not editing', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not trigger abuse report labels query', () => {
+ expect(labelsQueryHandlerSuccess).not.toHaveBeenCalled();
+ });
+
+ it('does not render the dropdown', () => {
+ expect(findDropdown().isVisible()).toBe(false);
+ });
+ });
+
+ describe('when editing', () => {
+ beforeEach(async () => {
+ createComponent();
+ await openLabelsDropdown();
+ });
+
+ it('triggers abuse report labels query', () => {
+ expect(labelsQueryHandlerSuccess).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders dropdown with fetched labels', () => {
+ expect(findDropdown().isVisible()).toBe(true);
+ expect(findDropdown().props('options')).toEqual([mockLabel1, mockLabel2]);
+ });
+
+ it('selects/deselects a label', async () => {
+ await selectLabel(mockLabel1);
+
+ expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1]);
+
+ await selectLabel(mockLabel1);
+
+ expect(selectedText()).toContain('None');
+ });
+
+ it('triggers abuse report labels query when search term is set', async () => {
+ findDropdown().vm.$emit('set-search', 'Dos');
+ await waitForPromises();
+
+ expect(labelsQueryHandlerSuccess).toHaveBeenCalledTimes(2);
+ expect(labelsQueryHandlerSuccess).toHaveBeenCalledWith({ searchTerm: 'Dos' });
+ });
+
+ it('does not render DropdownContentsCreateView', () => {
+ expect(findCreateView().exists()).toBe(false);
+ });
+
+ it('renders DropdownFooter', () => {
+ expect(findDropdownFooter().props('footerCreateLabelTitle')).toEqual('Create label');
+ expect(findDropdownFooter().props('footerManageLabelTitle')).toEqual('');
+ });
+
+ describe('when DropdownHeader emits `toggleDropdownContentsCreateView` event', () => {
+ beforeEach(() => {
+ findDropdownHeader().vm.$emit('toggleDropdownContentsCreateView');
+ });
+
+ it('renders DropdownContentsCreateView and removes DropdownFooter', () => {
+ expect(findCreateView().props('workspaceType')).toEqual('abuseReport');
+ expect(findDropdownFooter().exists()).toBe(false);
+ });
+
+ describe('when DropdownContentsCreateView emits `hideCreateView` event', () => {
+ it('removes itself', async () => {
+ findCreateView().vm.$emit('hideCreateView');
+ await nextTick();
+
+ expect(findCreateView().exists()).toBe(false);
+ });
+ });
+
+ describe('when DropdownContentsCreateView emits `labelCreated` event', () => {
+ it('selects created label', async () => {
+ findCreateView().vm.$emit('labelCreated', mockLabel1);
+ await nextTick();
+
+ expect(findDropdownValue().props('selectedLabels')).toEqual([mockLabel1]);
+ });
+ });
+ });
+
+ describe('when DropdownFooter emits `toggleDropdownContentsCreateView` event', () => {
+ it('renders DropdownContentsCreateView', async () => {
+ findDropdownFooter().vm.$emit('toggleDropdownContentsCreateView');
+ await nextTick();
+
+ expect(findCreateView().props('workspaceType')).toEqual('abuseReport');
+ });
+ });
+ });
+
+ describe('after edit', () => {
+ const setup = async (response) => {
+ mock.onPut(updatePath).reply(response, {});
+ jest.spyOn(axios, 'put');
+
+ createComponent();
+ await openLabelsDropdown();
+ await selectLabel(mockLabel1);
+
+ findDropdown().vm.$emit('hide');
+ };
+
+ describe('successful save', () => {
+ it('saves', async () => {
+ await setup(HTTP_STATUS_OK);
+
+ expect(axios.put).toHaveBeenCalledWith(updatePath, {
+ label_ids: [mockLabel1.id],
+ });
+ });
+ });
+
+ describe('unsuccessful save', () => {
+ it('creates an alert', async () => {
+ await setup(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while updating labels.',
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
+ });
+
+ describe('failed abuse report labels query', () => {
+ it('creates an alert', async () => {
+ createComponent({ labelsQueryHandler: labelsQueryHandlerFailure });
+ await openLabelsDropdown();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while searching for labels, please try again.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/report_actions_spec.js b/spec/frontend/admin/abuse_report/components/report_actions_spec.js
index 6dd6d0e55c5..0e20630db14 100644
--- a/spec/frontend/admin/abuse_report/components/report_actions_spec.js
+++ b/spec/frontend/admin/abuse_report/components/report_actions_spec.js
@@ -191,31 +191,4 @@ describe('ReportActions', () => {
);
});
});
-
- describe('when moderateUserPath is not present', () => {
- it('sends the request to updatePath', async () => {
- jest.spyOn(axios, 'put');
- axiosMock.onPut(report.updatePath).replyOnce(HTTP_STATUS_OK, {});
-
- const reportWithoutModerateUserPath = { ...report };
- delete reportWithoutModerateUserPath.moderateUserPath;
-
- createComponent({ report: reportWithoutModerateUserPath });
-
- clickActionsButton();
-
- await nextTick();
-
- selectAction(params.user_action);
- selectReason(params.reason);
-
- await nextTick();
-
- submitForm();
-
- await waitForPromises();
-
- expect(axios.put).toHaveBeenCalledWith(report.updatePath, expect.any(Object));
- });
- });
});
diff --git a/spec/frontend/admin/abuse_report/components/report_details_spec.js b/spec/frontend/admin/abuse_report/components/report_details_spec.js
new file mode 100644
index 00000000000..a5c43dcb82b
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/report_details_spec.js
@@ -0,0 +1,74 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import LabelsSelect from '~/admin/abuse_report/components/labels_select.vue';
+import ReportDetails from '~/admin/abuse_report/components/report_details.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import abuseReportQuery from '~/admin/abuse_report/components/graphql/abuse_report.query.graphql';
+import { createAlert } from '~/alert';
+import { mockAbuseReport, mockLabel1, mockReportQueryResponse } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('Report Details', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findLabelsSelect = () => wrapper.findComponent(LabelsSelect);
+
+ const abuseReportQueryHandlerSuccess = jest.fn().mockResolvedValue(mockReportQueryResponse);
+ const abuseReportQueryHandlerFailure = jest.fn().mockRejectedValue(new Error());
+
+ const createComponent = ({ abuseReportQueryHandler = abuseReportQueryHandlerSuccess } = {}) => {
+ fakeApollo = createMockApollo([[abuseReportQuery, abuseReportQueryHandler]]);
+ wrapper = shallowMount(ReportDetails, {
+ apolloProvider: fakeApollo,
+ propsData: {
+ reportId: mockAbuseReport.report.globalId,
+ },
+ });
+ };
+
+ afterEach(() => {
+ fakeApollo = null;
+ });
+
+ describe('successful abuse report query', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('triggers abuse report query', async () => {
+ await waitForPromises();
+
+ expect(abuseReportQueryHandlerSuccess).toHaveBeenCalledWith({
+ id: mockAbuseReport.report.globalId,
+ });
+ });
+
+ it('renders LabelsSelect with the fetched report', async () => {
+ expect(findLabelsSelect().props('report').labels).toEqual([]);
+
+ await waitForPromises();
+
+ expect(findLabelsSelect().props('report').labels).toEqual([mockLabel1]);
+ });
+ });
+
+ describe('failed abuse report query', () => {
+ beforeEach(async () => {
+ createComponent({ abuseReportQueryHandler: abuseReportQueryHandlerFailure });
+
+ await waitForPromises();
+ });
+
+ it('creates an alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while fetching labels, please try again.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/report_header_spec.js b/spec/frontend/admin/abuse_report/components/report_header_spec.js
index f22f3af091f..6ec380f0387 100644
--- a/spec/frontend/admin/abuse_report/components/report_header_spec.js
+++ b/spec/frontend/admin/abuse_report/components/report_header_spec.js
@@ -54,37 +54,30 @@ describe('ReportHeader', () => {
});
describe.each`
- status | text | variant | className | badgeIcon
- ${STATUS_OPEN} | ${REPORT_HEADER_I18N[STATUS_OPEN]} | ${'success'} | ${'issuable-status-badge-open'} | ${'issues'}
- ${STATUS_CLOSED} | ${REPORT_HEADER_I18N[STATUS_CLOSED]} | ${'info'} | ${'issuable-status-badge-closed'} | ${'issue-closed'}
- `(
- 'rendering the report $status status badge',
- ({ status, text, variant, className, badgeIcon }) => {
- beforeEach(() => {
- createComponent({ report: { ...report, status } });
- });
-
- it(`indicates the ${status} status`, () => {
- expect(findBadge().text()).toBe(text);
- });
-
- it(`with the ${variant} variant`, () => {
- expect(findBadge().props('variant')).toBe(variant);
- });
-
- it(`with the text '${text}' as 'aria-label'`, () => {
- expect(findBadge().attributes('aria-label')).toBe(text);
- });
-
- it(`contains the ${className} class`, () => {
- expect(findBadge().element.classList).toContain(className);
- });
-
- it(`has an icon with the ${badgeIcon} name`, () => {
- expect(findIcon().props('name')).toBe(badgeIcon);
- });
- },
- );
+ status | text | variant | badgeIcon
+ ${STATUS_OPEN} | ${REPORT_HEADER_I18N[STATUS_OPEN]} | ${'success'} | ${'issues'}
+ ${STATUS_CLOSED} | ${REPORT_HEADER_I18N[STATUS_CLOSED]} | ${'info'} | ${'issue-closed'}
+ `('rendering the report $status status badge', ({ status, text, variant, badgeIcon }) => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, status } });
+ });
+
+ it(`indicates the ${status} status`, () => {
+ expect(findBadge().text()).toBe(text);
+ });
+
+ it(`with the ${variant} variant`, () => {
+ expect(findBadge().props('variant')).toBe(variant);
+ });
+
+ it(`with the text '${text}' as 'aria-label'`, () => {
+ expect(findBadge().attributes('aria-label')).toBe(text);
+ });
+
+ it(`has an icon with the ${badgeIcon} name`, () => {
+ expect(findIcon().props('name')).toBe(badgeIcon);
+ });
+ });
it('renders the actions', () => {
const actionsComponent = findActions();
diff --git a/spec/frontend/admin/abuse_report/components/reported_content_spec.js b/spec/frontend/admin/abuse_report/components/reported_content_spec.js
index 9fc49f08f8c..2f16f5a7af2 100644
--- a/spec/frontend/admin/abuse_report/components/reported_content_spec.js
+++ b/spec/frontend/admin/abuse_report/components/reported_content_spec.js
@@ -14,7 +14,7 @@ const modalId = 'abuse-report-screenshot-modal';
describe('ReportedContent', () => {
let wrapper;
- const { report, reporter } = { ...mockAbuseReport };
+ const { report } = { ...mockAbuseReport };
const findScreenshotButton = () => wrapper.findByTestId('screenshot-button');
const findReportUrlButton = () => wrapper.findByTestId('report-url-button');
@@ -32,7 +32,6 @@ describe('ReportedContent', () => {
wrapper = shallowMountExtended(ReportedContent, {
propsData: {
report,
- reporter,
...props,
},
stubs: {
@@ -167,18 +166,18 @@ describe('ReportedContent', () => {
describe('rendering the card footer', () => {
it('renders the reporters avatar', () => {
- expect(findAvatar().props('src')).toBe(reporter.avatarUrl);
+ expect(findAvatar().props('src')).toBe(report.reporter.avatarUrl);
});
it('renders the users name', () => {
- expect(findCardFooter().text()).toContain(reporter.name);
+ expect(findCardFooter().text()).toContain(report.reporter.name);
});
it('renders a link to the users profile page', () => {
const link = findProfileLink();
- expect(link.attributes('href')).toBe(reporter.path);
- expect(link.text()).toBe(`@${reporter.username}`);
+ expect(link.attributes('href')).toBe(report.reporter.path);
+ expect(link.text()).toBe(`@${report.reporter.username}`);
});
it('renders the time-ago tooltip', () => {
diff --git a/spec/frontend/admin/abuse_report/components/user_details_spec.js b/spec/frontend/admin/abuse_report/components/user_details_spec.js
index ca499fbaa6e..f3d8d5bb610 100644
--- a/spec/frontend/admin/abuse_report/components/user_details_spec.js
+++ b/spec/frontend/admin/abuse_report/components/user_details_spec.js
@@ -18,7 +18,7 @@ describe('UserDetails', () => {
const findLinkFor = (attribute) => findLinkIn(findUserDetail(attribute));
const findTimeIn = (component) => component.findComponent(TimeAgoTooltip).props('time');
const findTimeFor = (attribute) => findTimeIn(findUserDetail(attribute));
- const findOtherReport = (index) => wrapper.findByTestId(`other-report-${index}`);
+ const findPastReport = (index) => wrapper.findByTestId(`past-report-${index}`);
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(UserDetails, {
@@ -38,8 +38,8 @@ describe('UserDetails', () => {
describe('createdAt', () => {
it('renders the users createdAt with the correct label', () => {
- expect(findUserDetailLabel('createdAt')).toBe(USER_DETAILS_I18N.createdAt);
- expect(findTimeFor('createdAt')).toBe(user.createdAt);
+ expect(findUserDetailLabel('created-at')).toBe(USER_DETAILS_I18N.createdAt);
+ expect(findTimeFor('created-at')).toBe(user.createdAt);
});
});
@@ -67,32 +67,34 @@ describe('UserDetails', () => {
describe('creditCard', () => {
it('renders the correct label', () => {
- expect(findUserDetailLabel('creditCard')).toBe(USER_DETAILS_I18N.creditCard);
+ expect(findUserDetailLabel('credit-card-verification')).toBe(USER_DETAILS_I18N.creditCard);
});
it('renders the users name', () => {
- expect(findUserDetail('creditCard').text()).toContain(
+ expect(findUserDetail('credit-card-verification').text()).toContain(
sprintf(USER_DETAILS_I18N.registeredWith, { ...user.creditCard }),
);
- expect(findUserDetail('creditCard').text()).toContain(user.creditCard.name);
+ expect(findUserDetail('credit-card-verification').text()).toContain(user.creditCard.name);
});
describe('similar credit cards', () => {
it('renders the number of similar records', () => {
- expect(findUserDetail('creditCard').text()).toContain(
+ expect(findUserDetail('credit-card-verification').text()).toContain(
sprintf('Card matches %{similarRecordsCount} accounts', { ...user.creditCard }),
);
});
it('renders a link to the matching cards', () => {
- expect(findLinkFor('creditCard').attributes('href')).toBe(user.creditCard.cardMatchesLink);
+ expect(findLinkFor('credit-card-verification').attributes('href')).toBe(
+ user.creditCard.cardMatchesLink,
+ );
- expect(findLinkFor('creditCard').text()).toBe(
+ expect(findLinkFor('credit-card-verification').text()).toBe(
sprintf('%{similarRecordsCount} accounts', { ...user.creditCard }),
);
- expect(findLinkFor('creditCard').text()).toContain(
+ expect(findLinkFor('credit-card-verification').text()).toContain(
user.creditCard.similarRecordsCount.toString(),
);
});
@@ -105,13 +107,13 @@ describe('UserDetails', () => {
});
it('does not render the number of similar records', () => {
- expect(findUserDetail('creditCard').text()).not.toContain(
+ expect(findUserDetail('credit-card-verification').text()).not.toContain(
sprintf('Card matches %{similarRecordsCount} accounts', { ...user.creditCard }),
);
});
it('does not render a link to the matching cards', () => {
- expect(findLinkFor('creditCard').exists()).toBe(false);
+ expect(findLinkFor('credit-card-verification').exists()).toBe(false);
});
});
});
@@ -124,55 +126,55 @@ describe('UserDetails', () => {
});
it('does not render the users creditCard', () => {
- expect(findUserDetail('creditCard').exists()).toBe(false);
+ expect(findUserDetail('credit-card-verification').exists()).toBe(false);
});
});
});
describe('otherReports', () => {
it('renders the correct label', () => {
- expect(findUserDetailLabel('otherReports')).toBe(USER_DETAILS_I18N.otherReports);
+ expect(findUserDetailLabel('past-closed-reports')).toBe(USER_DETAILS_I18N.pastReports);
});
- describe.each(user.otherReports)('renders a line for report %#', (otherReport) => {
- const index = user.otherReports.indexOf(otherReport);
+ describe.each(user.pastClosedReports)('renders a line for report %#', (pastReport) => {
+ const index = user.pastClosedReports.indexOf(pastReport);
it('renders the category', () => {
- expect(findOtherReport(index).text()).toContain(
- sprintf('Reported for %{category}', { ...otherReport }),
+ expect(findPastReport(index).text()).toContain(
+ sprintf('Reported for %{category}', { ...pastReport }),
);
});
it('renders a link to the report', () => {
- expect(findLinkIn(findOtherReport(index)).attributes('href')).toBe(otherReport.reportPath);
+ expect(findLinkIn(findPastReport(index)).attributes('href')).toBe(pastReport.reportPath);
});
it('renders the time it was created', () => {
- expect(findTimeIn(findOtherReport(index))).toBe(otherReport.createdAt);
+ expect(findTimeIn(findPastReport(index))).toBe(pastReport.createdAt);
});
});
describe('when the users otherReports is empty', () => {
beforeEach(() => {
createComponent({
- user: { ...user, otherReports: [] },
+ user: { ...user, pastClosedReports: [] },
});
});
it('does not render the users otherReports', () => {
- expect(findUserDetail('otherReports').exists()).toBe(false);
+ expect(findUserDetail('past-closed-reports').exists()).toBe(false);
});
});
});
describe('normalLocation', () => {
it('renders the correct label', () => {
- expect(findUserDetailLabel('normalLocation')).toBe(USER_DETAILS_I18N.normalLocation);
+ expect(findUserDetailLabel('normal-location')).toBe(USER_DETAILS_I18N.normalLocation);
});
describe('when the users mostUsedIp is blank', () => {
it('renders the users lastSignInIp', () => {
- expect(findUserDetailValue('normalLocation')).toBe(user.lastSignInIp);
+ expect(findUserDetailValue('normal-location')).toBe(user.lastSignInIp);
});
});
@@ -186,23 +188,25 @@ describe('UserDetails', () => {
});
it('renders the users mostUsedIp', () => {
- expect(findUserDetailValue('normalLocation')).toBe(mostUsedIp);
+ expect(findUserDetailValue('normal-location')).toBe(mostUsedIp);
});
});
});
describe('lastSignInIp', () => {
it('renders the users lastSignInIp with the correct label', () => {
- expect(findUserDetailLabel('lastSignInIp')).toBe(USER_DETAILS_I18N.lastSignInIp);
- expect(findUserDetailValue('lastSignInIp')).toBe(user.lastSignInIp);
+ expect(findUserDetailLabel('last-sign-in-ip')).toBe(USER_DETAILS_I18N.lastSignInIp);
+ expect(findUserDetailValue('last-sign-in-ip')).toBe(user.lastSignInIp);
});
});
it.each(['snippets', 'groups', 'notes'])(
'renders the users %s with the correct label',
(attribute) => {
- expect(findUserDetailLabel(attribute)).toBe(USER_DETAILS_I18N[attribute]);
- expect(findUserDetailValue(attribute)).toBe(
+ const testId = `user-${attribute}-count`;
+
+ expect(findUserDetailLabel(testId)).toBe(USER_DETAILS_I18N[attribute]);
+ expect(findUserDetailValue(testId)).toBe(
USER_DETAILS_I18N[`${attribute}Count`](user[`${attribute}Count`]),
);
},
diff --git a/spec/frontend/admin/abuse_report/mock_data.js b/spec/frontend/admin/abuse_report/mock_data.js
index 8ff0c7d507a..ee61eabfa66 100644
--- a/spec/frontend/admin/abuse_report/mock_data.js
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -15,7 +15,7 @@ export const mockAbuseReport = {
similarRecordsCount: 2,
cardMatchesLink: '/admin/users/spamuser417/card_match',
},
- otherReports: [
+ pastClosedReports: [
{
category: 'offensive',
createdAt: '2023-02-28T10:09:54.982Z',
@@ -32,14 +32,27 @@ export const mockAbuseReport = {
snippetsCount: 0,
groupsCount: 0,
notesCount: 6,
- },
- reporter: {
- username: 'reporter',
- name: 'R Porter',
- avatarUrl: 'https://www.gravatar.com/avatar/a2579caffc69ea5d7606f9dd9d8504ba?s=80&d=identicon',
- path: '/reporter',
+ similarOpenReports: [
+ {
+ status: 'open',
+ message: 'This is obvious spam',
+ reportedAt: '2023-03-29T09:39:50.502Z',
+ category: 'spam',
+ type: 'issue',
+ content: '',
+ screenshot: null,
+ reporter: {
+ username: 'reporter 2',
+ name: 'Another Reporter',
+ avatarUrl: 'https://www.gravatar.com/avatar/anotherreporter',
+ path: '/reporter-2',
+ },
+ updatePath: '/admin/abuse_reports/28',
+ },
+ ],
},
report: {
+ globalId: 'gid://gitlab/AbuseReport/1',
status: 'open',
message: 'This is obvious spam',
reportedAt: '2023-03-29T09:39:50.502Z',
@@ -52,5 +65,66 @@ export const mockAbuseReport = {
'/uploads/-/system/abuse_report/screenshot/27/Screenshot_2023-03-30_at_16.56.37.png',
updatePath: '/admin/abuse_reports/27',
moderateUserPath: '/admin/abuse_reports/27/moderate_user',
+ reporter: {
+ username: 'reporter',
+ name: 'R Porter',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/a2579caffc69ea5d7606f9dd9d8504ba?s=80&d=identicon',
+ path: '/reporter',
+ },
+ },
+};
+
+export const mockLabel1 = {
+ id: 'gid://gitlab/Admin::AbuseReportLabel/1',
+ title: 'Uno',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+};
+
+export const mockLabel2 = {
+ id: 'gid://gitlab/Admin::AbuseReportLabel/2',
+ title: 'Dos',
+ color: '#F0AD4E',
+ textColor: '#FFFFFF',
+ description: null,
+};
+
+export const mockLabelsQueryResponse = {
+ data: {
+ labels: {
+ nodes: [mockLabel1, mockLabel2],
+ __typename: 'LabelConnection',
+ },
+ },
+};
+
+export const mockReportQueryResponse = {
+ data: {
+ abuseReport: {
+ labels: {
+ nodes: [mockLabel1],
+ __typename: 'LabelConnection',
+ },
+ __typename: 'AbuseReport',
+ },
+ },
+};
+
+export const mockCreateLabelResponse = {
+ data: {
+ labelCreate: {
+ label: {
+ id: 'gid://gitlab/Admin::AbuseReportLabel/1',
+ color: '#ed9121',
+ description: null,
+ title: 'abuse report label',
+ textColor: '#FFFFFF',
+ __typename: 'Label',
+ },
+ errors: [],
+ __typename: 'AbuseReportLabelCreatePayload',
+ },
},
};
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
index 8482faccca0..7f915dbacb1 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
@@ -1,3 +1,4 @@
+import { GlLabel } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
@@ -13,6 +14,7 @@ describe('AbuseReportRow', () => {
const findListItem = () => wrapper.findComponent(ListItem);
const findAbuseCategory = () => wrapper.findComponent(AbuseCategory);
+ const findLabels = () => wrapper.findAllComponents(GlLabel);
const findAbuseReportTitle = () => wrapper.findByTestId('abuse-report-title');
const findDisplayedDate = () => wrapper.findByTestId('abuse-report-date');
@@ -95,6 +97,18 @@ describe('AbuseReportRow', () => {
expect(findAbuseCategory().exists()).toBe(true);
});
+ it('renders labels', () => {
+ const labels = findLabels();
+ expect(labels).toHaveLength(2);
+
+ const { color, title } = mockAbuseReports[0].labels[0];
+ expect(labels.at(0).props()).toMatchObject({
+ backgroundColor: color,
+ title,
+ target: `${window.location.href}?${encodeURIComponent('label_name[]')}=${title}`,
+ });
+ });
+
describe('aggregated report', () => {
const mockAggregatedAbuseReport = mockAbuseReports[1];
const { reportedUser, category, count } = mockAggregatedAbuseReport;
diff --git a/spec/frontend/admin/abuse_reports/mock_data.js b/spec/frontend/admin/abuse_reports/mock_data.js
index 33a28a21cca..3101321d02d 100644
--- a/spec/frontend/admin/abuse_reports/mock_data.js
+++ b/spec/frontend/admin/abuse_reports/mock_data.js
@@ -1,3 +1,5 @@
+import { mockLabel1, mockLabel2 } from '../abuse_report/mock_data';
+
export const mockAbuseReports = [
{
category: 'spam',
@@ -7,6 +9,7 @@ export const mockAbuseReports = [
reportedUser: { name: 'Mr. Abuser' },
reportPath: '/admin/abuse_reports/1',
count: 1,
+ labels: [mockLabel1, mockLabel2],
},
{
category: 'phishing',
diff --git a/spec/frontend/admin/applications/components/__snapshots__/delete_application_spec.js.snap b/spec/frontend/admin/applications/components/__snapshots__/delete_application_spec.js.snap
index 459a113b6d1..7f068cf9ee9 100644
--- a/spec/frontend/admin/applications/components/__snapshots__/delete_application_spec.js.snap
+++ b/spec/frontend/admin/applications/components/__snapshots__/delete_application_spec.js.snap
@@ -10,7 +10,6 @@ exports[`DeleteApplication the modal component form matches the snapshot 1`] = `
type="hidden"
value="delete"
/>
-
<input
name="authenticity_token"
type="hidden"
diff --git a/spec/frontend/admin/topics/components/__snapshots__/remove_avatar_spec.js.snap b/spec/frontend/admin/topics/components/__snapshots__/remove_avatar_spec.js.snap
index 00f742c3614..e0fca2590e7 100644
--- a/spec/frontend/admin/topics/components/__snapshots__/remove_avatar_spec.js.snap
+++ b/spec/frontend/admin/topics/components/__snapshots__/remove_avatar_spec.js.snap
@@ -10,7 +10,6 @@ exports[`RemoveAvatar the modal component form matches the snapshot 1`] = `
type="hidden"
value="delete"
/>
-
<input
name="authenticity_token"
type="hidden"
diff --git a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap
index 4237685e45c..d8157c6ff20 100644
--- a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap
+++ b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_item_spec.js.snap
@@ -1,3 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`AssociationsListItem renders interpolated message in a \`li\` element 1`] = `"<li><strong>5</strong> groups</li>"`;
+exports[`AssociationsListItem renders interpolated message in a \`li\` element 1`] = `
+<li>
+ <strong>
+ 5
+ </strong>
+ groups
+</li>
+`;
diff --git a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
index 265569ac0e3..7f853f13363 100644
--- a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
+++ b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap
@@ -10,13 +10,11 @@ exports[`Delete user modal renders modal with form included 1`] = `
type="hidden"
value="delete"
/>
-
<input
name="authenticity_token"
type="hidden"
value="csrf"
/>
-
<gl-form-input-stub
autocomplete="off"
autofocus=""
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index 69755c6142a..b44986ea7de 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -18,7 +18,7 @@ describe('AdminUserActions component', () => {
const findUserActions = (id) => wrapper.findByTestId(`user-actions-${id}`);
const findEditButton = (id = user.id) => findUserActions(id).find('[data-testid="edit"]');
const findActionsDropdown = (id = user.id) =>
- findUserActions(id).find('[data-testid="dropdown-toggle"]');
+ findUserActions(id).find('[data-testid="user-actions-dropdown-toggle"]');
const findDisclosureGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
const initComponent = ({ actions = [], showButtonLabels } = {}) => {
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
index 80d3676ffee..84156d6daf3 100644
--- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -7,7 +7,6 @@ exports[`Alert integration settings form default state should match the default
message="Action to take when receiving an alert. %{docsLink}"
/>
</p>
-
<form>
<gl-form-group-stub
class="gl-pl-0"
@@ -16,15 +15,14 @@ exports[`Alert integration settings form default state should match the default
>
<gl-form-checkbox-stub
checked="true"
- data-qa-selector="create_incident_checkbox"
- id="2"
+ data-testid="create-incident-checkbox"
+ id="reference-0"
>
<span>
Create an incident. Incidents are created for each alert triggered.
</span>
</gl-form-checkbox-stub>
</gl-form-group-stub>
-
<gl-form-group-stub
class="col-8 col-md-9 gl-px-6"
label-for="alert-integration-settings-issue-template"
@@ -34,11 +32,9 @@ exports[`Alert integration settings form default state should match the default
>
<label
class="gl-display-inline-flex"
- for="alert-integration-settings-issue-template"
+ for="reference-1"
>
-
Incident template (optional).
-
<gl-link-stub
href="/help/user/project/description_templates#create-an-issue-template"
target="_blank"
@@ -50,14 +46,13 @@ exports[`Alert integration settings form default state should match the default
</span>
</gl-link-stub>
</label>
-
<gl-collapsible-listbox-stub
block="true"
category="primary"
- data-qa-selector="incident_templates_dropdown"
+ data-testid="incident-templates-dropdown"
headertext=""
icon=""
- id="alert-integration-settings-issue-template"
+ id="reference-1"
items="[object Object]"
noresultstext="No results found"
placement="left"
@@ -71,50 +66,45 @@ exports[`Alert integration settings form default state should match the default
variant="default"
/>
</gl-form-group-stub>
-
<gl-form-group-stub
- class="gl-pl-0 gl-mb-5"
+ class="gl-mb-5 gl-pl-0"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-checkbox-stub
- data-qa-selector="enable_email_notification_checkbox"
- id="3"
+ data-testid="enable-email-notification-checkbox"
+ id="reference-2"
>
<span>
Send a single email notification to Owners and Maintainers for new alerts.
</span>
</gl-form-checkbox-stub>
</gl-form-group-stub>
-
<gl-form-group-stub
- class="gl-pl-0 gl-mb-5"
+ class="gl-mb-5 gl-pl-0"
labeldescription=""
optionaltext="(optional)"
>
<gl-form-checkbox-stub
checked="true"
- id="4"
+ id="reference-3"
>
<span>
Automatically close associated incident when a recovery alert notification resolves an alert
</span>
</gl-form-checkbox-stub>
</gl-form-group-stub>
-
<gl-button-stub
buttontextclasses=""
category="primary"
class="js-no-auto-disable"
- data-qa-selector="save_changes_button"
+ data-testid="save-changes-button"
icon=""
size="medium"
type="submit"
variant="confirm"
>
-
Save changes
-
</gl-button-stub>
</form>
</div>
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index a16a03a2fc5..e01dde8f62c 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -57,7 +57,7 @@ describe('AlertsSettingsWrapper', () => {
const findIntegrationsList = () => wrapper.findComponent(IntegrationsList);
const findLoader = () => findIntegrationsList().findComponent(GlLoadingIcon);
const findIntegrations = () => findIntegrationsList().findAll('table tbody tr');
- const findAddIntegrationBtn = () => wrapper.findByTestId('add-integration-btn');
+ const findAddIntegrationBtn = () => wrapper.findByTestId('add-integration-button');
const findAlertsSettingsForm = () => wrapper.findComponent(AlertsSettingsForm);
const findAlert = () => wrapper.findComponent(GlAlert);
diff --git a/spec/frontend/analytics/cycle_analytics/components/__snapshots__/total_time_spec.js.snap b/spec/frontend/analytics/cycle_analytics/components/__snapshots__/total_time_spec.js.snap
index 92927ef16ec..5f712ba41f4 100644
--- a/spec/frontend/analytics/cycle_analytics/components/__snapshots__/total_time_spec.js.snap
+++ b/spec/frontend/analytics/cycle_analytics/components/__snapshots__/total_time_spec.js.snap
@@ -1,28 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`TotalTime with a blank object should render -- 1`] = `"<span> -- </span>"`;
+exports[`TotalTime with a blank object should render -- 1`] = `
+<span>
+ --
+</span>
+`;
exports[`TotalTime with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = `
-"<span>
- 3 <span>days</span></span>"
+<span>
+ 3
+ <span>
+ days
+ </span>
+</span>
`;
exports[`TotalTime with a valid time object with {"hours": 7, "mins": 20, "seconds": 10} 1`] = `
-"<span>
- 7 <span>hrs</span></span>"
+<span>
+ 7
+ <span>
+ hrs
+ </span>
+</span>
`;
exports[`TotalTime with a valid time object with {"hours": 23, "mins": 10} 1`] = `
-"<span>
- 23 <span>hrs</span></span>"
+<span>
+ 23
+ <span>
+ hrs
+ </span>
+</span>
`;
exports[`TotalTime with a valid time object with {"mins": 47, "seconds": 3} 1`] = `
-"<span>
- 47 <span>mins</span></span>"
+<span>
+ 47
+ <span>
+ mins
+ </span>
+</span>
`;
exports[`TotalTime with a valid time object with {"seconds": 35} 1`] = `
-"<span>
- 35 <span>s</span></span>"
+<span>
+ 35
+ <span>
+ s
+ </span>
+</span>
`;
diff --git a/spec/frontend/api/application_settings_api_spec.js b/spec/frontend/api/application_settings_api_spec.js
new file mode 100644
index 00000000000..92a6a159913
--- /dev/null
+++ b/spec/frontend/api/application_settings_api_spec.js
@@ -0,0 +1,45 @@
+import MockAdapter from 'axios-mock-adapter';
+import * as applicationSettingsApi from '~/api/application_settings_api';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+
+describe('~/api/application_settings_api.js', () => {
+ const MOCK_SETTINGS_RES = { test_setting: 'foo' };
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ window.gon = { api_version: 'v7' };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('getApplicationSettings', () => {
+ it('fetches application settings', () => {
+ const expectedUrl = '/api/v7/application/settings';
+ jest.spyOn(axios, 'get');
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, MOCK_SETTINGS_RES);
+
+ return applicationSettingsApi.getApplicationSettings().then(({ data }) => {
+ expect(data).toEqual(MOCK_SETTINGS_RES);
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl);
+ });
+ });
+ });
+
+ describe('updateApplicationSettings', () => {
+ it('updates application settings', () => {
+ const expectedUrl = '/api/v7/application/settings';
+ const MOCK_REQ = { another_setting: 'bar' };
+ jest.spyOn(axios, 'put');
+ mock.onPut(expectedUrl).reply(HTTP_STATUS_OK, MOCK_SETTINGS_RES);
+
+ return applicationSettingsApi.updateApplicationSettings(MOCK_REQ).then(({ data }) => {
+ expect(data).toEqual(MOCK_SETTINGS_RES);
+ expect(axios.put).toHaveBeenCalledWith(expectedUrl, MOCK_REQ);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap b/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
index 58aee76e381..1456830b0eb 100644
--- a/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
+++ b/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
@@ -2,63 +2,45 @@
exports[`Keep latest artifact toggle when application keep latest artifact setting is enabled sets correct setting value in toggle with query result 1`] = `
<div>
- <!---->
-
<div
- class="gl-toggle-wrapper gl-display-flex gl-mb-0 flex-grow-1 gl-flex-direction-column"
+ class="gl-display-flex gl-flex-direction-column gl-mb-0 gl-toggle-wrapper"
data-testid="toggle-wrapper"
>
<span
- class="gl-toggle-label-container gl-mb-3"
+ class="gl-flex-shrink-0 gl-mb-3 gl-toggle-label"
+ data-testid="toggle-label"
+ id="reference-0"
+ >
+ Keep artifacts from most recent successful jobs
+ </span>
+ <button
+ aria-checked="true"
+ aria-describedby="toggle-help-2"
+ aria-labelledby="reference-0"
+ class="gl-flex-shrink-0 gl-toggle is-checked"
+ role="switch"
+ type="button"
>
<span
- class="gl-toggle-label"
- data-testid="toggle-label"
- id="toggle-label-4"
+ class="toggle-icon"
>
- Keep artifacts from most recent successful jobs
+ <gl-icon-stub
+ name="mobile-issue-close"
+ size="16"
+ />
</span>
-
- <!---->
- </span>
-
+ </button>
<span
- class="gl-toggle-switch-container"
+ class="gl-help-label"
+ data-testid="toggle-help"
+ id="reference-1"
>
- <!---->
-
- <button
- aria-checked="true"
- aria-describedby="toggle-help-2"
- aria-labelledby="toggle-label-4"
- class="gl-flex-shrink-0 gl-toggle is-checked"
- role="switch"
- type="button"
- >
- <span
- class="toggle-icon"
- >
- <gl-icon-stub
- name="mobile-issue-close"
- size="16"
- />
- </span>
- </button>
-
- <span
- class="gl-help-label"
- data-testid="toggle-help"
- id="toggle-help-2"
- >
-
The latest artifacts created by jobs in the most recent successful pipeline will be stored.
-
- <gl-link-stub
- href="/help/ci/pipelines/job_artifacts"
- >
- Learn more.
- </gl-link-stub>
- </span>
+ <gl-link-stub
+ href="/help/ci/pipelines/job_artifacts"
+ >
+ Learn more.
+ </gl-link-stub>
</span>
</div>
</div>
diff --git a/spec/frontend/avatar_helper_spec.js b/spec/frontend/avatar_helper_spec.js
deleted file mode 100644
index 91bf8e28774..00000000000
--- a/spec/frontend/avatar_helper_spec.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import { TEST_HOST } from 'spec/test_constants';
-import {
- DEFAULT_SIZE_CLASS,
- IDENTICON_BG_COUNT,
- renderAvatar,
- renderIdenticon,
- getIdenticonBackgroundClass,
- getIdenticonTitle,
-} from '~/helpers/avatar_helper';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
-
-function matchAll(str) {
- return new RegExp(`^${str}$`);
-}
-
-describe('avatar_helper', () => {
- describe('getIdenticonBackgroundClass', () => {
- it('returns identicon bg class from id that is a number', () => {
- expect(getIdenticonBackgroundClass(1)).toEqual('bg2');
- });
-
- it('returns identicon bg class from id that is a string', () => {
- expect(getIdenticonBackgroundClass('1')).toEqual('bg2');
- });
-
- it('returns identicon bg class from id that is a GraphQL string id', () => {
- expect(getIdenticonBackgroundClass('gid://gitlab/Project/1')).toEqual('bg2');
- });
-
- it('returns identicon bg class from unparsable string', () => {
- expect(getIdenticonBackgroundClass('gid://gitlab/')).toEqual('bg1');
- });
-
- it(`wraps around if id is bigger than ${IDENTICON_BG_COUNT}`, () => {
- expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT + 4)).toEqual('bg5');
- expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT * 5 + 6)).toEqual('bg7');
- });
- });
-
- describe('getIdenticonTitle', () => {
- it('returns identicon title from name', () => {
- expect(getIdenticonTitle('Lorem')).toEqual('L');
- expect(getIdenticonTitle('dolar-sit-amit')).toEqual('D');
- expect(getIdenticonTitle('%-with-special-chars')).toEqual('%');
- });
-
- it('returns space if name is falsey', () => {
- expect(getIdenticonTitle('')).toEqual(' ');
- expect(getIdenticonTitle(null)).toEqual(' ');
- });
- });
-
- describe('renderIdenticon', () => {
- it('renders with the first letter as title and bg based on id', () => {
- const entity = {
- id: IDENTICON_BG_COUNT + 3,
- name: 'Xavior',
- };
- const options = {
- sizeClass: 's32',
- };
-
- const result = renderIdenticon(entity, options);
-
- expect(result).toHaveClass(`identicon ${options.sizeClass} bg4`);
- expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
- });
-
- it('renders with defaults, if no options are given', () => {
- const entity = {
- id: 1,
- name: 'tanuki',
- };
-
- const result = renderIdenticon(entity);
-
- expect(result).toHaveClass(`identicon ${DEFAULT_SIZE_CLASS} bg2`);
- expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
- });
- });
-
- describe('renderAvatar', () => {
- it('renders an image with the avatarUrl', () => {
- const avatarUrl = `${TEST_HOST}/not-real-assets/test.png`;
-
- const result = renderAvatar({
- avatar_url: avatarUrl,
- });
-
- expect(result).toBeMatchedBy('img');
- expect(result).toHaveAttr('src', avatarUrl);
- expect(result).toHaveClass(DEFAULT_SIZE_CLASS);
- });
-
- it('renders an identicon if no avatarUrl', () => {
- const entity = {
- id: 1,
- name: 'walrus',
- };
- const options = {
- sizeClass: 's16',
- };
-
- const result = renderAvatar(entity, options);
-
- expect(result).toHaveClass(`identicon ${options.sizeClass} bg2`);
- expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
- });
- });
-});
diff --git a/spec/frontend/behaviors/markdown/paste_markdown_table_spec.js b/spec/frontend/behaviors/markdown/paste_markdown_table_spec.js
index 7044618fd9e..154347b08b5 100644
--- a/spec/frontend/behaviors/markdown/paste_markdown_table_spec.js
+++ b/spec/frontend/behaviors/markdown/paste_markdown_table_spec.js
@@ -77,7 +77,8 @@ describe('PasteMarkdownTable', () => {
data.getData = jest.fn().mockImplementation((type) => {
if (type === 'text/html') {
return '<table><tr><td>First</td><td>Last</td><tr><td>John</td><td>Doe</td><tr><td>Jane</td><td>Doe</td></table>';
- } else if (type === 'text/plain') {
+ }
+ if (type === 'text/plain') {
return 'First\tLast\nJohn\tDoe\nJane\tDoe';
}
@@ -102,7 +103,8 @@ describe('PasteMarkdownTable', () => {
data.getData = jest.fn().mockImplementation((type) => {
if (type === 'text/html') {
return '<table><tr><td>First</td><td>Last</td><tr><td>John</td><td>Doe</td><tr><td>Jane</td><td>/td></table>';
- } else if (type === 'text/plain') {
+ }
+ if (type === 'text/plain') {
return 'First\tLast\nJohn\tDoe\nJane';
}
diff --git a/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
index 1733c4d4bb4..bd7485e9d80 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
@@ -2,10 +2,10 @@
exports[`Blob Header Editing rendering matches the snapshot 1`] = `
<div
- class="js-file-title file-title-flex-parent"
+ class="file-title-flex-parent js-file-title"
>
<div
- class="gl-display-flex gl-align-items-center gl-w-full"
+ class="gl-align-items-center gl-display-flex gl-w-full"
>
<gl-form-input-stub
class="form-control js-snippet-file-name"
@@ -14,8 +14,6 @@ exports[`Blob Header Editing rendering matches the snapshot 1`] = `
type="text"
value="foo.md"
/>
-
- <!---->
</div>
</div>
`;
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 4ae55f34e4c..292a0da2bfe 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
@@ -2,9 +2,8 @@
exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
<div
- class="file-header-content d-flex align-items-center lh-100"
+ class="align-items-center d-flex file-header-content lh-100"
>
-
<file-icon-stub
aria-hidden="true"
cssclasses="gl-mr-3"
@@ -12,14 +11,12 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
filename="foo/bar/dummy.md"
size="16"
/>
-
<strong
- class="file-title-name mr-1 js-blob-header-filepath"
+ class="file-title-name js-blob-header-filepath mr-1"
data-qa-selector="file_title_content"
>
foo/bar/dummy.md
</strong>
-
<clipboard-button-stub
category="tertiary"
cssclass="gl-mr-2"
@@ -30,13 +27,10 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
tooltipplacement="top"
variant="default"
/>
-
<small
class="gl-mr-3"
>
a lot
</small>
-
- <!---->
</div>
`;
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
deleted file mode 100644
index b430dc15557..00000000000
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ /dev/null
@@ -1,34 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
-<div
- class="js-file-title file-title-flex-parent"
->
- <div
- class="gl-display-flex"
- >
- <table-of-contents-stub
- class="gl-pr-2"
- />
-
- <blob-filepath-stub
- blob="[object Object]"
- showpath="true"
- />
- </div>
-
- <div
- class="gl-display-flex gl-flex-wrap file-actions"
- >
- <viewer-switcher-stub
- docicon="document"
- value="simple"
- />
-
- <default-actions-stub
- activeviewer="simple"
- rawpath="https://testing.com/flightjs/flight/snippets/51/raw"
- />
- </div>
-</div>
-`;
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index 47e09bb38bc..922d6a0211b 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -1,4 +1,6 @@
+import Vue from 'vue';
import { shallowMount, mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BlobHeader from '~/blob/components/blob_header.vue';
import DefaultActions from '~/blob/components/blob_header_default_actions.vue';
@@ -10,8 +12,14 @@ import {
SIMPLE_BLOB_VIEWER_TITLE,
} from '~/blob/components/constants';
import TableContents from '~/blob/components/table_contents.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
+import userInfoQuery from '~/blob/queries/user_info.query.graphql';
+import applicationInfoQuery from '~/blob/queries/application_info.query.graphql';
+import { Blob, userInfoMock, applicationInfoMock } from './mock_data';
-import { Blob } from './mock_data';
+Vue.use(VueApollo);
describe('Blob Header Default Actions', () => {
let wrapper;
@@ -26,14 +34,29 @@ describe('Blob Header Default Actions', () => {
const findBlobFilePath = () => wrapper.findComponent(BlobFilepath);
const findRichTextEditorBtn = () => wrapper.findByLabelText(RICH_BLOB_VIEWER_TITLE);
const findSimpleTextEditorBtn = () => wrapper.findByLabelText(SIMPLE_BLOB_VIEWER_TITLE);
+ const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
- function createComponent({
+ async function createComponent({
blobProps = {},
options = {},
propsData = {},
mountFn = shallowMount,
} = {}) {
+ const userInfoMockResolver = jest.fn().mockResolvedValue({
+ data: { ...userInfoMock },
+ });
+
+ const applicationInfoMockResolver = jest.fn().mockResolvedValue({
+ data: { ...applicationInfoMock },
+ });
+
+ const fakeApollo = createMockApollo([
+ [userInfoQuery, userInfoMockResolver],
+ [applicationInfoQuery, applicationInfoMockResolver],
+ ]);
+
wrapper = mountFn(BlobHeader, {
+ apolloProvider: fakeApollo,
provide: {
...defaultProvide,
},
@@ -43,12 +66,40 @@ describe('Blob Header Default Actions', () => {
},
...options,
});
+
+ await waitForPromises();
}
describe('rendering', () => {
- it('matches the snapshot', () => {
- createComponent();
- expect(wrapper.element).toMatchSnapshot();
+ describe('WebIdeLink component', () => {
+ it('renders the WebIdeLink component with the correct props', async () => {
+ const { ideEditPath, editBlobPath, gitpodBlobUrl, pipelineEditorPath } = Blob;
+ const showForkSuggestion = false;
+ await createComponent({ propsData: { showForkSuggestion } });
+
+ expect(findWebIdeLink().props()).toMatchObject({
+ showEditButton: true,
+ editUrl: editBlobPath,
+ webIdeUrl: ideEditPath,
+ needsToFork: showForkSuggestion,
+ showPipelineEditorButton: Boolean(pipelineEditorPath),
+ pipelineEditorUrl: pipelineEditorPath,
+ gitpodUrl: gitpodBlobUrl,
+ showGitpodButton: applicationInfoMock.gitpodEnabled,
+ gitpodEnabled: userInfoMock.currentUser.gitpodEnabled,
+ userPreferencesGitpodPath: userInfoMock.currentUser.preferencesGitpodPath,
+ userProfileEnableGitpodPath: userInfoMock.currentUser.profileEnableGitpodPath,
+ });
+ });
+
+ it.each([[{ archived: true }], [{ editBlobPath: null }]])(
+ 'does not render the WebIdeLink component when blob is archived or does not have an edit path',
+ (blobProps) => {
+ createComponent({ blobProps });
+
+ expect(findWebIdeLink().exists()).toBe(false);
+ },
+ );
});
describe('default render', () => {
diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js
index 6ecf5091591..7ed526fba97 100644
--- a/spec/frontend/blob/components/mock_data.js
+++ b/spec/frontend/blob/components/mock_data.js
@@ -30,6 +30,10 @@ export const Blob = {
richViewer: {
...RichViewerMock,
},
+ ideEditPath: 'ide/edit',
+ editBlobPath: 'edit/blob',
+ gitpodBlobUrl: 'gitpod/blob/url',
+ pipelineEditorPath: 'pipeline/editor/path',
};
export const BinaryBlob = {
@@ -60,3 +64,14 @@ export const SimpleBlobContentMock = {
export const mockEnvironmentName = 'my.testing.environment';
export const mockEnvironmentPath = 'https://my.testing.environment';
+
+export const userInfoMock = {
+ currentUser: {
+ id: '123',
+ gitpodEnabled: true,
+ preferencesGitpodPath: '/-/profile/preferences#user_gitpod_enabled',
+ profileEnableGitpodPath: '/-/profile?user%5Bgitpod_enabled%5D=true',
+ },
+};
+
+export const applicationInfoMock = { gitpodEnabled: true };
diff --git a/spec/frontend/blob/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js
index de39a8f688a..c7a86d6230a 100644
--- a/spec/frontend/blob/line_highlighter_spec.js
+++ b/spec/frontend/blob/line_highlighter_spec.js
@@ -72,6 +72,15 @@ describe('LineHighlighter', () => {
expect(utils.scrollToElement).toHaveBeenCalledWith('#L5', expect.anything());
});
+ it('does not scroll to the first highlighted line when disableScroll is `true`', () => {
+ jest.spyOn(utils, 'scrollToElement');
+ const highlighter = new LineHighlighter();
+ const scrollEnabled = false;
+ highlighter.highlightHash('#L5-25', scrollEnabled);
+
+ expect(utils.scrollToElement).not.toHaveBeenCalled();
+ });
+
it('discards click events', () => {
const clickSpy = jest.fn();
diff --git a/spec/frontend/blob/openapi/index_spec.js b/spec/frontend/blob/openapi/index_spec.js
index 95e86398ab8..c96a021550d 100644
--- a/spec/frontend/blob/openapi/index_spec.js
+++ b/spec/frontend/blob/openapi/index_spec.js
@@ -4,16 +4,16 @@ import { TEST_HOST } from 'helpers/test_constants';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import renderOpenApi from '~/blob/openapi';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import setWindowLocation from 'helpers/set_window_location_helper';
describe('OpenAPI blob viewer', () => {
const id = 'js-openapi-viewer';
const mockEndpoint = 'some/endpoint';
let mock;
- beforeEach(async () => {
+ beforeEach(() => {
setHTMLFixture(`<div id="${id}" data-endpoint="${mockEndpoint}"></div>`);
mock = new MockAdapter(axios).onGet().reply(HTTP_STATUS_OK);
- await renderOpenApi();
});
afterEach(() => {
@@ -21,9 +21,28 @@ describe('OpenAPI blob viewer', () => {
mock.restore();
});
- it('initializes SwaggerUI with the correct configuration', () => {
- expect(document.body.innerHTML).toContain(
- `<iframe src="${TEST_HOST}/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000"></iframe>`,
- );
+ describe('without config options', () => {
+ beforeEach(async () => {
+ await renderOpenApi();
+ });
+
+ it('initializes SwaggerUI without config options', () => {
+ expect(document.body.innerHTML).toContain(
+ `<iframe src="${TEST_HOST}/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000"></iframe>`,
+ );
+ });
+ });
+
+ describe('with config options', () => {
+ beforeEach(async () => {
+ setWindowLocation('?displayOperationId=true');
+ await renderOpenApi();
+ });
+
+ it('initializes SwaggerUI with the correct config options', () => {
+ expect(document.body.innerHTML).toContain(
+ `<iframe src="${TEST_HOST}/-/sandbox/swagger?displayOperationId=true" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000"></iframe>`,
+ );
+ });
});
});
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 1740676161f..95b5712bab0 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -91,6 +91,7 @@ describe('Board card component', () => {
rootPath: '/',
scopedLabelsAvailable: false,
isEpicBoard,
+ allowSubEpics: isEpicBoard,
issuableType: TYPE_ISSUE,
isGroupBoard,
isApolloBoard: false,
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 167efb94fcc..f0d40af94fe 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -72,6 +72,7 @@ describe('Board card', () => {
issuableType: 'issue',
isGroupBoard: true,
disabled: false,
+ allowSubEpics: false,
isApolloBoard: false,
...provide,
},
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index b17a5589c07..fa18b47cf54 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -87,6 +87,7 @@ describe('BoardsSelector', () => {
isGroupBoard = false,
isProjectBoard = false,
provide = {},
+ props = {},
} = {}) => {
fakeApollo = createMockApollo([
[projectBoardsQuery, projectBoardsQueryHandler],
@@ -100,6 +101,7 @@ describe('BoardsSelector', () => {
apolloProvider: fakeApollo,
propsData: {
throttleDuration,
+ ...props,
},
attachTo: document.body,
provide: {
@@ -307,4 +309,14 @@ describe('BoardsSelector', () => {
});
});
});
+
+ describe('Apollo boards', () => {
+ it('displays loading state of dropdown while current board is being fetched', () => {
+ createComponent({
+ props: { isCurrentBoardLoading: true },
+ provide: { isApolloBoard: true },
+ });
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
index 5b5b68d5dbe..16ad54f0854 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -61,12 +61,7 @@ describe('IssueBoardFilter', () => {
({ isSignedIn }) => {
createComponent({ isSignedIn });
- const tokens = mockTokens(
- fetchLabelsSpy,
- fetchUsersSpy,
- wrapper.vm.fetchMilestones,
- isSignedIn,
- );
+ const tokens = mockTokens(fetchLabelsSpy, fetchUsersSpy, isSignedIn);
expect(findBoardsFilteredSearch().props('tokens')).toEqual(orderBy(tokens, ['title']));
},
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index 1b526e6fbec..f354067e226 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -8,6 +8,7 @@ import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.v
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
import issueSetTitleMutation from '~/boards/graphql/issue_set_title.mutation.graphql';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
import { updateIssueTitleResponse, updateEpicTitleResponse } from '../../mock_data';
@@ -40,6 +41,10 @@ describe('BoardSidebarTitle', () => {
.fn()
.mockResolvedValue(updateEpicTitleResponse);
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
+
afterEach(() => {
localStorage.clear();
store = null;
@@ -207,8 +212,7 @@ describe('BoardSidebarTitle', () => {
it('collapses sidebar and renders former item title', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
- expect(storeDispatch).toHaveBeenCalledWith(
- 'setError',
+ expect(cacheUpdates.setError).toHaveBeenCalledWith(
expect.objectContaining({ message: 'An error occurred when updating the title' }),
);
});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 8f57a6eb7da..dfcdb4c05d0 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -827,7 +827,7 @@ export const mockConfidentialToken = {
],
};
-export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn) => [
+export const mockTokens = (fetchLabels, fetchUsers, isSignedIn) => [
{
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
@@ -870,7 +870,8 @@ export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn)
shouldSkipSort: true,
token: MilestoneToken,
unique: true,
- fetchMilestones,
+ fullPath: 'gitlab-org',
+ isProject: false,
},
{
icon: 'issues',
diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
index 4da56a865d5..ee8031f2475 100644
--- a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
+++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
@@ -16,102 +16,82 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
toggletext=""
variant="default"
>
-
<ul
aria-labelledby="dropdown-toggle-btn-25"
class="gl-new-dropdown-contents"
data-testid="disclosure-content"
- id="disclosure-26"
+ id="reference-0"
tabindex="-1"
>
<gl-disclosure-dropdown-item-stub
item="[object Object]"
/>
</ul>
-
</gl-base-dropdown-stub>
-
<b-button-stub
- class="gl-display-block gl-md-display-none! gl-button btn-danger-secondary"
+ class="btn-danger-secondary gl-button gl-display-block gl-md-display-none!"
data-testid="delete-merged-branches-button"
size="md"
tag="button"
type="button"
variant="danger"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- Delete merged branches
-
+ Delete merged branches
</span>
</b-button-stub>
-
<div>
<form
action="/namespace/project/-/merged_branches"
method="post"
>
<p>
- You are about to
+ You are about to
<strong>
delete all branches
</strong>
- that were merged into
+ that were merged into
<code>
master
</code>
.
</p>
-
<p>
-
This may include merged branches that are not visible on the current screen.
-
</p>
-
<p>
-
A branch won't be deleted if it is protected or associated with an open merge request.
-
</p>
-
<p>
- This bulk action is
+ This bulk action is
<strong>
permanent and cannot be undone or recovered
</strong>
.
</p>
-
<p>
- Plese type the following to confirm:
+ Plese type the following to confirm:
<code>
delete
</code>
- .
+ .
<b-form-input-stub
aria-labelledby="input-label"
autocomplete="off"
- class="gl-form-input gl-mt-2 gl-form-input-sm"
+ class="gl-form-input gl-form-input-sm gl-mt-2"
debounce="0"
formatter="[Function]"
type="text"
value=""
/>
</p>
-
<input
name="_method"
type="hidden"
value="delete"
/>
-
<input
name="authenticity_token"
type="hidden"
@@ -119,7 +99,7 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
/>
</form>
<div
- class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0 gl-mr-3"
+ class="gl-display-flex gl-flex-direction-row gl-flex-wrap gl-justify-content-end gl-m-0 gl-mr-3"
>
<b-button-stub
class="gl-button"
@@ -129,19 +109,12 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
type="button"
variant="default"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
Cancel
-
</span>
</b-button-stub>
-
<b-button-stub
class="gl-button"
data-testid="delete-merged-branches-confirmation-button"
@@ -151,10 +124,6 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
type="button"
variant="danger"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
diff --git a/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap b/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap
index 2afca66b0c1..81a57653f61 100644
--- a/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap
+++ b/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Branch divergence graph component renders ahead and behind count 1`] = `
<div
- class="divergence-graph px-2 d-none d-md-block"
+ class="d-md-block d-none divergence-graph px-2"
title="10 commits behind main, 10 commits ahead"
>
<graph-bar-stub
@@ -10,11 +10,9 @@ exports[`Branch divergence graph component renders ahead and behind count 1`] =
maxcommits="100"
position="left"
/>
-
<div
- class="graph-separator float-left mt-1"
+ class="float-left graph-separator mt-1"
/>
-
<graph-bar-stub
count="10"
maxcommits="100"
@@ -25,7 +23,7 @@ exports[`Branch divergence graph component renders ahead and behind count 1`] =
exports[`Branch divergence graph component renders distance count 1`] = `
<div
- class="divergence-graph px-2 d-none d-md-block"
+ class="d-md-block d-none divergence-graph px-2"
title="More than 900 commits different with main"
>
<graph-bar-stub
diff --git a/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js b/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js
new file mode 100644
index 00000000000..d14b78d2f4d
--- /dev/null
+++ b/spec/frontend/ci/admin/jobs_table/admin_job_table_app_spec.js
@@ -0,0 +1,445 @@
+import { GlLoadingIcon, GlEmptyState, GlAlert, GlIntersectionObserver } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import JobsTableTabs from '~/ci/jobs_page/components/jobs_table_tabs.vue';
+import JobsSkeletonLoader from '~/ci/admin/jobs_table/components/jobs_skeleton_loader.vue';
+import getAllJobsQuery from '~/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql';
+import getAllJobsCount from '~/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql';
+import getCancelableJobsQuery from '~/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql';
+import AdminJobsTableApp from '~/ci/admin/jobs_table/admin_jobs_table_app.vue';
+import CancelJobs from '~/ci/admin/jobs_table/components/cancel_jobs.vue';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import { createAlert } from '~/alert';
+import { TEST_HOST } from 'spec/test_constants';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
+import * as urlUtils from '~/lib/utils/url_utility';
+import {
+ JOBS_FETCH_ERROR_MSG,
+ CANCELABLE_JOBS_ERROR_MSG,
+ LOADING_ARIA_LABEL,
+ RAW_TEXT_WARNING_ADMIN,
+ JOBS_COUNT_ERROR_MESSAGE,
+} from '~/ci/admin/jobs_table/constants';
+import { TOKEN_TYPE_JOBS_RUNNER_TYPE } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ mockAllJobsResponsePaginated,
+ mockCancelableJobsCountResponse,
+ mockAllJobsResponseEmpty,
+ statuses,
+ mockFailedSearchToken,
+ mockAllJobsCountResponse,
+} from 'jest/ci/jobs_mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+
+describe('Job table app', () => {
+ let wrapper;
+
+ const successHandler = jest.fn().mockResolvedValue(mockAllJobsResponsePaginated);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const cancelHandler = jest.fn().mockResolvedValue(mockCancelableJobsCountResponse);
+ const emptyHandler = jest.fn().mockResolvedValue(mockAllJobsResponseEmpty);
+ const countSuccessHandler = jest.fn().mockResolvedValue(mockAllJobsCountResponse);
+
+ const findSkeletonLoader = () => wrapper.findComponent(JobsSkeletonLoader);
+ const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
+ const findTable = () => wrapper.findComponent(JobsTable);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findTabs = () => wrapper.findComponent(JobsTableTabs);
+ const findCancelJobsButton = () => wrapper.findComponent(CancelJobs);
+ const findFilteredSearch = () => wrapper.findComponent(JobsFilteredSearch);
+
+ const mockSearchTokenRunnerType = {
+ type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ value: { data: 'INSTANCE_TYPE', operator: '=' },
+ };
+
+ const triggerInfiniteScroll = () =>
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
+
+ const createMockApolloProvider = (handler, cancelableHandler, countHandler) => {
+ const requestHandlers = [
+ [getAllJobsQuery, handler],
+ [getCancelableJobsQuery, cancelableHandler],
+ [getAllJobsCount, countHandler],
+ ];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = ({
+ handler = successHandler,
+ cancelableHandler = cancelHandler,
+ countHandler = countSuccessHandler,
+ mountFn = shallowMount,
+ data = {},
+ provideOptions = {},
+ } = {}) => {
+ wrapper = mountFn(AdminJobsTableApp, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ jobStatuses: statuses,
+ glFeatures: { adminJobsFilterRunnerType: true },
+ ...provideOptions,
+ },
+ apolloProvider: createMockApolloProvider(handler, cancelableHandler, countHandler),
+ });
+ };
+
+ describe('loading state', () => {
+ it('should display skeleton loader when loading', () => {
+ createComponent();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ expect(findTable().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+
+ it('when switching tabs only the skeleton loader should show', () => {
+ createComponent();
+
+ findTabs().vm.$emit('fetchJobsByStatus', null);
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+ });
+
+ describe('loaded state', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should display the jobs table with data', () => {
+ expect(findTable().exists()).toBe(true);
+ expect(findSkeletonLoader().exists()).toBe(false);
+ expect(findLoadingSpinner().exists()).toBe(false);
+ });
+
+ it('should refetch jobs query on fetchJobsByStatus event', async () => {
+ expect(successHandler).toHaveBeenCalledTimes(1);
+
+ await findTabs().vm.$emit('fetchJobsByStatus');
+
+ expect(successHandler).toHaveBeenCalledTimes(2);
+ });
+
+ it('avoids refetch jobs query when scope has not changed', async () => {
+ expect(successHandler).toHaveBeenCalledTimes(1);
+
+ await findTabs().vm.$emit('fetchJobsByStatus', null);
+
+ expect(successHandler).toHaveBeenCalledTimes(1);
+ });
+
+ it('should refetch jobs count query when the amount jobs and count do not match', async () => {
+ expect(countSuccessHandler).toHaveBeenCalledTimes(1);
+
+ // after applying filter a new count is fetched
+ findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(successHandler).toHaveBeenCalledTimes(2);
+
+ // tab is switched to `finished`, no count
+ await findTabs().vm.$emit('fetchJobsByStatus', ['FAILED', 'SUCCESS', 'CANCELED']);
+
+ // tab is switched back to `all`, the old filter count has to be overwritten with new count
+ await findTabs().vm.$emit('fetchJobsByStatus', null);
+
+ expect(successHandler).toHaveBeenCalledTimes(4);
+ });
+
+ describe('when infinite scrolling is triggered', () => {
+ it('does not display a skeleton loader', () => {
+ triggerInfiniteScroll();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('handles infinite scrolling by calling fetch more', async () => {
+ triggerInfiniteScroll();
+
+ await nextTick();
+
+ const pageSize = 50;
+
+ expect(findLoadingSpinner().exists()).toBe(true);
+ expect(findLoadingSpinner().attributes('aria-label')).toBe(LOADING_ARIA_LABEL);
+
+ await waitForPromises();
+
+ expect(findLoadingSpinner().exists()).toBe(false);
+
+ expect(successHandler).toHaveBeenLastCalledWith({
+ first: pageSize,
+ after: mockAllJobsResponsePaginated.data.jobs.pageInfo.endCursor,
+ });
+ });
+ });
+ });
+
+ describe('empty state', () => {
+ it('should display empty state if there are no jobs and tab scope is null', async () => {
+ createComponent({ handler: emptyHandler, mountFn: mount });
+
+ await waitForPromises();
+
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('should not display empty state if there are jobs and tab scope is not null', async () => {
+ createComponent({ handler: successHandler, mountFn: mount });
+
+ await waitForPromises();
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTable().exists()).toBe(true);
+ });
+ });
+
+ describe('error state', () => {
+ it('should show an alert if there is an error fetching the jobs data', async () => {
+ createComponent({ handler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(JOBS_FETCH_ERROR_MSG);
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('should show an alert if there is an error fetching the jobs count data', async () => {
+ createComponent({ handler: successHandler, countHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(JOBS_COUNT_ERROR_MESSAGE);
+ });
+
+ it('should show an alert if there is an error fetching the cancelable jobs data', async () => {
+ createComponent({ handler: successHandler, cancelableHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(CANCELABLE_JOBS_ERROR_MSG);
+ });
+
+ it('jobs table should still load if count query fails', async () => {
+ createComponent({ handler: successHandler, countHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('jobs table should still load if cancel query fails', async () => {
+ createComponent({ handler: successHandler, cancelableHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('jobs count should be zero if count query fails', async () => {
+ createComponent({ handler: successHandler, countHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findTabs().props('allJobsCount')).toBe(0);
+ });
+
+ it('cancel button should be hidden if query fails', async () => {
+ createComponent({ handler: successHandler, cancelableHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findCancelJobsButton().exists()).toBe(false);
+ });
+ });
+
+ describe('cancel jobs button', () => {
+ it('should display cancel all jobs button', async () => {
+ createComponent({ cancelableHandler: cancelHandler, mountFn: mount });
+
+ await waitForPromises();
+
+ expect(findCancelJobsButton().exists()).toBe(true);
+ });
+
+ it('should not display cancel all jobs button', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findCancelJobsButton().exists()).toBe(false);
+ });
+ });
+
+ describe('filtered search', () => {
+ it('should display filtered search', () => {
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ // this test should be updated once BE supports tab and filtered search filtering
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/356210
+ it.each`
+ scope | shouldDisplay
+ ${null} | ${true}
+ ${['FAILED', 'SUCCESS', 'CANCELED']} | ${false}
+ `(
+ 'with tab scope $scope the filtered search displays $shouldDisplay',
+ async ({ scope, shouldDisplay }) => {
+ createComponent();
+
+ await waitForPromises();
+
+ await findTabs().vm.$emit('fetchJobsByStatus', scope);
+
+ expect(findFilteredSearch().exists()).toBe(shouldDisplay);
+ },
+ );
+
+ describe.each`
+ searchTokens | expectedQueryParams
+ ${[]} | ${{ runnerTypes: null, statuses: null }}
+ ${[mockFailedSearchToken]} | ${{ runnerTypes: null, statuses: 'FAILED' }}
+ ${[mockFailedSearchToken, mockSearchTokenRunnerType]} | ${{ runnerTypes: 'INSTANCE_TYPE', statuses: 'FAILED' }}
+ `('when filtering jobs by searchTokens', ({ searchTokens, expectedQueryParams }) => {
+ it(`refetches jobs query including filters ${JSON.stringify(
+ expectedQueryParams,
+ )}`, async () => {
+ createComponent();
+
+ expect(successHandler).toHaveBeenCalledTimes(1);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', searchTokens);
+
+ expect(successHandler).toHaveBeenCalledTimes(2);
+ expect(successHandler).toHaveBeenNthCalledWith(2, { first: 50, ...expectedQueryParams });
+ });
+
+ it(`refetches jobs count query including filters ${JSON.stringify(
+ expectedQueryParams,
+ )}`, async () => {
+ createComponent();
+
+ expect(countSuccessHandler).toHaveBeenCalledTimes(1);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', searchTokens);
+
+ expect(countSuccessHandler).toHaveBeenCalledTimes(2);
+ expect(countSuccessHandler).toHaveBeenNthCalledWith(2, expectedQueryParams);
+ });
+ });
+
+ it('shows raw text warning when user inputs raw text', async () => {
+ const expectedWarning = {
+ message: RAW_TEXT_WARNING_ADMIN,
+ type: 'warning',
+ };
+
+ createComponent();
+
+ expect(successHandler).toHaveBeenCalledTimes(1);
+ expect(countSuccessHandler).toHaveBeenCalledTimes(1);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']);
+
+ expect(createAlert).toHaveBeenCalledWith(expectedWarning);
+ expect(successHandler).toHaveBeenCalledTimes(1);
+ expect(countSuccessHandler).toHaveBeenCalledTimes(1);
+ });
+
+ it('updates URL query string when filtering jobs by status', async () => {
+ createComponent();
+
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED`,
+ });
+ });
+
+ it('resets query param after clearing tokens', () => {
+ createComponent();
+
+ jest.spyOn(urlUtils, 'updateHistory');
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 50,
+ statuses: 'FAILED',
+ runnerTypes: null,
+ });
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED`,
+ });
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', []);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/`,
+ });
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 50,
+ statuses: null,
+ runnerTypes: null,
+ });
+ });
+
+ describe('when feature flag `adminJobsFilterRunnerType` is disabled', () => {
+ const provideOptions = { glFeatures: { adminJobsFilterRunnerType: false } };
+
+ describe.each`
+ searchTokens | expectedQueryParams
+ ${[]} | ${{ statuses: null }}
+ ${[mockFailedSearchToken]} | ${{ statuses: 'FAILED' }}
+ ${[mockFailedSearchToken, mockSearchTokenRunnerType]} | ${{ statuses: 'FAILED' }}
+ `('when filtering jobs by searchTokens', ({ searchTokens, expectedQueryParams }) => {
+ it(`refetches jobs query including filters ${JSON.stringify(
+ expectedQueryParams,
+ )}`, async () => {
+ createComponent({ provideOptions });
+
+ expect(successHandler).toHaveBeenCalledTimes(1);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', searchTokens);
+
+ expect(successHandler).toHaveBeenCalledTimes(2);
+ expect(successHandler).toHaveBeenNthCalledWith(2, { first: 50, ...expectedQueryParams });
+ });
+
+ it(`refetches jobs count query including filters ${JSON.stringify(
+ expectedQueryParams,
+ )}`, async () => {
+ createComponent({ provideOptions });
+
+ expect(countSuccessHandler).toHaveBeenCalledTimes(1);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', searchTokens);
+
+ expect(countSuccessHandler).toHaveBeenCalledTimes(2);
+ expect(countSuccessHandler).toHaveBeenNthCalledWith(2, expectedQueryParams);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js b/spec/frontend/ci/admin/jobs_table/components/cancel_jobs_modal_spec.js
index d90393d8ab3..c3d1d0266f4 100644
--- a/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
+++ b/spec/frontend/ci/admin/jobs_table/components/cancel_jobs_modal_spec.js
@@ -4,7 +4,7 @@ import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import CancelJobsModal from '~/pages/admin/jobs/components/cancel_jobs_modal.vue';
+import CancelJobsModal from '~/ci/admin/jobs_table/components/cancel_jobs_modal.vue';
import { setVueErrorHandler } from '../../../../__helpers__/set_vue_error_handler';
jest.mock('~/lib/utils/url_utility', () => ({
diff --git a/spec/frontend/ci/admin/jobs_table/components/cancel_jobs_spec.js b/spec/frontend/ci/admin/jobs_table/components/cancel_jobs_spec.js
new file mode 100644
index 00000000000..2884e4ed521
--- /dev/null
+++ b/spec/frontend/ci/admin/jobs_table/components/cancel_jobs_spec.js
@@ -0,0 +1,54 @@
+import { GlButton } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { TEST_HOST } from 'helpers/test_constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CancelJobs from '~/ci/admin/jobs_table/components/cancel_jobs.vue';
+import CancelJobsModal from '~/ci/admin/jobs_table/components/cancel_jobs_modal.vue';
+import { CANCEL_JOBS_MODAL_ID, CANCEL_BUTTON_TOOLTIP } from '~/ci/admin/jobs_table/constants';
+
+describe('CancelJobs component', () => {
+ let wrapper;
+
+ const findCancelJobs = () => wrapper.findComponent(CancelJobs);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(CancelJobsModal);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(CancelJobs, {
+ directives: {
+ GlModal: createMockDirective('gl-modal'),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ propsData: {
+ url: `${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('has correct inputs', () => {
+ expect(findCancelJobs().props().url).toBe(`${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`);
+ });
+
+ it('has correct button variant', () => {
+ expect(findButton().props().variant).toBe('danger');
+ });
+
+ it('checks that button and modal are connected', () => {
+ const buttonModalDirective = getBinding(findButton().element, 'gl-modal');
+ const modalId = findModal().props('modalId');
+
+ expect(buttonModalDirective.value).toBe(CANCEL_JOBS_MODAL_ID);
+ expect(modalId).toBe(CANCEL_JOBS_MODAL_ID);
+ });
+
+ it('checks that tooltip is displayed', () => {
+ const buttonTooltipDirective = getBinding(findButton().element, 'gl-tooltip');
+
+ expect(buttonTooltipDirective.value).toBe(CANCEL_BUTTON_TOOLTIP);
+ });
+});
diff --git a/spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js b/spec/frontend/ci/admin/jobs_table/components/cells/project_cell_spec.js
index 3366d60d9f3..3e391e74394 100644
--- a/spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js
+++ b/spec/frontend/ci/admin/jobs_table/components/cells/project_cell_spec.js
@@ -1,7 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import ProjectCell from '~/pages/admin/jobs/components/table/cell/project_cell.vue';
-import { mockAllJobsNodes } from '../../../../../../jobs/mock_data';
+import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
+import { mockAllJobsNodes } from 'jest/ci/jobs_mock_data';
const mockJob = mockAllJobsNodes[0];
diff --git a/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js b/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js
index 2f76ad66dd5..2f1dae71572 100644
--- a/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js
+++ b/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js
@@ -1,8 +1,8 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import RunnerCell from '~/pages/admin/jobs/components/table/cells/runner_cell.vue';
-import { RUNNER_EMPTY_TEXT } from '~/pages/admin/jobs/components/constants';
-import { allRunnersData } from '../../../../../../ci/runner/mock_data';
+import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
+import { RUNNER_EMPTY_TEXT } from '~/ci/admin/jobs_table/constants';
+import { allRunnersData } from 'jest/ci/runner/mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
diff --git a/spec/frontend/pages/admin/jobs/components/jobs_skeleton_loader_spec.js b/spec/frontend/ci/admin/jobs_table/components/jobs_skeleton_loader_spec.js
index 03e5cd75420..0d2f5f58121 100644
--- a/spec/frontend/pages/admin/jobs/components/jobs_skeleton_loader_spec.js
+++ b/spec/frontend/ci/admin/jobs_table/components/jobs_skeleton_loader_spec.js
@@ -1,6 +1,6 @@
import { GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue';
+import JobsSkeletonLoader from '~/ci/admin/jobs_table/components/jobs_skeleton_loader.vue';
describe('jobs_skeleton_loader.vue', () => {
let wrapper;
diff --git a/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/ci/admin/jobs_table/graphql/cache_config_spec.js
index 59e9eda6343..36fbbafac44 100644
--- a/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js
+++ b/spec/frontend/ci/admin/jobs_table/graphql/cache_config_spec.js
@@ -1,9 +1,9 @@
-import cacheConfig from '~/pages/admin/jobs/components/table/graphql/cache_config';
+import cacheConfig from '~/ci/admin/jobs_table/graphql/cache_config';
import {
CIJobConnectionExistingCache,
CIJobConnectionIncomingCache,
CIJobConnectionIncomingCacheRunningStatus,
-} from '../../../../../../jobs/mock_data';
+} from 'jest/ci/jobs_mock_data';
const firstLoadArgs = { first: 3, statuses: 'PENDING' };
const runningArgs = { first: 3, statuses: 'RUNNING' };
diff --git a/spec/frontend/ci/artifacts/components/feedback_banner_spec.js b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
deleted file mode 100644
index 53e0fdac6f6..00000000000
--- a/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { GlBanner } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
-import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
-import {
- I18N_FEEDBACK_BANNER_TITLE,
- I18N_FEEDBACK_BANNER_BUTTON,
- FEEDBACK_URL,
-} from '~/ci/artifacts/constants';
-
-const mockBannerImagePath = 'banner/image/path';
-
-describe('Artifacts management feedback banner', () => {
- let wrapper;
- let userCalloutDismissSpy;
-
- const findBanner = () => wrapper.findComponent(GlBanner);
-
- const createComponent = ({ shouldShowCallout = true } = {}) => {
- userCalloutDismissSpy = jest.fn();
-
- wrapper = shallowMount(FeedbackBanner, {
- provide: {
- artifactsManagementFeedbackImagePath: mockBannerImagePath,
- },
- stubs: {
- UserCalloutDismisser: makeMockUserCalloutDismisser({
- dismiss: userCalloutDismissSpy,
- shouldShowCallout,
- }),
- },
- });
- };
-
- it('is displayed with the correct props', () => {
- createComponent();
-
- expect(findBanner().props()).toMatchObject({
- title: I18N_FEEDBACK_BANNER_TITLE,
- buttonText: I18N_FEEDBACK_BANNER_BUTTON,
- buttonLink: FEEDBACK_URL,
- svgPath: mockBannerImagePath,
- });
- });
-
- it('dismisses the callout when closed', () => {
- createComponent();
-
- findBanner().vm.$emit('close');
-
- expect(userCalloutDismissSpy).toHaveBeenCalled();
- });
-
- it('is not displayed once it has been dismissed', () => {
- createComponent({ shouldShowCallout: false });
-
- expect(findBanner().exists()).toBe(false);
- });
-});
diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
index e062140246b..1cbb1a714c9 100644
--- a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -13,7 +13,6 @@ import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import waitForPromises from 'helpers/wait_for_promises';
import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
-import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
@@ -46,8 +45,6 @@ describe('JobArtifactsTable component', () => {
const mockToastShow = jest.fn();
- const findBanner = () => wrapper.findComponent(FeedbackBanner);
-
const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
const findTable = () => wrapper.findComponent(GlTable);
const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails);
@@ -162,7 +159,6 @@ describe('JobArtifactsTable component', () => {
projectPath: 'project/path',
projectId,
canDestroyArtifacts,
- artifactsManagementFeedbackImagePath: 'banner/image/path',
},
mocks: {
$toast: {
@@ -175,12 +171,6 @@ describe('JobArtifactsTable component', () => {
});
};
- it('renders feedback banner', () => {
- createComponent();
-
- expect(findBanner().exists()).toBe(true);
- });
-
it('when loading, shows a loading state', () => {
createComponent();
@@ -373,6 +363,7 @@ describe('JobArtifactsTable component', () => {
it('is disabled when job has no metadata.gz', async () => {
const jobWithoutMetadata = {
...job,
+ hasArtifacts: true,
artifacts: { nodes: [archiveArtifact] },
};
@@ -389,6 +380,7 @@ describe('JobArtifactsTable component', () => {
it('is disabled when job has no artifacts', async () => {
const jobWithoutArtifacts = {
...job,
+ hasArtifacts: false,
artifacts: { nodes: [] },
};
diff --git a/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js b/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
deleted file mode 100644
index 8990a70d4ef..00000000000
--- a/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import $ from 'jquery';
-import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
-import htmlPipelineSchedulesEditWithVariables from 'test_fixtures/pipeline_schedules/edit_with_variables.html';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import VariableList from '~/ci/ci_variable_list/ci_variable_list';
-
-const HIDE_CLASS = 'hide';
-
-describe('VariableList', () => {
- let $wrapper;
- let variableList;
-
- describe('with only key/value inputs', () => {
- describe('with no variables', () => {
- beforeEach(() => {
- setHTMLFixture(htmlPipelineSchedulesEdit);
- $wrapper = $('.js-ci-variable-list-section');
-
- variableList = new VariableList({
- container: $wrapper,
- formField: 'schedule',
- });
- variableList.init();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('should remove the row when clicking the remove button', () => {
- $wrapper.find('.js-row-remove-button').trigger('click');
-
- expect($wrapper.find('.js-row').length).toBe(0);
- });
-
- it('should add another row when editing the last rows key input', () => {
- const $row = $wrapper.find('.js-row');
- $row.find('.js-ci-variable-input-key').val('foo').trigger('input');
-
- expect($wrapper.find('.js-row').length).toBe(2);
-
- // Check for the correct default in the new row
- const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
-
- expect($keyInput.val()).toBe('');
- });
-
- it('should add another row when editing the last rows value textarea', () => {
- const $row = $wrapper.find('.js-row');
- $row.find('.js-ci-variable-input-value').val('foo').trigger('input');
-
- expect($wrapper.find('.js-row').length).toBe(2);
-
- // Check for the correct default in the new row
- const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
-
- expect($valueInput.val()).toBe('');
- });
-
- it('should remove empty row after blurring', () => {
- const $row = $wrapper.find('.js-row');
- $row.find('.js-ci-variable-input-key').val('foo').trigger('input');
-
- expect($wrapper.find('.js-row').length).toBe(2);
-
- $row.find('.js-ci-variable-input-key').val('').trigger('input').trigger('blur');
-
- expect($wrapper.find('.js-row').length).toBe(1);
- });
- });
-
- describe('with persisted variables', () => {
- beforeEach(() => {
- setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
- $wrapper = $('.js-ci-variable-list-section');
-
- variableList = new VariableList({
- container: $wrapper,
- formField: 'schedule',
- });
- variableList.init();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('should have "Reveal values" button initially when there are already variables', () => {
- expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values');
- });
-
- it('should reveal hidden values', () => {
- const $row = $wrapper.find('.js-row:first-child');
- const $inputValue = $row.find('.js-ci-variable-input-value');
- const $placeholder = $row.find('.js-secret-value-placeholder');
-
- expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
- expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
-
- // Reveal values
- $wrapper.find('.js-secret-value-reveal-button').click();
-
- expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
- expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
- });
- });
- });
-
- describe('toggleEnableRow method', () => {
- beforeEach(() => {
- setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
- $wrapper = $('.js-ci-variable-list-section');
-
- variableList = new VariableList({
- container: $wrapper,
- formField: 'variables',
- });
- variableList.init();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('should disable all key inputs', () => {
- expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
-
- variableList.toggleEnableRow(false);
-
- expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
- });
-
- it('should disable all remove buttons', () => {
- expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
-
- variableList.toggleEnableRow(false);
-
- expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
- });
-
- it('should enable all remove buttons', () => {
- variableList.toggleEnableRow(false);
-
- expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
-
- variableList.toggleEnableRow(true);
-
- expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
- });
-
- it('should enable all key inputs', () => {
- variableList.toggleEnableRow(false);
-
- expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
-
- variableList.toggleEnableRow(true);
-
- expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
- });
- });
-});
diff --git a/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js b/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
deleted file mode 100644
index 3ef5427f288..00000000000
--- a/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import $ from 'jquery';
-import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
-
-describe('NativeFormVariableList', () => {
- let $wrapper;
-
- beforeEach(() => {
- setHTMLFixture(htmlPipelineSchedulesEdit);
- $wrapper = $('.js-ci-variable-list-section');
-
- setupNativeFormVariableList({
- container: $wrapper,
- formField: 'schedule',
- });
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('onFormSubmit', () => {
- it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
- const $row = $wrapper.find('.js-row');
-
- expect($row.find('.js-ci-variable-input-key').attr('name')).toBe(
- 'schedule[variables_attributes][][key]',
- );
-
- expect($row.find('.js-ci-variable-input-value').attr('name')).toBe(
- 'schedule[variables_attributes][][secret_value]',
- );
-
- $wrapper.closest('form').trigger('trigger-submit');
-
- expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('');
- expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('');
- });
- });
-});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
index 762c9611dac..ab5d914a6a1 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
@@ -1,42 +1,90 @@
-import { GlDrawer, GlFormSelect } from '@gitlab/ui';
+import { GlDrawer, GlFormCombobox, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
+import { awsTokenList } from '~/ci/ci_variable_list/components/ci_variable_autocomplete_tokens';
import {
ADD_VARIABLE_ACTION,
+ DRAWER_EVENT_LABEL,
+ EDIT_VARIABLE_ACTION,
+ EVENT_ACTION,
variableOptions,
+ projectString,
variableTypes,
} from '~/ci/ci_variable_list/constants';
+import { mockTracking } from 'helpers/tracking_helper';
+import { mockVariablesWithScopes } from '../mocks';
describe('CI Variable Drawer', () => {
let wrapper;
+ let trackingSpy;
+
+ const mockProjectVariable = mockVariablesWithScopes(projectString)[0];
+ const mockProjectVariableFileType = mockVariablesWithScopes(projectString)[1];
+ const mockEnvScope = 'staging';
+ const mockEnvironments = ['*', 'dev', 'staging', 'production'];
+
+ // matches strings that contain at least 8 consecutive characters consisting of only
+ // letters (both uppercase and lowercase), digits, or the specified special characters
+ const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
+
+ // matches strings that consist of at least 8 or more non-whitespace characters
+ const maskableRawRegex = '^\\S{8,}$';
const defaultProps = {
areEnvironmentsLoading: false,
- hasEnvScopeQuery: true,
+ areScopedVariablesAvailable: true,
+ environments: mockEnvironments,
+ hideEnvironmentScope: false,
+ selectedVariable: {},
mode: ADD_VARIABLE_ACTION,
};
- const createComponent = ({ mountFn = shallowMountExtended, props = {} } = {}) => {
+ const defaultProvide = {
+ isProtectedByDefault: true,
+ environmentScopeLink: '/help/environments',
+ maskableRawRegex,
+ maskableRegex,
+ };
+
+ const createComponent = ({
+ mountFn = shallowMountExtended,
+ props = {},
+ provide = {},
+ stubs = {},
+ } = {}) => {
wrapper = mountFn(CiVariableDrawer, {
propsData: {
...defaultProps,
...props,
},
provide: {
- environmentScopeLink: '/help/environments',
+ ...defaultProvide,
+ ...provide,
},
+ stubs,
});
};
+ const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-btn');
+ const findDisabledEnvironmentScopeDropdown = () => wrapper.findComponent(GlFormInput);
const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findEnvironmentScopeDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
+ const findExpandedCheckbox = () => wrapper.findByTestId('ci-variable-expanded-checkbox');
+ const findKeyField = () => wrapper.findComponent(GlFormCombobox);
+ const findMaskedCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox');
+ const findProtectedCheckbox = () => wrapper.findByTestId('ci-variable-protected-checkbox');
+ const findValueField = () => wrapper.findByTestId('ci-variable-value');
+ const findValueLabel = () => wrapper.findByTestId('ci-variable-value-label');
+ const findTitle = () => findDrawer().find('h2');
const findTypeDropdown = () => wrapper.findComponent(GlFormSelect);
describe('validations', () => {
- beforeEach(() => {
- createComponent({ mountFn: mountExtended });
- });
-
describe('type dropdown', () => {
+ beforeEach(() => {
+ createComponent({ mountFn: mountExtended });
+ });
+
it('adds each type option as a dropdown item', () => {
expect(findTypeDropdown().findAll('option')).toHaveLength(variableOptions.length);
@@ -50,20 +98,288 @@ describe('CI Variable Drawer', () => {
variableTypes.envType,
);
});
+
+ it('renders the selected variable type', () => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ areEnvironmentsLoading: true,
+ selectedVariable: mockProjectVariableFileType,
+ },
+ });
+
+ expect(findTypeDropdown().element.value).toBe(variableTypes.fileType);
+ });
+ });
+
+ describe('environment scope dropdown', () => {
+ it('passes correct props to the dropdown', () => {
+ createComponent({
+ props: {
+ areEnvironmentsLoading: true,
+ selectedVariable: { ...mockProjectVariable, environmentScope: mockEnvScope },
+ },
+ stubs: { CiEnvironmentsDropdown },
+ });
+
+ expect(findEnvironmentScopeDropdown().props()).toMatchObject({
+ areEnvironmentsLoading: true,
+ environments: mockEnvironments,
+ selectedEnvironmentScope: mockEnvScope,
+ });
+ });
+
+ it('hides environment scope dropdown when hideEnvironmentScope is true', () => {
+ createComponent({
+ props: { hideEnvironmentScope: true },
+ stubs: { CiEnvironmentsDropdown },
+ });
+
+ expect(findEnvironmentScopeDropdown().exists()).toBe(false);
+ });
+
+ it('disables the environment scope dropdown when areScopedVariablesAvailable is false', () => {
+ createComponent({
+ mountFn: mountExtended,
+ props: { areScopedVariablesAvailable: false },
+ });
+
+ expect(findEnvironmentScopeDropdown().exists()).toBe(false);
+ expect(findDisabledEnvironmentScopeDropdown().attributes('readonly')).toBe('readonly');
+ });
+ });
+
+ describe('protected flag', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('is true by default when isProtectedByDefault is true', () => {
+ expect(findProtectedCheckbox().attributes('checked')).toBeDefined();
+ });
+
+ it('is not checked when isProtectedByDefault is false', () => {
+ createComponent({ provide: { isProtectedByDefault: false } });
+
+ expect(findProtectedCheckbox().attributes('checked')).toBeUndefined();
+ });
+
+ it('inherits value of selected variable when editing', () => {
+ createComponent({
+ props: {
+ selectedVariable: mockProjectVariableFileType,
+ mode: EDIT_VARIABLE_ACTION,
+ },
+ });
+
+ expect(findProtectedCheckbox().attributes('checked')).toBeUndefined();
+ });
+ });
+
+ describe('masked flag', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('is false by default', () => {
+ expect(findMaskedCheckbox().attributes('checked')).toBeUndefined();
+ });
+
+ it('inherits value of selected variable when editing', () => {
+ createComponent({
+ props: {
+ selectedVariable: mockProjectVariableFileType,
+ mode: EDIT_VARIABLE_ACTION,
+ },
+ });
+
+ expect(findMaskedCheckbox().attributes('checked')).toBeDefined();
+ });
+ });
+
+ describe('expanded flag', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('is true by default when adding a variable', () => {
+ expect(findExpandedCheckbox().attributes('checked')).toBeDefined();
+ });
+
+ it('inherits value of selected variable when editing', () => {
+ createComponent({
+ props: {
+ selectedVariable: mockProjectVariableFileType,
+ mode: EDIT_VARIABLE_ACTION,
+ },
+ });
+
+ expect(findExpandedCheckbox().attributes('checked')).toBeUndefined();
+ });
+
+ it("sets the variable's raw value", async () => {
+ await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
+ await findExpandedCheckbox().vm.$emit('change');
+ await findConfirmBtn().vm.$emit('click');
+
+ const sentRawValue = wrapper.emitted('add-variable')[0][0].raw;
+ expect(sentRawValue).toBe(!defaultProps.raw);
+ });
+
+ it('shows help text when variable is not expanded (will be evaluated as raw)', async () => {
+ expect(findExpandedCheckbox().attributes('checked')).toBeDefined();
+ expect(findDrawer().text()).not.toContain(
+ 'Variable value will be evaluated as raw string.',
+ );
+
+ await findExpandedCheckbox().vm.$emit('change');
+
+ expect(findExpandedCheckbox().attributes('checked')).toBeUndefined();
+ expect(findDrawer().text()).toContain('Variable value will be evaluated as raw string.');
+ });
+
+ it('shows help text when variable is expanded and contains the $ character', async () => {
+ expect(findDrawer().text()).not.toContain(
+ 'Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
+ );
+
+ await findValueField().vm.$emit('input', '$NEW_VALUE');
+
+ expect(findDrawer().text()).toContain(
+ 'Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
+ );
+ });
+ });
+
+ describe('key', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('prompts AWS tokens as options', () => {
+ expect(findKeyField().props('tokenList')).toBe(awsTokenList);
+ });
+
+ it('cannot submit with empty key', async () => {
+ expect(findConfirmBtn().attributes('disabled')).toBeDefined();
+
+ await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
+
+ expect(findConfirmBtn().attributes('disabled')).toBeUndefined();
+ });
+ });
+
+ describe('value', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('can submit empty value', async () => {
+ await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
+
+ // value is empty by default
+ expect(findConfirmBtn().attributes('disabled')).toBeUndefined();
+ });
+
+ describe.each`
+ value | canSubmit | trackingErrorProperty
+ ${'secretValue'} | ${true} | ${null}
+ ${'~v@lid:symbols.'} | ${true} | ${null}
+ ${'short'} | ${false} | ${null}
+ ${'multiline\nvalue'} | ${false} | ${'\n'}
+ ${'dollar$ign'} | ${false} | ${'$'}
+ ${'unsupported|char'} | ${false} | ${'|'}
+ `('masking requirements', ({ value, canSubmit, trackingErrorProperty }) => {
+ beforeEach(async () => {
+ createComponent();
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
+ await findValueField().vm.$emit('input', value);
+ await findMaskedCheckbox().vm.$emit('input', true);
+ });
+
+ it(`${
+ canSubmit ? 'can submit' : 'shows validation errors and disables submit button'
+ } when value is '${value}'`, () => {
+ if (canSubmit) {
+ expect(findValueLabel().attributes('invalid-feedback')).toBe('');
+ expect(findConfirmBtn().attributes('disabled')).toBeUndefined();
+ } else {
+ expect(findValueLabel().attributes('invalid-feedback')).toBe(
+ 'This variable value does not meet the masking requirements.',
+ );
+ expect(findConfirmBtn().attributes('disabled')).toBeDefined();
+ }
+ });
+
+ it(`${
+ trackingErrorProperty ? 'sends the correct' : 'does not send the'
+ } variable validation tracking event when value is '${value}'`, () => {
+ const trackingEventSent = trackingErrorProperty ? 1 : 0;
+ expect(trackingSpy).toHaveBeenCalledTimes(trackingEventSent);
+
+ if (trackingErrorProperty) {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
+ label: DRAWER_EVENT_LABEL,
+ property: trackingErrorProperty,
+ });
+ }
+ });
+ });
+
+ it('only sends the tracking event once', async () => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
+ await findMaskedCheckbox().vm.$emit('input', true);
+
+ expect(trackingSpy).toHaveBeenCalledTimes(0);
+
+ await findValueField().vm.$emit('input', 'unsupported|char');
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+
+ await findValueField().vm.$emit('input', 'dollar$ign');
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ });
});
});
describe('drawer events', () => {
- beforeEach(() => {
+ it('emits `close-form` when closing the drawer', async () => {
createComponent();
- });
- it('emits `close-form` when closing the drawer', async () => {
expect(wrapper.emitted('close-form')).toBeUndefined();
await findDrawer().vm.$emit('close');
expect(wrapper.emitted('close-form')).toHaveLength(1);
});
+
+ describe('when adding a variable', () => {
+ beforeEach(() => {
+ createComponent({ stubs: { GlDrawer } });
+ });
+
+ it('title and confirm button renders the correct text', () => {
+ expect(findTitle().text()).toBe('Add Variable');
+ expect(findConfirmBtn().text()).toBe('Add Variable');
+ });
+ });
+
+ describe('when editing a variable', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { mode: EDIT_VARIABLE_ACTION },
+ stubs: { GlDrawer },
+ });
+ });
+
+ it('title and confirm button renders the correct text', () => {
+ expect(findTitle().text()).toBe('Edit Variable');
+ expect(findConfirmBtn().text()).toBe('Edit Variable');
+ });
+ });
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index f5737c61eea..79dd638e2bd 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -77,6 +77,21 @@ describe('Ci variable table', () => {
selectedVariable: {},
});
});
+
+ it('passes props down correctly to the ci drawer', async () => {
+ createComponent({ featureFlags: { ciVariableDrawer: true } });
+
+ await findCiVariableTable().vm.$emit('set-selected-variable');
+
+ expect(findCiVariableDrawer().props()).toEqual({
+ areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
+ areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
+ environments: defaultProps.environments,
+ hideEnvironmentScope: defaultProps.hideEnvironmentScope,
+ mode: ADD_VARIABLE_ACTION,
+ selectedVariable: {},
+ });
+ });
});
describe.each`
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
index 39c03a41660..de24c389511 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
@@ -105,9 +105,8 @@ describe('Ci variable table', () => {
index | text
${0} | ${'Key (Click to sort descending)'}
${1} | ${'Value'}
- ${2} | ${'Attributes'}
- ${3} | ${'Environments'}
- ${4} | ${'Actions'}
+ ${2} | ${'Environments'}
+ ${3} | ${'Actions'}
`('renders the $text column', ({ index, text }) => {
expect(findTableColumnText(index)).toEqual(text);
});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/ci/common/pipelines_table_spec.js
index 950a6b21e16..26dd1a2fcc5 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/ci/common/pipelines_table_spec.js
@@ -4,23 +4,23 @@ import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
-import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
-import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
-import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
-import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
-import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
+import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
+import PipelineTriggerer from '~/ci/pipelines_page/components/pipeline_triggerer.vue';
+import PipelineUrl from '~/ci/pipelines_page/components/pipeline_url.vue';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
+import PipelinesTimeago from '~/ci/pipelines_page/components/time_ago.vue';
import {
PipelineKeyOptions,
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
TRACKING_CATEGORIES,
-} from '~/pipelines/constants';
+} from '~/ci/constants';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-jest.mock('~/pipelines/event_hub');
+jest.mock('~/ci/event_hub');
describe('Pipelines Table', () => {
let pipeline;
diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/ci/common/private/job_links_layer_spec.js
index 88ba84c395a..c2defc8d770 100644
--- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js
+++ b/spec/frontend/ci/common/private/job_links_layer_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
-import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
-import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
+import LinksInner from '~/ci/pipeline_details/graph/components/links_inner.vue';
+import LinksLayer from '~/ci/common/private/job_links_layer.vue';
-import { generateResponse } from '../graph/mock_data';
+import { generateResponse } from 'jest/ci/pipeline_details/graph/mock_data';
describe('links layer component', () => {
let wrapper;
diff --git a/spec/frontend/ci/common/private/jobs_filtered_search/jobs_filtered_search_spec.js b/spec/frontend/ci/common/private/jobs_filtered_search/jobs_filtered_search_spec.js
new file mode 100644
index 00000000000..079738557a4
--- /dev/null
+++ b/spec/frontend/ci/common/private/jobs_filtered_search/jobs_filtered_search_spec.js
@@ -0,0 +1,123 @@
+import { GlFilteredSearch } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import {
+ OPERATORS_IS,
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+ TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ TOKEN_TITLE_JOBS_RUNNER_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
+import { mockFailedSearchToken } from 'jest/ci/jobs_mock_data';
+
+describe('Jobs filtered search', () => {
+ let wrapper;
+
+ const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
+ const getSearchToken = (type) =>
+ findFilteredSearch()
+ .props('availableTokens')
+ .find((token) => token.type === type);
+
+ const findStatusToken = () => getSearchToken('status');
+ const findRunnerTypeToken = () => getSearchToken('jobs-runner-type');
+
+ const createComponent = (props, provideOptions = {}) => {
+ wrapper = shallowMount(JobsFilteredSearch, {
+ propsData: {
+ ...props,
+ },
+ provide: {
+ glFeatures: { adminJobsFilterRunnerType: true },
+ ...provideOptions,
+ },
+ });
+ };
+
+ it('displays filtered search', () => {
+ createComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ it('displays status token', () => {
+ createComponent();
+
+ expect(findStatusToken()).toMatchObject({
+ type: TOKEN_TYPE_STATUS,
+ icon: 'status',
+ title: TOKEN_TITLE_STATUS,
+ unique: true,
+ operators: OPERATORS_IS,
+ });
+ });
+
+ it('displays token for runner type', () => {
+ createComponent();
+
+ expect(findRunnerTypeToken()).toMatchObject({
+ type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ title: TOKEN_TITLE_JOBS_RUNNER_TYPE,
+ operators: OPERATORS_IS,
+ });
+ });
+
+ it('emits filter token to parent component', () => {
+ createComponent();
+
+ findFilteredSearch().vm.$emit('submit', mockFailedSearchToken);
+
+ expect(wrapper.emitted('filterJobsBySearch')).toEqual([[mockFailedSearchToken]]);
+ });
+
+ it('filtered search value is empty array when no query string is passed', () => {
+ createComponent();
+
+ expect(findFilteredSearch().props('value')).toEqual([]);
+ });
+
+ describe('with query string passed', () => {
+ it('filtered search returns correct data shape', () => {
+ const tokenStatusesValue = 'SUCCESS';
+ const tokenRunnerTypesValue = 'INSTANCE_VALUE';
+
+ createComponent({
+ queryString: { statuses: tokenStatusesValue, runnerTypes: tokenRunnerTypesValue },
+ });
+
+ expect(findFilteredSearch().props('value')).toEqual([
+ { type: TOKEN_TYPE_STATUS, value: { data: tokenStatusesValue, operator: '=' } },
+ {
+ type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ value: { data: tokenRunnerTypesValue, operator: '=' },
+ },
+ ]);
+ });
+ });
+
+ describe('when feature flag `adminJobsFilterRunnerType` is disabled', () => {
+ const provideOptions = { glFeatures: { adminJobsFilterRunnerType: false } };
+
+ it('does not display token for runner type', () => {
+ createComponent(null, provideOptions);
+
+ expect(findRunnerTypeToken()).toBeUndefined();
+ });
+
+ describe('with query string passed', () => {
+ it('filtered search returns only data shape for search token `status` and not for search token `jobs runner type`', () => {
+ const tokenStatusesValue = 'SUCCESS';
+ const tokenRunnerTypesValue = 'INSTANCE_VALUE';
+
+ createComponent(
+ { queryString: { statuses: tokenStatusesValue, runnerTypes: tokenRunnerTypesValue } },
+ provideOptions,
+ );
+
+ expect(findFilteredSearch().props('value')).toEqual([
+ { type: TOKEN_TYPE_STATUS, value: { data: tokenStatusesValue, operator: '=' } },
+ ]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/ci/common/private/jobs_filtered_search/tokens/job_status_token_spec.js
index 6755b854f01..78a1963d939 100644
--- a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
+++ b/spec/frontend/ci/common/private/jobs_filtered_search/tokens/job_status_token_spec.js
@@ -1,7 +1,7 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
-import JobStatusToken from '~/jobs/components/filtered_search/tokens/job_status_token.vue';
+import JobStatusToken from '~/ci/common/private/jobs_filtered_search/tokens/job_status_token.vue';
import {
TOKEN_TITLE_STATUS,
TOKEN_TYPE_STATUS,
diff --git a/spec/frontend/ci/common/private/jobs_filtered_search/utils_spec.js b/spec/frontend/ci/common/private/jobs_filtered_search/utils_spec.js
new file mode 100644
index 00000000000..8f6d2368bf4
--- /dev/null
+++ b/spec/frontend/ci/common/private/jobs_filtered_search/utils_spec.js
@@ -0,0 +1,22 @@
+import { validateQueryString } from '~/ci/common/private/jobs_filtered_search/utils';
+
+describe('Filtered search utils', () => {
+ describe('validateQueryString', () => {
+ it.each`
+ queryStringObject | expected
+ ${{ statuses: 'SUCCESS' }} | ${{ statuses: 'SUCCESS' }}
+ ${{ statuses: 'failed' }} | ${{ statuses: 'FAILED' }}
+ ${{ runnerTypes: 'instance_type' }} | ${{ runnerTypes: 'INSTANCE_TYPE' }}
+ ${{ runnerTypes: 'wrong_runner_type' }} | ${null}
+ ${{ statuses: 'SUCCESS', runnerTypes: 'instance_type' }} | ${{ statuses: 'SUCCESS', runnerTypes: 'INSTANCE_TYPE' }}
+ ${{ wrong: 'SUCCESS' }} | ${null}
+ ${{ statuses: 'wrong' }} | ${null}
+ ${{ wrong: 'wrong' }} | ${null}
+ `(
+ 'when provided $queryStringObject, the expected result is $expected',
+ ({ queryStringObject, expected }) => {
+ expect(validateQueryString(queryStringObject)).toEqual(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/jobs/components/job/empty_state_spec.js b/spec/frontend/ci/job_details/components/empty_state_spec.js
index 970c2591795..992ed88e81b 100644
--- a/spec/frontend/jobs/components/job/empty_state_spec.js
+++ b/spec/frontend/ci/job_details/components/empty_state_spec.js
@@ -1,7 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import EmptyState from '~/jobs/components/job/empty_state.vue';
-import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
-import { mockFullPath, mockId } from './mock_data';
+import EmptyState from '~/ci/job_details/components/empty_state.vue';
+import ManualVariablesForm from '~/ci/job_details/components/manual_variables_form.vue';
+import { mockFullPath, mockId } from '../mock_data';
describe('Empty State', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job/environments_block_spec.js b/spec/frontend/ci/job_details/components/environments_block_spec.js
index ab36f79ea5e..56ae6b44e9a 100644
--- a/spec/frontend/jobs/components/job/environments_block_spec.js
+++ b/spec/frontend/ci/job_details/components/environments_block_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue';
+import EnvironmentsBlock from '~/ci/job_details/components/environments_block.vue';
const TEST_CLUSTER_NAME = 'test_cluster';
const TEST_CLUSTER_PATH = 'path/to/test_cluster';
diff --git a/spec/frontend/jobs/components/job/erased_block_spec.js b/spec/frontend/ci/job_details/components/erased_block_spec.js
index aeab676fc7e..7eb856f97f1 100644
--- a/spec/frontend/jobs/components/job/erased_block_spec.js
+++ b/spec/frontend/ci/job_details/components/erased_block_spec.js
@@ -1,6 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import ErasedBlock from '~/jobs/components/job/erased_block.vue';
+import ErasedBlock from '~/ci/job_details/components/erased_block.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Erased block', () => {
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/ci/job_details/components/job_header_spec.js
index da9bc0f8a2f..6fc55732353 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/ci/job_details/components/job_header_spec.js
@@ -2,7 +2,7 @@ import { GlButton, GlAvatarLink, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import HeaderCi from '~/vue_shared/components/header_ci_component.vue';
+import JobHeader from '~/ci/job_details/components/job_header.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('Header CI Component', () => {
@@ -16,6 +16,7 @@ describe('Header CI Component', () => {
text: 'failed',
details_path: 'path',
},
+ name: 'Job build_job',
time: '2017-05-08T14:57:39.781Z',
user: {
id: 1234,
@@ -25,7 +26,7 @@ describe('Header CI Component', () => {
email: 'foo@bar.com',
avatar_url: 'link',
},
- hasSidebarButton: true,
+ shouldRenderTriggeredLabel: true,
};
const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
@@ -33,12 +34,12 @@ describe('Header CI Component', () => {
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
const findStatusTooltip = () => wrapper.findComponent(GlTooltip);
- const findActionButtons = () => wrapper.findByTestId('ci-header-action-buttons');
- const findHeaderItemText = () => wrapper.findByTestId('ci-header-item-text');
+ const findActionButtons = () => wrapper.findByTestId('job-header-action-buttons');
+ const findJobName = () => wrapper.findByTestId('job-name');
const createComponent = (props, slots) => {
wrapper = extendedWrapper(
- shallowMount(HeaderCi, {
+ shallowMount(JobHeader, {
propsData: {
...defaultProps,
...props,
@@ -50,7 +51,7 @@ describe('Header CI Component', () => {
describe('render', () => {
beforeEach(() => {
- createComponent({ itemName: 'Pipeline' });
+ createComponent();
});
it('should render status badge', () => {
@@ -72,7 +73,7 @@ describe('Header CI Component', () => {
describe('user avatar', () => {
beforeEach(() => {
- createComponent({ itemName: 'Pipeline' });
+ createComponent();
});
it('contains the username', () => {
@@ -93,7 +94,6 @@ describe('Header CI Component', () => {
beforeEach(() => {
createComponent({
- itemName: 'Pipeline',
user: { ...defaultProps.user, status: { message: STATUS_MESSAGE } },
});
});
@@ -108,7 +108,6 @@ describe('Header CI Component', () => {
beforeEach(() => {
createComponent({
- itemName: 'Pipeline',
user: { ...defaultProps.user, id: `gid://gitlab/User/${1}` },
});
});
@@ -125,29 +124,19 @@ describe('Header CI Component', () => {
});
});
- describe('with item id', () => {
+ describe('job name', () => {
beforeEach(() => {
- createComponent({ itemName: 'Pipeline', itemId: '123' });
+ createComponent();
});
- it('should render item name and id', () => {
- expect(findHeaderItemText().text()).toBe('Pipeline #123');
- });
- });
-
- describe('without item id', () => {
- beforeEach(() => {
- createComponent({ itemName: 'Job build_job' });
- });
-
- it('should render item name', () => {
- expect(findHeaderItemText().text()).toBe('Job build_job');
+ it('should render the job name', () => {
+ expect(findJobName().text()).toBe('Job build_job');
});
});
describe('slot', () => {
it('should render header action buttons', () => {
- createComponent({ itemName: 'Job build_job' }, { slots: { default: 'Test Actions' } });
+ createComponent({}, { slots: { default: 'Test Actions' } });
expect(findActionButtons().exists()).toBe(true);
expect(findActionButtons().text()).toBe('Test Actions');
@@ -156,10 +145,10 @@ describe('Header CI Component', () => {
describe('shouldRenderTriggeredLabel', () => {
it('should render created keyword when the shouldRenderTriggeredLabel is false', () => {
- createComponent({ shouldRenderTriggeredLabel: false, itemName: 'Job build_job' });
+ createComponent({ shouldRenderTriggeredLabel: false });
expect(wrapper.text()).toContain('created');
- expect(wrapper.text()).not.toContain('triggered');
+ expect(wrapper.text()).not.toContain('started');
});
});
});
diff --git a/spec/frontend/jobs/components/job/job_log_controllers_spec.js b/spec/frontend/ci/job_details/components/job_log_controllers_spec.js
index 7b6d58f63d1..84c664aca34 100644
--- a/spec/frontend/jobs/components/job/job_log_controllers_spec.js
+++ b/spec/frontend/ci/job_details/components/job_log_controllers_spec.js
@@ -1,10 +1,10 @@
import { GlSearchBoxByClick } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import JobLogControllers from '~/jobs/components/job/job_log_controllers.vue';
+import JobLogControllers from '~/ci/job_details/components/job_log_controllers.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import * as commonUtils from '~/lib/utils/common_utils';
-import { mockJobLog } from '../../mock_data';
+import { mockJobLog } from 'jest/ci/jobs_mock_data';
const mockToastShow = jest.fn();
@@ -307,11 +307,9 @@ describe('Job log controllers', () => {
});
it('emits search results', () => {
- const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]];
-
findJobLogSearch().vm.$emit('submit');
- expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults);
+ expect(wrapper.emitted('searchResults')).toHaveLength(1);
});
it('clears search results', () => {
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js
index 5adedea28a5..e3d5c448338 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import CollapsibleSection from '~/jobs/components/log/collapsible_section.vue';
+import CollapsibleSection from '~/ci/job_details/components/log/collapsible_section.vue';
+import LogLineHeader from '~/ci/job_details/components/log/line_header.vue';
import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data';
describe('Job Log Collapsible Section', () => {
@@ -10,6 +11,7 @@ describe('Job Log Collapsible Section', () => {
const findCollapsibleLine = () => wrapper.find('.collapsible-line');
const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg');
+ const findLogLineHeader = () => wrapper.findComponent(LogLineHeader);
const createComponent = (props = {}) => {
wrapper = mount(CollapsibleSection, {
@@ -68,4 +70,26 @@ describe('Job Log Collapsible Section', () => {
await nextTick();
expect(wrapper.emitted('onClickCollapsibleLine').length).toBe(1);
});
+
+ describe('with search results', () => {
+ it('passes isHighlighted prop correctly', () => {
+ const mockSearchResults = [
+ {
+ content: [{ text: 'foo' }],
+ lineNumber: 1,
+ offset: 5,
+ section: 'prepare-script',
+ section_header: true,
+ },
+ ];
+
+ createComponent({
+ section: collapsibleSectionOpened,
+ jobLogEndpoint,
+ searchResults: mockSearchResults,
+ });
+
+ expect(findLogLineHeader().props('isHighlighted')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/log/duration_badge_spec.js b/spec/frontend/ci/job_details/components/log/duration_badge_spec.js
index 644d05366a0..0d5f60cefd1 100644
--- a/spec/frontend/jobs/components/log/duration_badge_spec.js
+++ b/spec/frontend/ci/job_details/components/log/duration_badge_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import DurationBadge from '~/jobs/components/log/duration_badge.vue';
+import DurationBadge from '~/ci/job_details/components/log/duration_badge.vue';
describe('Job Log Duration Badge', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/ci/job_details/components/log/line_header_spec.js
index c02d8c22655..7d1b05346f2 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_header_spec.js
@@ -1,14 +1,14 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
-import DurationBadge from '~/jobs/components/log/duration_badge.vue';
-import LineHeader from '~/jobs/components/log/line_header.vue';
-import LineNumber from '~/jobs/components/log/line_number.vue';
+import DurationBadge from '~/ci/job_details/components/log/duration_badge.vue';
+import LineHeader from '~/ci/job_details/components/log/line_header.vue';
+import LineNumber from '~/ci/job_details/components/log/line_number.vue';
describe('Job Log Header Line', () => {
let wrapper;
- const data = {
+ const defaultProps = {
line: {
content: [
{
@@ -22,7 +22,7 @@ describe('Job Log Header Line', () => {
path: '/jashkenas/underscore/-/jobs/335',
};
- const createComponent = (props = {}) => {
+ const createComponent = (props = defaultProps) => {
wrapper = mount(LineHeader, {
propsData: {
...props,
@@ -32,7 +32,7 @@ describe('Job Log Header Line', () => {
describe('line', () => {
beforeEach(() => {
- createComponent(data);
+ createComponent();
});
it('renders the line number component', () => {
@@ -40,17 +40,17 @@ describe('Job Log Header Line', () => {
});
it('renders a span the provided text', () => {
- expect(wrapper.find('span').text()).toBe(data.line.content[0].text);
+ expect(wrapper.find('span').text()).toBe(defaultProps.line.content[0].text);
});
it('renders the provided style as a class attribute', () => {
- expect(wrapper.find('span').classes()).toContain(data.line.content[0].style);
+ expect(wrapper.find('span').classes()).toContain(defaultProps.line.content[0].style);
});
});
describe('when isCloses is true', () => {
beforeEach(() => {
- createComponent({ ...data, isClosed: true });
+ createComponent({ ...defaultProps, isClosed: true });
});
it('sets icon name to be chevron-lg-right', () => {
@@ -60,7 +60,7 @@ describe('Job Log Header Line', () => {
describe('when isCloses is false', () => {
beforeEach(() => {
- createComponent({ ...data, isClosed: false });
+ createComponent({ ...defaultProps, isClosed: false });
});
it('sets icon name to be chevron-lg-down', () => {
@@ -70,7 +70,7 @@ describe('Job Log Header Line', () => {
describe('on click', () => {
beforeEach(() => {
- createComponent(data);
+ createComponent();
});
it('emits toggleLine event', async () => {
@@ -83,7 +83,7 @@ describe('Job Log Header Line', () => {
describe('with duration', () => {
beforeEach(() => {
- createComponent({ ...data, duration: '00:10' });
+ createComponent({ ...defaultProps, duration: '00:10' });
});
it('renders the duration badge', () => {
@@ -96,7 +96,7 @@ describe('Job Log Header Line', () => {
beforeEach(() => {
setWindowLocation(`http://foo.com/root/ci-project/-/jobs/6353#L77`);
- createComponent(data);
+ createComponent();
});
it('highlights line', () => {
@@ -108,12 +108,26 @@ describe('Job Log Header Line', () => {
beforeEach(() => {
setWindowLocation(`http://foo.com/root/ci-project/-/jobs/6353`);
- createComponent(data);
+ createComponent();
});
it('does not highlight line', () => {
expect(wrapper.classes()).not.toContain('gl-bg-gray-700');
});
});
+
+ describe('search results', () => {
+ it('highlights the job log lines', () => {
+ createComponent({ ...defaultProps, isHighlighted: true });
+
+ expect(wrapper.classes()).toContain('gl-bg-gray-700');
+ });
+
+ it('does not highlight the job log lines', () => {
+ createComponent();
+
+ expect(wrapper.classes()).not.toContain('gl-bg-gray-700');
+ });
+ });
});
});
diff --git a/spec/frontend/jobs/components/log/line_number_spec.js b/spec/frontend/ci/job_details/components/log/line_number_spec.js
index 4130c124a30..d5c1d0fd985 100644
--- a/spec/frontend/jobs/components/log/line_number_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_number_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import LineNumber from '~/jobs/components/log/line_number.vue';
+import LineNumber from '~/ci/job_details/components/log/line_number.vue';
describe('Job Log Line Number', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/log/line_spec.js b/spec/frontend/ci/job_details/components/log/line_spec.js
index fad7a03beef..b6f3a2b68df 100644
--- a/spec/frontend/jobs/components/log/line_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import Line from '~/jobs/components/log/line.vue';
-import LineNumber from '~/jobs/components/log/line_number.vue';
+import Line from '~/ci/job_details/components/log/line.vue';
+import LineNumber from '~/ci/job_details/components/log/line_number.vue';
import setWindowLocation from 'helpers/set_window_location_helper';
const httpUrl = 'http://example.com';
@@ -182,16 +182,6 @@ describe('Job Log Line', () => {
});
describe('job log search', () => {
- const mockSearchResults = [
- {
- offset: 1533,
- content: [{ text: '$ echo "82.71"', style: 'term-fg-l-green term-bold' }],
- section: 'step-script',
- lineNumber: 20,
- },
- { offset: 1560, content: [{ text: '82.71' }], section: 'step-script', lineNumber: 21 },
- ];
-
it('applies highlight class to search result elements', () => {
createComponent({
line: {
@@ -201,7 +191,7 @@ describe('Job Log Line', () => {
lineNumber: 21,
},
path: '/root/ci-project/-/jobs/1089',
- searchResults: mockSearchResults,
+ isHighlighted: true,
});
expect(wrapper.classes()).toContain('gl-bg-gray-700');
@@ -216,7 +206,6 @@ describe('Job Log Line', () => {
lineNumber: 29,
},
path: '/root/ci-project/-/jobs/1089',
- searchResults: mockSearchResults,
});
expect(wrapper.classes()).not.toContain('gl-bg-gray-700');
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/ci/job_details/components/log/log_spec.js
index 9407b340950..cc1621b87d6 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/ci/job_details/components/log/log_spec.js
@@ -4,9 +4,9 @@ import Vue from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { scrollToElement } from '~/lib/utils/common_utils';
-import Log from '~/jobs/components/log/log.vue';
-import LogLineHeader from '~/jobs/components/log/line_header.vue';
-import { logLinesParser } from '~/jobs/store/utils';
+import Log from '~/ci/job_details/components/log/log.vue';
+import LogLineHeader from '~/ci/job_details/components/log/line_header.vue';
+import { logLinesParser } from '~/ci/job_details/store/utils';
import { jobLog } from './mock_data';
jest.mock('~/lib/utils/common_utils', () => ({
@@ -23,8 +23,11 @@ describe('Job Log', () => {
Vue.use(Vuex);
- const createComponent = () => {
+ const createComponent = (props) => {
wrapper = mount(Log, {
+ propsData: {
+ ...props,
+ },
store,
});
};
@@ -47,6 +50,7 @@ describe('Job Log', () => {
});
const findCollapsibleLine = () => wrapper.findComponent(LogLineHeader);
+ const findAllCollapsibleLines = () => wrapper.findAllComponents(LogLineHeader);
describe('line numbers', () => {
beforeEach(() => {
@@ -131,5 +135,28 @@ describe('Job Log', () => {
expect(wrapper.find('#L6').exists()).toBe(true);
});
});
+
+ describe('with search results', () => {
+ it('passes isHighlighted prop correctly', () => {
+ const mockSearchResults = [
+ {
+ offset: 1002,
+ content: [
+ {
+ text: 'Using Docker executor with image dev.gitlab.org3',
+ },
+ ],
+ section: 'prepare-executor',
+ section_header: true,
+ lineNumber: 2,
+ },
+ ];
+
+ createComponent({ searchResults: mockSearchResults });
+
+ expect(findAllCollapsibleLines().at(0).props('isHighlighted')).toBe(true);
+ expect(findAllCollapsibleLines().at(1).props('isHighlighted')).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/ci/job_details/components/log/mock_data.js
index fa51b92a044..fa51b92a044 100644
--- a/spec/frontend/jobs/components/log/mock_data.js
+++ b/spec/frontend/ci/job_details/components/log/mock_data.js
diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/ci/job_details/components/manual_variables_form_spec.js
index 989fe5c11e9..3391cafb4fc 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/ci/job_details/components/manual_variables_form_spec.js
@@ -6,14 +6,14 @@ import { createAlert } from '~/alert';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TYPENAME_CI_BUILD } from '~/graphql_shared/constants';
-import { JOB_GRAPHQL_ERRORS } from '~/jobs/constants';
+import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import waitForPromises from 'helpers/wait_for_promises';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
-import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
-import playJobMutation from '~/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql';
-import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql';
+import ManualVariablesForm from '~/ci/job_details/components/manual_variables_form.vue';
+import getJobQuery from '~/ci/job_details/graphql/queries/get_job.query.graphql';
+import playJobMutation from '~/ci/job_details/graphql/mutations/job_play_with_variables.mutation.graphql';
+import retryJobMutation from '~/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql';
import {
mockFullPath,
@@ -22,7 +22,7 @@ import {
mockJobWithVariablesResponse,
mockJobPlayMutationData,
mockJobRetryMutationData,
-} from './mock_data';
+} from '../mock_data';
const localVue = createLocalVue();
jest.mock('~/alert');
diff --git a/spec/frontend/jobs/components/job/artifacts_block_spec.js b/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js
index f9e52a5ae43..1d61bf3243f 100644
--- a/spec/frontend/jobs/components/job/artifacts_block_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js
@@ -1,7 +1,7 @@
import { GlPopover } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
-import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue';
+import ArtifactsBlock from '~/ci/job_details/components/sidebar/artifacts_block.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Artifacts block', () => {
@@ -16,10 +16,10 @@ describe('Artifacts block', () => {
});
const findArtifactRemoveElt = () => wrapper.findByTestId('artifacts-remove-timeline');
- const findJobLockedElt = () => wrapper.findByTestId('job-locked-message');
+ const findJobLockedElt = () => wrapper.findByTestId('artifacts-locked-message-content');
const findKeepBtn = () => wrapper.findByTestId('keep-artifacts');
const findDownloadBtn = () => wrapper.findByTestId('download-artifacts');
- const findBrowseBtn = () => wrapper.findByTestId('browse-artifacts');
+ const findBrowseBtn = () => wrapper.findByTestId('browse-artifacts-button');
const findArtifactsHelpLink = () => wrapper.findByTestId('artifacts-help-link');
const findPopover = () => wrapper.findComponent(GlPopover);
diff --git a/spec/frontend/jobs/components/job/commit_block_spec.js b/spec/frontend/ci/job_details/components/sidebar/commit_block_spec.js
index 1c28b5079d7..e9a848bcd11 100644
--- a/spec/frontend/jobs/components/job/commit_block_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/commit_block_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CommitBlock from '~/jobs/components/job/sidebar/commit_block.vue';
+import CommitBlock from '~/ci/job_details/components/sidebar/commit_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('Commit block', () => {
diff --git a/spec/frontend/ci/job_details/components/sidebar/external_links_block_spec.js b/spec/frontend/ci/job_details/components/sidebar/external_links_block_spec.js
new file mode 100644
index 00000000000..1f2c448f1c6
--- /dev/null
+++ b/spec/frontend/ci/job_details/components/sidebar/external_links_block_spec.js
@@ -0,0 +1,49 @@
+import { GlLink } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ExternalLinksBlock from '~/ci/job_details/components/sidebar/external_links_block.vue';
+
+describe('External links block', () => {
+ let wrapper;
+
+ const createWrapper = (propsData) => {
+ wrapper = mountExtended(ExternalLinksBlock, {
+ propsData: {
+ ...propsData,
+ },
+ });
+ };
+
+ const findAllLinks = () => wrapper.findAllComponents(GlLink);
+ const findLink = () => findAllLinks().at(0);
+
+ it('renders a list of links', () => {
+ createWrapper({
+ externalLinks: [
+ {
+ label: 'URL 1',
+ url: 'https://url1.example.com/',
+ },
+ {
+ label: 'URL 2',
+ url: 'https://url2.example.com/',
+ },
+ ],
+ });
+
+ expect(findAllLinks()).toHaveLength(2);
+ });
+
+ it('renders a link', () => {
+ createWrapper({
+ externalLinks: [
+ {
+ label: 'Example URL',
+ url: 'https://example.com/',
+ },
+ ],
+ });
+
+ expect(findLink().text()).toBe('Example URL');
+ expect(findLink().attributes('href')).toBe('https://example.com/');
+ });
+});
diff --git a/spec/frontend/jobs/components/job/job_container_item_spec.js b/spec/frontend/ci/job_details/components/sidebar/job_container_item_spec.js
index 39782130d38..0eabaefd5de 100644
--- a/spec/frontend/jobs/components/job/job_container_item_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/job_container_item_spec.js
@@ -2,9 +2,9 @@ import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
-import JobContainerItem from '~/jobs/components/job/sidebar/job_container_item.vue';
+import JobContainerItem from '~/ci/job_details/components/sidebar/job_container_item.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import job from '../../mock_data';
+import job from 'jest/ci/jobs_mock_data';
describe('JobContainerItem', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js b/spec/frontend/ci/job_details/components/sidebar/job_retry_forward_deployment_modal_spec.js
index a44a13259aa..075bccd57cc 100644
--- a/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/job_retry_forward_deployment_modal_spec.js
@@ -1,9 +1,8 @@
import { GlLink, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue';
-import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '~/jobs/constants';
-import createStore from '~/jobs/store';
-import job from '../../mock_data';
+import JobRetryForwardDeploymentModal from '~/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue';
+import createStore from '~/ci/job_details/store';
+import job from 'jest/ci/jobs_mock_data';
describe('Job Retry Forward Deployment Modal', () => {
let store;
@@ -32,9 +31,11 @@ describe('Job Retry Forward Deployment Modal', () => {
describe('Modal configuration', () => {
it('should display the correct messages', () => {
const modal = findModal();
- expect(modal.attributes('title')).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.title);
- expect(modal.text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.info);
- expect(modal.text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.areYouSure);
+ expect(modal.attributes('title')).toMatch('Are you sure you want to retry this job?');
+ expect(modal.text()).toMatch(
+ "You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment. Retrying this job could result in overwriting the environment with the older source code.",
+ );
+ expect(modal.text()).toMatch('Are you sure you want to proceed?');
});
});
@@ -49,7 +50,7 @@ describe('Job Retry Forward Deployment Modal', () => {
createWrapper({ provide: { retryOutdatedJobDocsUrl } });
expect(findLink().attributes('href')).toBe(retryOutdatedJobDocsUrl);
- expect(findLink().text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.moreInfo);
+ expect(findLink().text()).toMatch('More information');
});
});
diff --git a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js b/spec/frontend/ci/job_details/components/sidebar/job_sidebar_retry_button_spec.js
index 8a63bfdc3d6..8fdf6b72ee1 100644
--- a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/job_sidebar_retry_button_spec.js
@@ -1,7 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import JobsSidebarRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
-import createStore from '~/jobs/store';
-import job from '../../mock_data';
+import JobsSidebarRetryButton from '~/ci/job_details/components/sidebar/job_sidebar_retry_button.vue';
+import createStore from '~/ci/job_details/store';
+import job from 'jest/ci/jobs_mock_data';
describe('Job Sidebar Retry Button', () => {
let store;
diff --git a/spec/frontend/jobs/components/job/jobs_container_spec.js b/spec/frontend/ci/job_details/components/sidebar/jobs_container_spec.js
index 05660880751..b2b675199ed 100644
--- a/spec/frontend/jobs/components/job/jobs_container_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/jobs_container_spec.js
@@ -1,7 +1,7 @@
import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue';
+import JobsContainer from '~/ci/job_details/components/sidebar/jobs_container.vue';
describe('Jobs List block', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js b/spec/frontend/ci/job_details/components/sidebar/sidebar_detail_row_spec.js
index 546f5392caf..52c886e3c88 100644
--- a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/sidebar_detail_row_spec.js
@@ -1,5 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import SidebarDetailRow from '~/jobs/components/job/sidebar/sidebar_detail_row.vue';
+import SidebarDetailRow from '~/ci/job_details/components/sidebar/sidebar_detail_row.vue';
import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility';
describe('Sidebar detail row', () => {
diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js
index cf182330578..1063bec6f3b 100644
--- a/spec/frontend/jobs/components/job/sidebar_header_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js
@@ -3,10 +3,10 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import SidebarHeader from '~/jobs/components/job/sidebar/sidebar_header.vue';
-import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue';
-import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
-import { mockFullPath, mockId, mockJobResponse } from './mock_data';
+import SidebarHeader from '~/ci/job_details/components/sidebar/sidebar_header.vue';
+import JobRetryButton from '~/ci/job_details/components/sidebar/job_sidebar_retry_button.vue';
+import getJobQuery from '~/ci/job_details/graphql/queries/get_job.query.graphql';
+import { mockFullPath, mockId, mockJobResponse } from '../../mock_data';
Vue.use(VueApollo);
@@ -53,6 +53,8 @@ describe('Sidebar Header', () => {
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findEraseButton = () => wrapper.findByTestId('job-log-erase-link');
+ const findNewIssueButton = () => wrapper.findByTestId('job-new-issue');
+ const findTerminalLink = () => wrapper.findByTestId('terminal-link');
const findJobName = () => wrapper.findByTestId('job-name');
const findRetryButton = () => wrapper.findComponent(JobRetryButton);
@@ -67,6 +69,8 @@ describe('Sidebar Header', () => {
expect(findCancelButton().exists()).toBe(false);
expect(findEraseButton().exists()).toBe(false);
expect(findRetryButton().exists()).toBe(false);
+ expect(findNewIssueButton().exists()).toBe(false);
+ expect(findTerminalLink().exists()).toBe(false);
});
it('renders a retry button with a path', async () => {
@@ -83,5 +87,15 @@ describe('Sidebar Header', () => {
await createComponentWithApollo({ restJob: { erase_path: 'erase/path' } });
expect(findEraseButton().exists()).toBe(true);
});
+
+ it('should render link to new issue', async () => {
+ await createComponentWithApollo({ restJob: { new_issue_path: 'new/issue/path' } });
+ expect(findNewIssueButton().attributes('href')).toBe('new/issue/path');
+ });
+
+ it('should render terminal link', async () => {
+ await createComponentWithApollo({ restJob: { terminal_path: 'terminal/path' } });
+ expect(findTerminalLink().attributes('href')).toBe('terminal/path');
+ });
});
});
diff --git a/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js b/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js
index c1028f3929d..e188d99b8b1 100644
--- a/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DetailRow from '~/jobs/components/job/sidebar/sidebar_detail_row.vue';
-import SidebarJobDetailsContainer from '~/jobs/components/job/sidebar/sidebar_job_details_container.vue';
-import createStore from '~/jobs/store';
-import job from '../../mock_data';
+import DetailRow from '~/ci/job_details/components/sidebar/sidebar_detail_row.vue';
+import SidebarJobDetailsContainer from '~/ci/job_details/components/sidebar/sidebar_job_details_container.vue';
+import createStore from '~/ci/job_details/store';
+import job from 'jest/ci/jobs_mock_data';
describe('Job Sidebar Details Container', () => {
let store;
@@ -53,6 +53,7 @@ describe('Job Sidebar Details Container', () => {
['erased_at', 'Erased: 3 weeks ago'],
['finished_at', 'Finished: 3 weeks ago'],
['queued_duration', 'Queued: 9 seconds'],
+ ['id', 'Job ID: #4757'],
['runner', 'Runner: #1 (ABCDEFGH) local ci runner'],
['coverage', 'Coverage: 20%'],
])('uses %s to render job-%s', async (detail, value) => {
@@ -77,7 +78,7 @@ describe('Job Sidebar Details Container', () => {
createWrapper();
await store.dispatch('receiveJobSuccess', job);
- expect(findAllDetailsRow()).toHaveLength(7);
+ expect(findAllDetailsRow()).toHaveLength(8);
});
describe('duration row', () => {
diff --git a/spec/frontend/jobs/components/job/sidebar_spec.js b/spec/frontend/ci/job_details/components/sidebar/sidebar_spec.js
index fbff64b4d78..88e1f41b270 100644
--- a/spec/frontend/jobs/components/job/sidebar_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/sidebar_spec.js
@@ -4,13 +4,14 @@ import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue';
-import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue';
-import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue';
-import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue';
-import StagesDropdown from '~/jobs/components/job/sidebar/stages_dropdown.vue';
-import createStore from '~/jobs/store';
-import job, { jobsInStage } from '../../mock_data';
+import ArtifactsBlock from '~/ci/job_details/components/sidebar/artifacts_block.vue';
+import ExternalLinksBlock from '~/ci/job_details/components/sidebar/external_links_block.vue';
+import JobRetryForwardDeploymentModal from '~/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue';
+import JobsContainer from '~/ci/job_details/components/sidebar/jobs_container.vue';
+import Sidebar from '~/ci/job_details/components/sidebar/sidebar.vue';
+import StagesDropdown from '~/ci/job_details/components/sidebar/stages_dropdown.vue';
+import createStore from '~/ci/job_details/store';
+import job, { jobsInStage } from 'jest/ci/jobs_mock_data';
describe('Sidebar details block', () => {
let mock;
@@ -20,8 +21,7 @@ describe('Sidebar details block', () => {
const forwardDeploymentFailure = 'forward_deployment_failure';
const findModal = () => wrapper.findComponent(JobRetryForwardDeploymentModal);
const findArtifactsBlock = () => wrapper.findComponent(ArtifactsBlock);
- const findNewIssueButton = () => wrapper.findByTestId('job-new-issue');
- const findTerminalLink = () => wrapper.findByTestId('terminal-link');
+ const findExternalLinksBlock = () => wrapper.findComponent(ExternalLinksBlock);
const findJobStagesDropdown = () => wrapper.findComponent(StagesDropdown);
const findJobsContainer = () => wrapper.findComponent(JobsContainer);
@@ -48,36 +48,6 @@ describe('Sidebar details block', () => {
});
});
- describe('without terminal path', () => {
- it('does not render terminal link', async () => {
- createWrapper();
- await store.dispatch('receiveJobSuccess', job);
-
- expect(findTerminalLink().exists()).toBe(false);
- });
- });
-
- describe('with terminal path', () => {
- it('renders terminal link', async () => {
- createWrapper();
- await store.dispatch('receiveJobSuccess', { ...job, terminal_path: 'job/43123/terminal' });
-
- expect(findTerminalLink().exists()).toBe(true);
- });
- });
-
- describe('actions', () => {
- beforeEach(() => {
- createWrapper();
- return store.dispatch('receiveJobSuccess', job);
- });
-
- it('should render link to new issue', () => {
- expect(findNewIssueButton().attributes('href')).toBe(job.new_issue_path);
- expect(findNewIssueButton().text()).toBe('New issue');
- });
- });
-
describe('forward deployment failure', () => {
describe('when the relevant data is missing', () => {
it.each`
@@ -213,4 +183,40 @@ describe('Sidebar details block', () => {
expect(findArtifactsBlock().exists()).toBe(true);
});
});
+
+ describe('external links', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('external links block is not shown if there are no external links', () => {
+ expect(findExternalLinksBlock().exists()).toBe(false);
+ });
+
+ it('external links block is shown if there are external links', async () => {
+ store.state.job.annotations = [
+ {
+ name: 'external_links',
+ data: [
+ {
+ external_link: {
+ label: 'URL 1',
+ url: 'https://url1.example.com/',
+ },
+ },
+ {
+ external_link: {
+ label: 'URL 2',
+ url: 'https://url2.example.com/',
+ },
+ },
+ ],
+ },
+ ];
+
+ await nextTick();
+
+ expect(findExternalLinksBlock().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/job/stages_dropdown_spec.js b/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js
index c42edc62183..e007896c81e 100644
--- a/spec/frontend/jobs/components/job/stages_dropdown_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/stages_dropdown_spec.js
@@ -2,20 +2,20 @@ import { GlDisclosureDropdown, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { Mousetrap } from '~/lib/mousetrap';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import StagesDropdown from '~/jobs/components/job/sidebar/stages_dropdown.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import StagesDropdown from '~/ci/job_details/components/sidebar/stages_dropdown.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import * as copyToClipboard from '~/behaviors/copy_to_clipboard';
import {
mockPipelineWithoutRef,
mockPipelineWithoutMR,
mockPipelineWithAttachedMR,
mockPipelineDetached,
-} from '../../mock_data';
+} from 'jest/ci/jobs_mock_data';
describe('Stages Dropdown', () => {
let wrapper;
- const findStatus = () => wrapper.findComponent(CiIcon);
+ const findStatus = () => wrapper.findComponent(CiBadgeLink);
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findSelectedStageText = () => findDropdown().props('toggleText');
@@ -46,7 +46,8 @@ describe('Stages Dropdown', () => {
});
it('renders pipeline status', () => {
- expect(findStatus().exists()).toBe(true);
+ expect(findStatus().props('status')).toBe(mockPipelineWithoutMR.details.status);
+ expect(findStatus().props('size')).toBe('sm');
});
it('renders dropdown with stages', () => {
diff --git a/spec/frontend/jobs/components/job/trigger_block_spec.js b/spec/frontend/ci/job_details/components/sidebar/trigger_block_spec.js
index 8bb2c1f3ad8..f2b00c42d53 100644
--- a/spec/frontend/jobs/components/job/trigger_block_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/trigger_block_spec.js
@@ -1,6 +1,6 @@
import { GlButton, GlTableLite } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import TriggerBlock from '~/jobs/components/job/sidebar/trigger_block.vue';
+import TriggerBlock from '~/ci/job_details/components/sidebar/trigger_block.vue';
describe('Trigger block', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job/stuck_block_spec.js b/spec/frontend/ci/job_details/components/stuck_block_spec.js
index 0f014a9222b..ec3b2d45a68 100644
--- a/spec/frontend/jobs/components/job/stuck_block_spec.js
+++ b/spec/frontend/ci/job_details/components/stuck_block_spec.js
@@ -1,6 +1,6 @@
import { GlBadge, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import StuckBlock from '~/jobs/components/job/stuck_block.vue';
+import StuckBlock from '~/ci/job_details/components/stuck_block.vue';
describe('Stuck Block Job component', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js b/spec/frontend/ci/job_details/components/unmet_prerequisites_block_spec.js
index 1072cdd6781..08966743901 100644
--- a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
+++ b/spec/frontend/ci/job_details/components/unmet_prerequisites_block_spec.js
@@ -1,6 +1,6 @@
import { GlAlert, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
+import UnmetPrerequisitesBlock from '~/ci/job_details/components/unmet_prerequisites_block.vue';
describe('Unmet Prerequisites Block Job component', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/ci/job_details/job_app_spec.js
index 8f5700ee22d..c2d91771495 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/ci/job_details/job_app_spec.js
@@ -5,20 +5,20 @@ import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
-import EmptyState from '~/jobs/components/job/empty_state.vue';
-import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue';
-import ErasedBlock from '~/jobs/components/job/erased_block.vue';
-import JobApp from '~/jobs/components/job/job_app.vue';
-import JobLog from '~/jobs/components/log/log.vue';
-import JobLogTopBar from 'ee_else_ce/jobs/components/job/job_log_controllers.vue';
-import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue';
-import StuckBlock from '~/jobs/components/job/stuck_block.vue';
-import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
-import createStore from '~/jobs/store';
+import EmptyState from '~/ci/job_details/components/empty_state.vue';
+import EnvironmentsBlock from '~/ci/job_details/components/environments_block.vue';
+import ErasedBlock from '~/ci/job_details/components/erased_block.vue';
+import JobApp from '~/ci/job_details/job_app.vue';
+import JobLog from '~/ci/job_details/components/log/log.vue';
+import JobLogTopBar from 'ee_else_ce/ci/job_details/components/job_log_controllers.vue';
+import Sidebar from '~/ci/job_details/components/sidebar/sidebar.vue';
+import StuckBlock from '~/ci/job_details/components/stuck_block.vue';
+import UnmetPrerequisitesBlock from '~/ci/job_details/components/unmet_prerequisites_block.vue';
+import createStore from '~/ci/job_details/store';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { MANUAL_STATUS } from '~/jobs/constants';
-import job from '../../mock_data';
+import { MANUAL_STATUS } from '~/ci/constants';
+import job from 'jest/ci/jobs_mock_data';
import { mockPendingJobData } from './mock_data';
describe('Job App', () => {
diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/ci/job_details/mock_data.js
index fb3a361c9c9..fb3a361c9c9 100644
--- a/spec/frontend/jobs/components/job/mock_data.js
+++ b/spec/frontend/ci/job_details/mock_data.js
diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/ci/job_details/store/actions_spec.js
index 73a158d52d8..bb5c1fe32bd 100644
--- a/spec/frontend/jobs/store/actions_spec.js
+++ b/spec/frontend/ci/job_details/store/actions_spec.js
@@ -26,9 +26,9 @@ import {
hideSidebar,
showSidebar,
toggleSidebar,
-} from '~/jobs/store/actions';
-import * as types from '~/jobs/store/mutation_types';
-import state from '~/jobs/store/state';
+} from '~/ci/job_details/store/actions';
+import * as types from '~/ci/job_details/store/mutation_types';
+import state from '~/ci/job_details/store/state';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
diff --git a/spec/frontend/jobs/store/getters_spec.js b/spec/frontend/ci/job_details/store/getters_spec.js
index c13b051c672..dfa5f9d4781 100644
--- a/spec/frontend/jobs/store/getters_spec.js
+++ b/spec/frontend/ci/job_details/store/getters_spec.js
@@ -1,5 +1,5 @@
-import * as getters from '~/jobs/store/getters';
-import state from '~/jobs/store/state';
+import * as getters from '~/ci/job_details/store/getters';
+import state from '~/ci/job_details/store/state';
describe('Job Store Getters', () => {
let localState;
diff --git a/spec/frontend/jobs/store/helpers.js b/spec/frontend/ci/job_details/store/helpers.js
index 402ae58971a..6b186e094e7 100644
--- a/spec/frontend/jobs/store/helpers.js
+++ b/spec/frontend/ci/job_details/store/helpers.js
@@ -1,4 +1,4 @@
-import state from '~/jobs/store/state';
+import state from '~/ci/job_details/store/state';
export const resetStore = (store) => {
store.replaceState(state());
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/ci/job_details/store/mutations_spec.js
index 89cda3b0544..0835c534fb9 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/ci/job_details/store/mutations_spec.js
@@ -1,6 +1,6 @@
-import * as types from '~/jobs/store/mutation_types';
-import mutations from '~/jobs/store/mutations';
-import state from '~/jobs/store/state';
+import * as types from '~/ci/job_details/store/mutation_types';
+import mutations from '~/ci/job_details/store/mutations';
+import state from '~/ci/job_details/store/state';
describe('Jobs Store Mutations', () => {
let stateCopy;
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/ci/job_details/store/utils_spec.js
index 37a6722c555..4ffba35761e 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/ci/job_details/store/utils_spec.js
@@ -7,7 +7,7 @@ import {
isCollapsibleSection,
findOffsetAndRemove,
getIncrementalLineNumber,
-} from '~/jobs/store/utils';
+} from '~/ci/job_details/store/utils';
import {
utilsMockData,
originalTrace,
diff --git a/spec/frontend/ci/job_details/utils_spec.js b/spec/frontend/ci/job_details/utils_spec.js
new file mode 100644
index 00000000000..7b5a97f3939
--- /dev/null
+++ b/spec/frontend/ci/job_details/utils_spec.js
@@ -0,0 +1,265 @@
+import { compactJobLog, filterAnnotations } from '~/ci/job_details/utils';
+import { mockJobLog } from 'jest/ci/jobs_mock_data';
+
+describe('Job utils', () => {
+ describe('compactJobLog', () => {
+ it('compacts job log correctly', () => {
+ const expectedResults = [
+ {
+ content: [
+ {
+ text: 'Running with gitlab-runner 15.0.0 (febb2a09)',
+ },
+ ],
+ lineNumber: 0,
+ offset: 0,
+ },
+ {
+ content: [
+ {
+ text: ' on colima-docker EwM9WzgD',
+ },
+ ],
+ lineNumber: 1,
+ offset: 54,
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-cyan term-bold',
+ text: 'Resolving secrets',
+ },
+ ],
+ lineNumber: 2,
+ offset: 91,
+ section: 'resolve-secrets',
+ section_duration: '00:00',
+ section_header: true,
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-cyan term-bold',
+ text: 'Preparing the "docker" executor',
+ },
+ ],
+ lineNumber: 4,
+ offset: 218,
+ section: 'prepare-executor',
+ section_duration: '00:01',
+ section_header: true,
+ },
+ {
+ content: [
+ {
+ text: 'Using Docker executor with image ruby:2.7 ...',
+ },
+ ],
+ lineNumber: 5,
+ offset: 317,
+ section: 'prepare-executor',
+ },
+ {
+ content: [
+ {
+ text: 'Pulling docker image ruby:2.7 ...',
+ },
+ ],
+ lineNumber: 6,
+ offset: 372,
+ section: 'prepare-executor',
+ },
+ {
+ content: [
+ {
+ text:
+ 'Using docker image sha256:55106bf6ba7f452c38d01ea760affc6ceb67d4b60068ffadab98d1b7b007668c for ruby:2.7 with digest ruby@sha256:23d08a4bae1a12ee3fce017f83204fcf9a02243443e4a516e65e5ff73810a449 ...',
+ },
+ ],
+ lineNumber: 7,
+ offset: 415,
+ section: 'prepare-executor',
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-cyan term-bold',
+ text: 'Preparing environment',
+ },
+ ],
+ lineNumber: 9,
+ offset: 665,
+ section: 'prepare-script',
+ section_duration: '00:01',
+ section_header: true,
+ },
+ {
+ content: [
+ {
+ text: 'Running on runner-ewm9wzgd-project-20-concurrent-0 via 8ea689ec6969...',
+ },
+ ],
+ lineNumber: 10,
+ offset: 752,
+ section: 'prepare-script',
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-cyan term-bold',
+ text: 'Getting source from Git repository',
+ },
+ ],
+ lineNumber: 12,
+ offset: 865,
+ section: 'get-sources',
+ section_duration: '00:01',
+ section_header: true,
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-green term-bold',
+ text: 'Fetching changes with git depth set to 20...',
+ },
+ ],
+ lineNumber: 13,
+ offset: 962,
+ section: 'get-sources',
+ },
+ {
+ content: [
+ {
+ text: 'Reinitialized existing Git repository in /builds/root/ci-project/.git/',
+ },
+ ],
+ lineNumber: 14,
+ offset: 1019,
+ section: 'get-sources',
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-green term-bold',
+ text: 'Checking out e0f63d76 as main...',
+ },
+ ],
+ lineNumber: 15,
+ offset: 1090,
+ section: 'get-sources',
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-green term-bold',
+ text: 'Skipping Git submodules setup',
+ },
+ ],
+ lineNumber: 16,
+ offset: 1136,
+ section: 'get-sources',
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-cyan term-bold',
+ text: 'Executing "step_script" stage of the job script',
+ },
+ ],
+ lineNumber: 18,
+ offset: 1217,
+ section: 'step-script',
+ section_duration: '00:00',
+ section_header: true,
+ },
+ {
+ content: [
+ {
+ text:
+ 'Using docker image sha256:55106bf6ba7f452c38d01ea760affc6ceb67d4b60068ffadab98d1b7b007668c for ruby:2.7 with digest ruby@sha256:23d08a4bae1a12ee3fce017f83204fcf9a02243443e4a516e65e5ff73810a449 ...',
+ },
+ ],
+ lineNumber: 19,
+ offset: 1327,
+ section: 'step-script',
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-green term-bold',
+ text: '$ echo "82.71"',
+ },
+ ],
+ lineNumber: 20,
+ offset: 1533,
+ section: 'step-script',
+ },
+ {
+ content: [
+ {
+ text: '82.71',
+ },
+ ],
+ lineNumber: 21,
+ offset: 1560,
+ section: 'step-script',
+ },
+ {
+ content: [
+ {
+ style: 'term-fg-l-green term-bold',
+ text: 'Job succeeded',
+ },
+ ],
+ lineNumber: 23,
+ offset: 1605,
+ },
+ ];
+
+ expect(compactJobLog(mockJobLog)).toStrictEqual(expectedResults);
+ });
+ });
+
+ describe('filterAnnotations', () => {
+ it('filters annotations by type', () => {
+ const data = [
+ {
+ name: 'b',
+ data: [
+ {
+ dummy: {},
+ },
+ {
+ external_link: {
+ label: 'URL 2',
+ url: 'https://url2.example.com/',
+ },
+ },
+ ],
+ },
+ {
+ name: 'a',
+ data: [
+ {
+ external_link: {
+ label: 'URL 1',
+ url: 'https://url1.example.com/',
+ },
+ },
+ ],
+ },
+ ];
+
+ expect(filterAnnotations(data, 'external_link')).toEqual([
+ {
+ label: 'URL 1',
+ url: 'https://url1.example.com/',
+ },
+ {
+ label: 'URL 2',
+ url: 'https://url2.example.com/',
+ },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/ci/jobs_mock_data.js
index 253e669e889..c428de3b9d8 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/ci/jobs_mock_data.js
@@ -989,6 +989,7 @@ export default {
},
erase_path: '/root/ci-mock/-/jobs/4757/erase',
artifacts: [null],
+ annotations: [],
runner: {
id: 1,
short_sha: 'ABCDEFGH',
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/actions_cell_spec.js
index f2d249b6014..1ffd680118e 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/actions_cell_spec.js
@@ -5,12 +5,12 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import ActionsCell from '~/jobs/components/table/cells/actions_cell.vue';
-import eventHub from '~/jobs/components/table/event_hub';
-import JobPlayMutation from '~/jobs/components/table/graphql/mutations/job_play.mutation.graphql';
-import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retry.mutation.graphql';
-import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql';
-import JobCancelMutation from '~/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql';
+import ActionsCell from '~/ci/jobs_page/components/job_cells/actions_cell.vue';
+import eventHub from '~/ci/jobs_page/event_hub';
+import JobPlayMutation from '~/ci/jobs_page/graphql/mutations/job_play.mutation.graphql';
+import JobRetryMutation from '~/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql';
+import JobUnscheduleMutation from '~/ci/jobs_page/graphql/mutations/job_unschedule.mutation.graphql';
+import JobCancelMutation from '~/ci/jobs_page/graphql/mutations/job_cancel.mutation.graphql';
import {
mockJobsNodes,
mockJobsNodesAsGuest,
@@ -18,7 +18,7 @@ import {
retryMutationResponse,
unscheduleMutationResponse,
cancelMutationResponse,
-} from '../../../mock_data';
+} from 'jest/ci/jobs_mock_data';
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/jobs/components/table/cells/duration_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js
index d015edb0e91..21f14ba0c98 100644
--- a/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DurationCell from '~/jobs/components/table/cells/duration_cell.vue';
+import DurationCell from '~/ci/jobs_page/components/job_cells/duration_cell.vue';
describe('Duration Cell', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/table/cells/job_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
index 73e37eed5f1..cb8f6ed8f9b 100644
--- a/spec/frontend/jobs/components/table/cells/job_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import JobCell from '~/jobs/components/table/cells/job_cell.vue';
-import { mockJobsNodes, mockJobsNodesAsGuest } from '../../../mock_data';
+import JobCell from '~/ci/jobs_page/components/job_cells/job_cell.vue';
+import { mockJobsNodes, mockJobsNodesAsGuest } from 'jest/ci/jobs_mock_data';
describe('Job Cell', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/pipeline_cell_spec.js
index 3d424b20964..6b212846897 100644
--- a/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/pipeline_cell_spec.js
@@ -2,7 +2,7 @@ import { GlAvatar } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import PipelineCell from '~/jobs/components/table/cells/pipeline_cell.vue';
+import PipelineCell from '~/ci/jobs_page/components/job_cells/pipeline_cell.vue';
const mockJobWithoutUser = {
id: 'gid://gitlab/Ci::Build/2264',
diff --git a/spec/frontend/jobs/components/table/jobs_table_empty_state_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js
index 05b066a9edc..f4893c4077f 100644
--- a/spec/frontend/jobs/components/table/jobs_table_empty_state_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js
@@ -1,6 +1,6 @@
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import JobsTableEmptyState from '~/jobs/components/table/jobs_table_empty_state.vue';
+import JobsTableEmptyState from '~/ci/jobs_page/components/jobs_table_empty_state.vue';
describe('Jobs table empty state', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
index 654b6d1c130..3adb95bf371 100644
--- a/spec/frontend/jobs/components/table/jobs_table_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
@@ -1,12 +1,12 @@
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import { DEFAULT_FIELDS_ADMIN } from '~/pages/admin/jobs/components/constants';
-import ProjectCell from '~/pages/admin/jobs/components/table/cell/project_cell.vue';
-import RunnerCell from '~/pages/admin/jobs/components/table/cells/runner_cell.vue';
-import { mockJobsNodes, mockAllJobsNodes } from '../../mock_data';
+import { DEFAULT_FIELDS_ADMIN } from '~/ci/admin/jobs_table/constants';
+import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
+import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
+import { mockJobsNodes, mockAllJobsNodes } from 'jest/ci/jobs_mock_data';
describe('Jobs Table', () => {
let wrapper;
@@ -62,6 +62,15 @@ describe('Jobs Table', () => {
});
expect(findAllCoverageJobs()).toHaveLength(jobsThatHaveCoverage.length);
});
+
+ describe('when stage of a job is missing', () => {
+ it('shows no stage', () => {
+ const stagelessJob = { ...mockJobsNodes[0], stage: null };
+ createComponent({ jobs: [stagelessJob] });
+
+ expect(findJobStage().exists()).toBe(false);
+ });
+ });
});
describe('regular user', () => {
diff --git a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_tabs_spec.js
index d20a732508a..c36f3841890 100644
--- a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_tabs_spec.js
@@ -2,8 +2,8 @@ import { GlTab } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue';
+import JobsTableTabs from '~/ci/jobs_page/components/jobs_table_tabs.vue';
+import CancelJobs from '~/ci/admin/jobs_table/components/cancel_jobs.vue';
describe('Jobs Table Tabs', () => {
let wrapper;
diff --git a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/ci/jobs_page/graphql/cache_config_spec.js
index e3b1ca1cce3..cfbd77f4154 100644
--- a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
+++ b/spec/frontend/ci/jobs_page/graphql/cache_config_spec.js
@@ -1,9 +1,9 @@
-import cacheConfig from '~/jobs/components/table/graphql/cache_config';
+import cacheConfig from '~/ci/jobs_page/graphql/cache_config';
import {
CIJobConnectionExistingCache,
CIJobConnectionIncomingCache,
CIJobConnectionIncomingCacheRunningStatus,
-} from '../../../mock_data';
+} from 'jest/ci/jobs_mock_data';
const firstLoadArgs = { first: 3, statuses: 'PENDING' };
const runningArgs = { first: 3, statuses: 'RUNNING' };
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/ci/jobs_page/job_page_app_spec.js
index 032b83ca22b..77443c9d490 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/ci/jobs_page/job_page_app_spec.js
@@ -7,20 +7,20 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import { createAlert } from '~/alert';
-import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.query.graphql';
-import getJobsCountQuery from '~/jobs/components/table/graphql/queries/get_jobs_count.query.graphql';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
-import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
-import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue';
+import getJobsQuery from '~/ci/jobs_page/graphql/queries/get_jobs.query.graphql';
+import getJobsCountQuery from '~/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import JobsTableApp from '~/ci/jobs_page/jobs_page_app.vue';
+import JobsTableTabs from '~/ci/jobs_page/components/jobs_table_tabs.vue';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
+import JobsSkeletonLoader from '~/ci/admin/jobs_table/components/jobs_skeleton_loader.vue';
import * as urlUtils from '~/lib/utils/url_utility';
import {
mockJobsResponsePaginated,
mockJobsResponseEmpty,
mockFailedSearchToken,
mockJobsCountResponse,
-} from '../../mock_data';
+} from 'jest/ci/jobs_mock_data';
const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo);
diff --git a/spec/frontend/ci/merge_requests/components/pipelines_table_wrapper_spec.js b/spec/frontend/ci/merge_requests/components/pipelines_table_wrapper_spec.js
new file mode 100644
index 00000000000..df9bf2a4235
--- /dev/null
+++ b/spec/frontend/ci/merge_requests/components/pipelines_table_wrapper_spec.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import { createAlert } from '~/alert';
+import PipelinesTableWrapper from '~/ci/merge_requests/components/pipelines_table_wrapper.vue';
+import getMergeRequestsPipelines from '~/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql';
+
+import { mergeRequestPipelinesResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+
+const pipelinesLength = mergeRequestPipelinesResponse.data.project.mergeRequest.pipelines.count;
+
+let wrapper;
+let mergeRequestPipelinesRequest;
+let apolloMock;
+
+const defaultProvide = {
+ graphqlPath: '/api/graphql/',
+ mergeRequestId: 1,
+ targetProjectFullPath: '/group/project',
+};
+
+const createComponent = () => {
+ const handlers = [[getMergeRequestsPipelines, mergeRequestPipelinesRequest]];
+
+ apolloMock = createMockApollo(handlers);
+
+ wrapper = shallowMount(PipelinesTableWrapper, {
+ apolloProvider: apolloMock,
+ provide: {
+ ...defaultProvide,
+ },
+ });
+
+ return waitForPromises();
+};
+
+const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+const findPipelineList = () => wrapper.findAll('li');
+
+beforeEach(() => {
+ mergeRequestPipelinesRequest = jest.fn();
+ mergeRequestPipelinesRequest.mockResolvedValue(mergeRequestPipelinesResponse);
+});
+afterEach(() => {
+ apolloMock = null;
+ createAlert.mockClear();
+});
+
+describe('PipelinesTableWrapper component', () => {
+ describe('When queries are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render the pipeline list', () => {
+ expect(findPipelineList()).toHaveLength(0);
+ });
+ });
+
+ describe('When there is an error fetching pipelines', () => {
+ beforeEach(async () => {
+ mergeRequestPipelinesRequest.mockRejectedValueOnce({ error: 'API error message' });
+ await createComponent();
+ });
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: "There was an error fetching this merge request's pipelines.",
+ });
+ });
+ });
+
+ describe('When queries have loaded', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('does not render the loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('renders a pipeline list', () => {
+ expect(findPipelineList()).toHaveLength(pipelinesLength);
+ });
+ });
+
+ describe('polling', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('polls every 10 seconds', () => {
+ expect(mergeRequestPipelinesRequest).toHaveBeenCalledTimes(1);
+
+ jest.advanceTimersByTime(5000);
+
+ expect(mergeRequestPipelinesRequest).toHaveBeenCalledTimes(1);
+
+ jest.advanceTimersByTime(5000);
+
+ expect(mergeRequestPipelinesRequest).toHaveBeenCalledTimes(2);
+ });
+ });
+});
diff --git a/spec/frontend/ci/merge_requests/mock_data.js b/spec/frontend/ci/merge_requests/mock_data.js
new file mode 100644
index 00000000000..1d8fdb88aa3
--- /dev/null
+++ b/spec/frontend/ci/merge_requests/mock_data.js
@@ -0,0 +1,30 @@
+const createMergeRequestPipelines = (count = 30) => {
+ const pipelines = [];
+
+ for (let i = 0; i < count; i += 1) {
+ pipelines.push({
+ id: i,
+ iid: i + 10,
+ path: `/project/pipelines/${i}`,
+ });
+ }
+
+ return {
+ count,
+ nodes: pipelines,
+ };
+};
+
+export const mergeRequestPipelinesResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: 'gid://gitlab/MergeRequest/1',
+ pipelines: createMergeRequestPipelines(),
+ },
+ },
+ },
+};
diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/ci/mixins/delayed_job_mixin_spec.js
index 098a63719fe..a1dab55bd07 100644
--- a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
+++ b/spec/frontend/ci/mixins/delayed_job_mixin_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import delayedJobFixture from 'test_fixtures/jobs/delayed.json';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
describe('DelayedJobMixin', () => {
let wrapper;
diff --git a/spec/frontend/ci/pipeline_details/dag/components/__snapshots__/dag_graph_spec.js.snap b/spec/frontend/ci/pipeline_details/dag/components/__snapshots__/dag_graph_spec.js.snap
new file mode 100644
index 00000000000..624c89a237c
--- /dev/null
+++ b/spec/frontend/ci/pipeline_details/dag/components/__snapshots__/dag_graph_spec.js.snap
@@ -0,0 +1,743 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`The DAG graph in the basic case renders the graph svg 1`] = `
+<svg
+ height="540"
+ viewBox="0,0,1000,540"
+ width="1000"
+>
+ <g
+ fill="none"
+ stroke-opacity="0.8"
+ >
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-0"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-1"
+ x1="116"
+ x2="361.3333333333333"
+ >
+ <stop
+ offset="0%"
+ stop-color="#e17223"
+ />
+ <stop
+ offset="100%"
+ stop-color="#83ab4a"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-2"
+ >
+ <path
+ d="
+ M100, 129
+ V158
+ H377.3333333333333
+ V100
+ H100
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip63)"
+ d="M108,129L190,129L190,129L369.3333333333333,129"
+ stroke="url(#dag-grad53)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-3"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-4"
+ x1="377.3333333333333"
+ x2="622.6666666666666"
+ >
+ <stop
+ offset="0%"
+ stop-color="#83ab4a"
+ />
+ <stop
+ offset="100%"
+ stop-color="#6f3500"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-5"
+ >
+ <path
+ d="
+ M361.3333333333333, 129.0000000000002
+ V158.0000000000002
+ H638.6666666666666
+ V100
+ H361.3333333333333
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip64)"
+ d="M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002"
+ stroke="url(#dag-grad54)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-6"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-7"
+ x1="116"
+ x2="622.6666666666666"
+ >
+ <stop
+ offset="0%"
+ stop-color="#5772ff"
+ />
+ <stop
+ offset="100%"
+ stop-color="#6f3500"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-8"
+ >
+ <path
+ d="
+ M100, 187.0000000000002
+ V241.00000000000003
+ H638.6666666666666
+ V158.0000000000002
+ H100
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip65)"
+ d="M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002"
+ stroke="url(#dag-grad55)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-9"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-10"
+ x1="116"
+ x2="361.3333333333333"
+ >
+ <stop
+ offset="0%"
+ stop-color="#b24800"
+ />
+ <stop
+ offset="100%"
+ stop-color="#006887"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-11"
+ >
+ <path
+ d="
+ M100, 269.9999999999998
+ V324
+ H377.3333333333333
+ V240.99999999999977
+ H100
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip66)"
+ d="M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998"
+ stroke="url(#dag-grad56)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-12"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-13"
+ x1="116"
+ x2="361.3333333333333"
+ >
+ <stop
+ offset="0%"
+ stop-color="#25d2d2"
+ />
+ <stop
+ offset="100%"
+ stop-color="#487900"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-14"
+ >
+ <path
+ d="
+ M100, 352.99999999999994
+ V407.00000000000006
+ H377.3333333333333
+ V323.99999999999994
+ H100
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip67)"
+ d="M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994"
+ stroke="url(#dag-grad57)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-15"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-16"
+ x1="377.3333333333333"
+ x2="622.6666666666666"
+ >
+ <stop
+ offset="0%"
+ stop-color="#006887"
+ />
+ <stop
+ offset="100%"
+ stop-color="#d84280"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-17"
+ >
+ <path
+ d="
+ M361.3333333333333, 270.0000000000001
+ V299.0000000000001
+ H638.6666666666666
+ V240.99999999999977
+ H361.3333333333333
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip68)"
+ d="M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001"
+ stroke="url(#dag-grad58)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-18"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-19"
+ x1="377.3333333333333"
+ x2="622.6666666666666"
+ >
+ <stop
+ offset="0%"
+ stop-color="#487900"
+ />
+ <stop
+ offset="100%"
+ stop-color="#d84280"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-20"
+ >
+ <path
+ d="
+ M361.3333333333333, 328.0000000000001
+ V381.99999999999994
+ H638.6666666666666
+ V299.0000000000001
+ H361.3333333333333
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip69)"
+ d="M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001"
+ stroke="url(#dag-grad59)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-21"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-22"
+ x1="377.3333333333333"
+ x2="622.6666666666666"
+ >
+ <stop
+ offset="0%"
+ stop-color="#487900"
+ />
+ <stop
+ offset="100%"
+ stop-color="#3547de"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-23"
+ >
+ <path
+ d="
+ M361.3333333333333, 411
+ V440
+ H638.6666666666666
+ V381.99999999999994
+ H361.3333333333333
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip70)"
+ d="M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411"
+ stroke="url(#dag-grad60)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-24"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-25"
+ x1="638.6666666666666"
+ x2="884"
+ >
+ <stop
+ offset="0%"
+ stop-color="#d84280"
+ />
+ <stop
+ offset="100%"
+ stop-color="#006887"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-26"
+ >
+ <path
+ d="
+ M622.6666666666666, 270.1890725105691
+ V299.1890725105691
+ H900
+ V241.0000000000001
+ H622.6666666666666
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip71)"
+ d="M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691"
+ stroke="url(#dag-grad61)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ <g
+ class="dag-link gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke-opacity gl-transition-timing-function-ease"
+ id="reference-27"
+ >
+ <lineargradient
+ gradientUnits="userSpaceOnUse"
+ id="reference-28"
+ x1="638.6666666666666"
+ x2="884"
+ >
+ <stop
+ offset="0%"
+ stop-color="#3547de"
+ />
+ <stop
+ offset="100%"
+ stop-color="#275600"
+ />
+ </lineargradient>
+ <clippath
+ id="reference-29"
+ >
+ <path
+ d="
+ M622.6666666666666, 411
+ V440
+ H900
+ V382
+ H622.6666666666666
+ Z
+ "
+ />
+ </clippath>
+ <path
+ clip-path="url(#dag-clip72)"
+ d="M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411"
+ stroke="url(#dag-grad62)"
+ stroke-width="56"
+ style="stroke-linejoin: round;"
+ />
+ </g>
+ </g>
+ <g>
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-30"
+ stroke="#e17223"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="108"
+ x2="108"
+ y1="104"
+ y2="154.00000000000003"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-31"
+ stroke="#83ab4a"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="369"
+ x2="369"
+ y1="104"
+ y2="154"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-32"
+ stroke="#5772ff"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="108"
+ x2="108"
+ y1="187.00000000000003"
+ y2="237.00000000000003"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-33"
+ stroke="#b24800"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="108"
+ x2="108"
+ y1="270"
+ y2="320.00000000000006"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-34"
+ stroke="#25d2d2"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="108"
+ x2="108"
+ y1="353.00000000000006"
+ y2="403.0000000000001"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-35"
+ stroke="#6f3500"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="630"
+ x2="630"
+ y1="104.0000000000002"
+ y2="212.00000000000009"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-36"
+ stroke="#006887"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="369"
+ x2="369"
+ y1="244.99999999999977"
+ y2="294.99999999999994"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-37"
+ stroke="#487900"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="369"
+ x2="369"
+ y1="327.99999999999994"
+ y2="436"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-38"
+ stroke="#d84280"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="630"
+ x2="630"
+ y1="245.00000000000009"
+ y2="353"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-39"
+ stroke="#3547de"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="630"
+ x2="630"
+ y1="386"
+ y2="436"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-40"
+ stroke="#006887"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="892"
+ x2="892"
+ y1="245.18907251056908"
+ y2="295.1890725105691"
+ />
+ <line
+ class="dag-node gl-cursor-pointer gl-transition-duration-slow gl-transition-property-stroke gl-transition-timing-function-ease"
+ id="reference-41"
+ stroke="#275600"
+ stroke-linecap="round"
+ stroke-width="16"
+ x1="892"
+ x2="892"
+ y1="386"
+ y2="436"
+ />
+ </g>
+ <g
+ class="gl-font-sm"
+ >
+ <foreignobject
+ class="gl-overflow-visible"
+ height="58.00000000000003px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="8"
+ y="100"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 58.00000000000003px; text-align: right;"
+ >
+ build_a
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="25px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="369.3333333333333"
+ y="75"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 25px; text-align: left;"
+ >
+ test_a
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="58px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="8"
+ y="183.00000000000003"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 58px; text-align: right;"
+ >
+ test_b
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="58.00000000000006px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="8"
+ y="266"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 58.00000000000006px; text-align: right;"
+ >
+ post_test_a
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="58.00000000000006px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="8"
+ y="349.00000000000006"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 58.00000000000006px; text-align: right;"
+ >
+ post_test_b
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="25px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="630.6666666666666"
+ y="75.0000000000002"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 25px; text-align: right;"
+ >
+ post_test_c
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="25px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="369.3333333333333"
+ y="215.99999999999977"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 25px; text-align: left;"
+ >
+ staging_a
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="25px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="369.3333333333333"
+ y="298.99999999999994"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 25px; text-align: left;"
+ >
+ staging_b
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="25px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="630.6666666666666"
+ y="216.00000000000009"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 25px; text-align: right;"
+ >
+ canary_a
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="25px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="630.6666666666666"
+ y="357"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 25px; text-align: right;"
+ >
+ canary_c
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="58px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="908"
+ y="241.18907251056908"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 58px; text-align: left;"
+ >
+ production_a
+ </div>
+ </foreignobject>
+ <foreignobject
+ class="gl-overflow-visible"
+ height="58px"
+ requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
+ width="84"
+ x="908"
+ y="382"
+ >
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break gl-pointer-events-none"
+ style="height: 58px; text-align: left;"
+ >
+ production_d
+ </div>
+ </foreignobject>
+ </g>
+</svg>
+`;
diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/ci/pipeline_details/dag/components/dag_annotations_spec.js
index 124f02bcec7..d1c338e50c6 100644
--- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
+++ b/spec/frontend/ci/pipeline_details/dag/components/dag_annotations_spec.js
@@ -1,8 +1,8 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
-import { singleNote, multiNote } from './mock_data';
+import DagAnnotations from '~/ci/pipeline_details/dag/components/dag_annotations.vue';
+import { singleNote, multiNote } from '../mock_data';
describe('The DAG annotations', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/ci/pipeline_details/dag/components/dag_graph_spec.js
index 6b46be3dd49..aff83c00e79 100644
--- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js
+++ b/spec/frontend/ci/pipeline_details/dag/components/dag_graph_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants';
-import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
-import { createSankey } from '~/pipelines/components/dag/drawing_utils';
-import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions';
-import { removeOrphanNodes } from '~/pipelines/components/parsing_utils';
-import { parsedData } from './mock_data';
+import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/ci/pipeline_details/dag/constants';
+import DagGraph from '~/ci/pipeline_details/dag/components/dag_graph.vue';
+import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
+import { highlightIn, highlightOut } from '~/ci/pipeline_details/dag/utils/interactions';
+import { removeOrphanNodes } from '~/ci/pipeline_details/utils/parsing_utils';
+import { parsedData } from '../mock_data';
describe('The DAG graph', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/ci/pipeline_details/dag/dag_spec.js
index 53719065611..de9490be607 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/ci/pipeline_details/dag/dag_spec.js
@@ -1,12 +1,12 @@
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/pipelines/components/dag/constants';
-import Dag from '~/pipelines/components/dag/dag.vue';
-import DagAnnotations from '~/pipelines/components/dag/dag_annotations.vue';
-import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
+import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/ci/pipeline_details/dag/constants';
+import Dag from '~/ci/pipeline_details/dag/dag.vue';
+import DagAnnotations from '~/ci/pipeline_details/dag/components/dag_annotations.vue';
+import DagGraph from '~/ci/pipeline_details/dag/components/dag_graph.vue';
-import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/pipelines/constants';
+import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/ci/pipeline_details/constants';
import {
mockParsedGraphQLNodes,
tooSmallGraph,
diff --git a/spec/frontend/pipelines/components/dag/mock_data.js b/spec/frontend/ci/pipeline_details/dag/mock_data.js
index f27e7cf3d6b..f27e7cf3d6b 100644
--- a/spec/frontend/pipelines/components/dag/mock_data.js
+++ b/spec/frontend/ci/pipeline_details/dag/mock_data.js
diff --git a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js b/spec/frontend/ci/pipeline_details/dag/utils/drawing_utils_spec.js
index 095ded01298..aea8e894bd4 100644
--- a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
+++ b/spec/frontend/ci/pipeline_details/dag/utils/drawing_utils_spec.js
@@ -1,6 +1,6 @@
-import { createSankey } from '~/pipelines/components/dag/drawing_utils';
-import { parseData } from '~/pipelines/components/parsing_utils';
-import { mockParsedGraphQLNodes } from './mock_data';
+import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
+import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
+import { mockParsedGraphQLNodes } from '../mock_data';
describe('DAG visualization drawing utilities', () => {
const parsed = parseData(mockParsedGraphQLNodes);
diff --git a/spec/frontend/ci/pipeline_details/graph/components/__snapshots__/links_inner_spec.js.snap b/spec/frontend/ci/pipeline_details/graph/components/__snapshots__/links_inner_spec.js.snap
new file mode 100644
index 00000000000..b31c0e59a33
--- /dev/null
+++ b/spec/frontend/ci/pipeline_details/graph/components/__snapshots__/links_inner_spec.js.snap
@@ -0,0 +1,110 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Links Inner component with a large number of needs matches snapshot and has expected path 1`] = `
+<div
+ class="gl-display-flex gl-relative"
+ totalgroups="10"
+>
+ <svg
+ class="gl-absolute gl-pointer-events-none"
+ height="445px"
+ id="reference-0"
+ viewBox="0,0,1019,445"
+ width="1019px"
+ >
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M202,118C52,118,52,138,102,138"
+ stroke-width="2"
+ />
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M202,118C62,118,62,148,112,148"
+ stroke-width="2"
+ />
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M222,138C72,138,72,158,122,158"
+ stroke-width="2"
+ />
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M212,128C82,128,82,168,132,168"
+ stroke-width="2"
+ />
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M232,148C92,148,92,178,142,178"
+ stroke-width="2"
+ />
+ </svg>
+</div>
+`;
+
+exports[`Links Inner component with a parallel need matches snapshot and has expected path 1`] = `
+<div
+ class="gl-display-flex gl-relative"
+ totalgroups="10"
+>
+ <svg
+ class="gl-absolute gl-pointer-events-none"
+ height="445px"
+ id="reference-0"
+ viewBox="0,0,1019,445"
+ width="1019px"
+ >
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M192,108C32,108,32,118,82,118"
+ stroke-width="2"
+ />
+ </svg>
+</div>
+`;
+
+exports[`Links Inner component with one need matches snapshot and has expected path 1`] = `
+<div
+ class="gl-display-flex gl-relative"
+ totalgroups="10"
+>
+ <svg
+ class="gl-absolute gl-pointer-events-none"
+ height="445px"
+ id="reference-0"
+ viewBox="0,0,1019,445"
+ width="1019px"
+ >
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M202,118C52,118,52,138,102,138"
+ stroke-width="2"
+ />
+ </svg>
+</div>
+`;
+
+exports[`Links Inner component with same stage needs matches snapshot and has expected path 1`] = `
+<div
+ class="gl-display-flex gl-relative"
+ totalgroups="10"
+>
+ <svg
+ class="gl-absolute gl-pointer-events-none"
+ height="445px"
+ id="reference-0"
+ viewBox="0,0,1019,445"
+ width="1019px"
+ >
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M192,108C32,108,32,118,82,118"
+ stroke-width="2"
+ />
+ <path
+ class="gl-fill-transparent gl-stroke-gray-200 gl-transition-duration-slow gl-transition-timing-function-ease"
+ d="M202,118C42,118,42,128,92,128"
+ stroke-width="2"
+ />
+ </svg>
+</div>
+`;
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/action_component_spec.js
index 890255f225e..9e177156d0e 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/action_component_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue';
+import ActionComponent from '~/ci/common/private/job_action_component.vue';
describe('pipeline graph action component', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
index e9bce037800..a98e79c69fe 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
@@ -1,15 +1,15 @@
import { shallowMount } from '@vue/test-utils';
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
-import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
-import JobItem from '~/pipelines/components/graph/job_item.vue';
-import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
-import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
-import { calculatePipelineLayersInfo } from '~/pipelines/components/graph/utils';
-import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
-
-import { generateResponse, pipelineWithUpstreamDownstream } from './mock_data';
+import { LAYER_VIEW, STAGE_VIEW } from '~/ci/pipeline_details/graph/constants';
+import PipelineGraph from '~/ci/pipeline_details/graph/components/graph_component.vue';
+import JobItem from '~/ci/pipeline_details/graph/components/job_item.vue';
+import LinkedPipelinesColumn from '~/ci/pipeline_details/graph/components/linked_pipelines_column.vue';
+import StageColumnComponent from '~/ci/pipeline_details/graph/components/stage_column_component.vue';
+import { calculatePipelineLayersInfo } from '~/ci/pipeline_details/graph/utils';
+import LinksLayer from '~/ci/common/private/job_links_layer.vue';
+
+import { generateResponse, pipelineWithUpstreamDownstream } from '../mock_data';
describe('graph component', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/ci/pipeline_details/graph/components/graph_view_selector_spec.js
index 65ae9d19978..bf98995de9c 100644
--- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/graph_view_selector_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
-import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
+import { LAYER_VIEW, STAGE_VIEW } from '~/ci/pipeline_details/graph/constants';
+import GraphViewSelector from '~/ci/pipeline_details/graph/components/graph_view_selector.vue';
describe('the graph view selector component', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_group_dropdown_spec.js
index 1419a7b9982..d5a1cfffe68 100644
--- a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_group_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
-import JobGroupDropdown from '~/pipelines/components/graph/job_group_dropdown.vue';
+import JobGroupDropdown from '~/ci/pipeline_details/graph/components/job_group_dropdown.vue';
describe('job group dropdown component', () => {
const group = {
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
index 8a8b0e9aa63..107f0df5c02 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import { GlBadge, GlModal, GlToast } from '@gitlab/ui';
-import JobItem from '~/pipelines/components/graph/job_item.vue';
+import JobItem from '~/ci/pipeline_details/graph/components/job_item.vue';
import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue';
+import ActionComponent from '~/ci/common/private/job_action_component.vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
@@ -15,7 +15,7 @@ import {
mockFailedJob,
triggerJob,
triggerJobWithRetryAction,
-} from './mock_data';
+} from '../mock_data';
describe('pipeline graph job item', () => {
useLocalStorageSpy();
diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js
index fca4c43d9fa..ca201aee648 100644
--- a/spec/frontend/pipelines/graph/job_name_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_name_component_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import jobNameComponent from '~/pipelines/components/jobs_shared/job_name_component.vue';
+import jobNameComponent from '~/ci/common/private/job_name_component.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
describe('job name component', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
index 8dae2aac664..5541b0db54a 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
@@ -6,10 +6,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
-import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
-import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
-import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
+import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/ci/pipeline_details/graph/constants';
+import LinkedPipelineComponent from '~/ci/pipeline_details/graph/components/linked_pipeline.vue';
+import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
+import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import mockPipeline from './linked_pipelines_mock_data';
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/ci/pipeline_details/graph/components/linked_pipelines_column_spec.js
index bcea140f2dd..30f05baceab 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/linked_pipelines_column_spec.js
@@ -10,14 +10,14 @@ import {
UPSTREAM,
LAYER_VIEW,
STAGE_VIEW,
-} from '~/pipelines/components/graph/constants';
-import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
-import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue';
-import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
-import * as parsingUtils from '~/pipelines/components/parsing_utils';
-import { LOAD_FAILURE } from '~/pipelines/constants';
-
-import { pipelineWithUpstreamDownstream, wrappedPipelineReturn } from './mock_data';
+} from '~/ci/pipeline_details/graph/constants';
+import PipelineGraph from '~/ci/pipeline_details/graph/components/graph_component.vue';
+import LinkedPipeline from '~/ci/pipeline_details/graph/components/linked_pipeline.vue';
+import LinkedPipelinesColumn from '~/ci/pipeline_details/graph/components/linked_pipelines_column.vue';
+import * as parsingUtils from '~/ci/pipeline_details/utils/parsing_utils';
+import { LOAD_FAILURE } from '~/ci/pipeline_details/constants';
+
+import { pipelineWithUpstreamDownstream, wrappedPipelineReturn } from '../mock_data';
const processedPipeline = pipelineWithUpstreamDownstream(mockPipelineResponse);
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/ci/pipeline_details/graph/components/linked_pipelines_mock_data.js
index f7f5738e46d..f7f5738e46d 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/linked_pipelines_mock_data.js
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/ci/pipeline_details/graph/components/links_inner_spec.js
index b4ffd2658fe..655b2ac74ac 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/links_inner_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
-import { parseData } from '~/pipelines/components/parsing_utils';
-import { createJobsHash } from '~/pipelines/utils';
+import LinksInner from '~/ci/pipeline_details/graph/components/links_inner.vue';
+import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
+import { createJobsHash } from '~/ci/pipeline_details/utils';
import {
jobRect,
largePipelineData,
@@ -11,7 +11,7 @@ import {
pipelineDataWithNoNeeds,
rootRect,
sameStageNeeds,
-} from '../pipeline_graph/mock_data';
+} from 'jest/ci/pipeline_editor/components/graph/mock_data';
describe('Links Inner component', () => {
const containerId = 'pipeline-graph-container';
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/stage_column_component_spec.js
index d4d7f1618c5..cc79205ec41 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/stage_column_component_spec.js
@@ -1,7 +1,7 @@
import { mount, shallowMount } from '@vue/test-utils';
-import JobItem from '~/pipelines/components/graph/job_item.vue';
-import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
-import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue';
+import JobItem from '~/ci/pipeline_details/graph/components/job_item.vue';
+import StageColumnComponent from '~/ci/pipeline_details/graph/components/stage_column_component.vue';
+import ActionComponent from '~/ci/common/private/job_action_component.vue';
const mockJob = {
id: 4250,
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/ci/pipeline_details/graph/graph_component_wrapper_spec.js
index 7b59d82ae6f..372ed2a4e1c 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/graph_component_wrapper_spec.js
@@ -23,15 +23,15 @@ import {
LAYER_VIEW,
STAGE_VIEW,
VIEW_TYPE_KEY,
-} from '~/pipelines/components/graph/constants';
-import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
-import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
-import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue';
-import * as Api from '~/pipelines/components/graph_shared/api';
-import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
-import * as parsingUtils from '~/pipelines/components/parsing_utils';
-import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
-import * as sentryUtils from '~/pipelines/utils';
+} from '~/ci/pipeline_details/graph/constants';
+import PipelineGraph from '~/ci/pipeline_details/graph/components/graph_component.vue';
+import PipelineGraphWrapper from '~/ci/pipeline_details/graph/graph_component_wrapper.vue';
+import GraphViewSelector from '~/ci/pipeline_details/graph/components/graph_view_selector.vue';
+import * as Api from '~/ci/pipeline_details/graph/api_utils';
+import LinksLayer from '~/ci/common/private/job_links_layer.vue';
+import * as parsingUtils from '~/ci/pipeline_details/utils/parsing_utils';
+import getPipelineHeaderData from '~/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql';
+import * as sentryUtils from '~/ci/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data';
import {
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/ci/pipeline_details/graph/mock_data.js
index 8d06d6931ed..a880a9cf4b0 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/ci/pipeline_details/graph/mock_data.js
@@ -1,10 +1,6 @@
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
-import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
-import {
- BUILD_KIND,
- BRIDGE_KIND,
- RETRY_ACTION_TITLE,
-} from '~/pipelines/components/graph/constants';
+import { unwrapPipelineData } from '~/ci/pipeline_details/graph/utils';
+import { BUILD_KIND, BRIDGE_KIND, RETRY_ACTION_TITLE } from '~/ci/pipeline_details/graph/constants';
// We mock this instead of using fixtures for performance reason.
const mockPipelineResponseCopy = JSON.parse(JSON.stringify(mockPipelineResponse));
diff --git a/spec/frontend/pipelines/pipeline_details_header_spec.js b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
index 5c75020afad..6e13658a773 100644
--- a/spec/frontend/pipelines/pipeline_details_header_spec.js
+++ b/spec/frontend/ci/pipeline_details/header/pipeline_details_header_spec.js
@@ -5,13 +5,13 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import PipelineDetailsHeader from '~/pipelines/components/pipeline_details_header.vue';
-import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants';
+import PipelineDetailsHeader from '~/ci/pipeline_details/header/pipeline_details_header.vue';
+import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/ci/constants';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
-import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
-import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import getPipelineDetailsQuery from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql';
+import cancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
+import deletePipelineMutation from '~/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql';
+import retryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
+import getPipelineDetailsQuery from '~/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql';
import {
pipelineHeaderSuccess,
pipelineHeaderRunning,
@@ -23,7 +23,7 @@ import {
pipelineRetryMutationResponseFailed,
pipelineCancelMutationResponseFailed,
pipelineDeleteMutationResponseFailed,
-} from './mock_data';
+} from '../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js b/spec/frontend/ci/pipeline_details/jobs/components/failed_jobs_table_spec.js
index 99a178120cc..7110a35ad4e 100644
--- a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
+++ b/spec/frontend/ci/pipeline_details/jobs/components/failed_jobs_table_spec.js
@@ -7,9 +7,9 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
-import RetryFailedJobMutation from '~/pipelines/graphql/mutations/retry_failed_job.mutation.graphql';
-import { TRACKING_CATEGORIES } from '~/pipelines/constants';
+import FailedJobsTable from '~/ci/pipeline_details/jobs/components/failed_jobs_table.vue';
+import RetryFailedJobMutation from '~/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
import {
successRetryMutationResponse,
failedRetryMutationResponse,
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js b/spec/frontend/ci/pipeline_details/jobs/failed_jobs_app_spec.js
index 6a2453704db..17b43aa422b 100644
--- a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
+++ b/spec/frontend/ci/pipeline_details/jobs/failed_jobs_app_spec.js
@@ -5,10 +5,10 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import FailedJobsApp from '~/pipelines/components/jobs/failed_jobs_app.vue';
-import FailedJobsTable from '~/pipelines/components/jobs/failed_jobs_table.vue';
-import GetFailedJobsQuery from '~/pipelines/graphql/queries/get_failed_jobs.query.graphql';
-import { mockFailedJobsQueryResponse } from '../../mock_data';
+import FailedJobsApp from '~/ci/pipeline_details/jobs/failed_jobs_app.vue';
+import FailedJobsTable from '~/ci/pipeline_details/jobs/components/failed_jobs_table.vue';
+import GetFailedJobsQuery from '~/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql';
+import { mockFailedJobsQueryResponse } from 'jest/ci/pipeline_details/mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/ci/pipeline_details/jobs/jobs_app_spec.js
index 39475788fe2..4a3a901502e 100644
--- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
+++ b/spec/frontend/ci/pipeline_details/jobs/jobs_app_spec.js
@@ -5,10 +5,10 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import getPipelineJobsQuery from '~/pipelines/graphql/queries/get_pipeline_jobs.query.graphql';
-import { mockPipelineJobsQueryResponse } from '../../mock_data';
+import JobsApp from '~/ci/pipeline_details/jobs/jobs_app.vue';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import getPipelineJobsQuery from '~/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql';
+import { mockPipelineJobsQueryResponse } from '../mock_data';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipelines/linked_pipelines_mock.json b/spec/frontend/ci/pipeline_details/linked_pipelines_mock.json
index a68283032d2..a68283032d2 100644
--- a/spec/frontend/pipelines/linked_pipelines_mock.json
+++ b/spec/frontend/ci/pipeline_details/linked_pipelines_mock.json
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/ci/pipeline_details/mock_data.js
index 673db3b5178..e32d0a0df47 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/ci/pipeline_details/mock_data.js
@@ -197,108 +197,6 @@ export const mockRunningPipelineHeaderData = {
},
};
-export const stageReply = {
- name: 'deploy',
- title: 'deploy: running',
- latest_statuses: [
- {
- id: 928,
- name: 'stop staging',
- started: false,
- build_path: '/twitter/flight/-/jobs/928',
- cancel_path: '/twitter/flight/-/jobs/928/cancel',
- playable: false,
- created_at: '2018-04-04T20:02:02.728Z',
- updated_at: '2018-04-04T20:02:02.766Z',
- status: {
- icon: 'status_pending',
- text: 'pending',
- label: 'pending',
- group: 'pending',
- tooltip: 'pending',
- has_details: true,
- details_path: '/twitter/flight/-/jobs/928',
- favicon:
- '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico',
- action: {
- icon: 'cancel',
- title: 'Cancel',
- path: '/twitter/flight/-/jobs/928/cancel',
- method: 'post',
- },
- },
- },
- {
- id: 926,
- name: 'production',
- started: false,
- build_path: '/twitter/flight/-/jobs/926',
- retry_path: '/twitter/flight/-/jobs/926/retry',
- play_path: '/twitter/flight/-/jobs/926/play',
- playable: true,
- created_at: '2018-04-04T20:00:57.202Z',
- updated_at: '2018-04-04T20:11:13.110Z',
- status: {
- icon: 'status_canceled',
- text: 'canceled',
- label: 'manual play action',
- group: 'canceled',
- tooltip: 'canceled',
- has_details: true,
- details_path: '/twitter/flight/-/jobs/926',
- favicon:
- '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/twitter/flight/-/jobs/926/play',
- method: 'post',
- },
- },
- },
- {
- id: 217,
- name: 'staging',
- started: '2018-03-07T08:41:46.234Z',
- build_path: '/twitter/flight/-/jobs/217',
- retry_path: '/twitter/flight/-/jobs/217/retry',
- playable: false,
- created_at: '2018-03-07T14:41:58.093Z',
- updated_at: '2018-03-07T14:41:58.093Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/twitter/flight/-/jobs/217',
- favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/twitter/flight/-/jobs/217/retry',
- method: 'post',
- },
- },
- },
- ],
- status: {
- icon: 'status_running',
- text: 'running',
- label: 'running',
- group: 'running',
- tooltip: 'running',
- has_details: true,
- details_path: '/twitter/flight/pipelines/13#deploy',
- favicon:
- '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
- },
- path: '/twitter/flight/pipelines/13#deploy',
- dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy',
-};
-
export const users = [
{
id: 1,
diff --git a/spec/frontend/pipelines/pipeline_tabs_spec.js b/spec/frontend/ci/pipeline_details/pipeline_tabs_spec.js
index 8d1cd98e981..8d67cdef05c 100644
--- a/spec/frontend/pipelines/pipeline_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_details/pipeline_tabs_spec.js
@@ -1,4 +1,4 @@
-import { createAppOptions } from '~/pipelines/pipeline_tabs';
+import { createAppOptions } from '~/ci/pipeline_details/pipeline_tabs';
jest.mock('~/lib/utils/url_utility', () => ({
removeParams: () => 'gitlab.com',
@@ -6,11 +6,11 @@ jest.mock('~/lib/utils/url_utility', () => ({
setUrlFragment: () => {},
}));
-jest.mock('~/pipelines/utils', () => ({
+jest.mock('~/ci/pipeline_details/utils', () => ({
getPipelineDefaultTab: () => '',
}));
-describe('~/pipelines/pipeline_tabs.js', () => {
+describe('~/ci/pipeline_details/pipeline_tabs.js', () => {
describe('createAppOptions', () => {
const SELECTOR = 'SELECTOR';
diff --git a/spec/frontend/pipelines/pipelines_store_spec.js b/spec/frontend/ci/pipeline_details/pipelines_store_spec.js
index f374ecd0c0a..43e605f4306 100644
--- a/spec/frontend/pipelines/pipelines_store_spec.js
+++ b/spec/frontend/ci/pipeline_details/pipelines_store_spec.js
@@ -1,4 +1,4 @@
-import PipelineStore from '~/pipelines/stores/pipelines_store';
+import PipelineStore from '~/ci/pipeline_details/stores/pipelines_store';
describe('Pipelines Store', () => {
let store;
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/ci/pipeline_details/tabs/pipeline_tabs_spec.js
index 0951e1ffb46..0f1835b7ec8 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_details/tabs/pipeline_tabs_spec.js
@@ -1,8 +1,8 @@
import { GlTab } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue';
-import { TRACKING_CATEGORIES } from '~/pipelines/constants';
+import PipelineTabs from '~/ci/pipeline_details/tabs/pipeline_tabs.vue';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
describe('The Pipeline Tabs', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/test_reports/empty_state_spec.js b/spec/frontend/ci/pipeline_details/test_reports/empty_state_spec.js
index ee0f8a90a11..ed1d6bc7d37 100644
--- a/spec/frontend/pipelines/test_reports/empty_state_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/empty_state_spec.js
@@ -1,6 +1,6 @@
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import EmptyState, { i18n } from '~/pipelines/components/test_reports/empty_state.vue';
+import EmptyState, { i18n } from '~/ci/pipeline_details/test_reports/empty_state.vue';
describe('Test report empty state', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/test_reports/mock_data.js b/spec/frontend/ci/pipeline_details/test_reports/mock_data.js
index c3ca1429842..7c9f9287c86 100644
--- a/spec/frontend/pipelines/test_reports/mock_data.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/mock_data.js
@@ -1,4 +1,4 @@
-import { TestStatus } from '~/pipelines/constants';
+import { TestStatus } from '~/ci/pipeline_details/constants';
export default [
{
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/ci/pipeline_details/test_reports/stores/actions_spec.js
index e05d2151f0a..6636a7f1ed6 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/stores/actions_spec.js
@@ -5,8 +5,8 @@ import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import * as actions from '~/pipelines/stores/test_reports/actions';
-import * as types from '~/pipelines/stores/test_reports/mutation_types';
+import * as actions from '~/ci/pipeline_details/stores/test_reports/actions';
+import * as types from '~/ci/pipeline_details/stores/test_reports/mutation_types';
jest.mock('~/alert');
diff --git a/spec/frontend/pipelines/test_reports/stores/getters_spec.js b/spec/frontend/ci/pipeline_details/test_reports/stores/getters_spec.js
index 70e3a01dbf1..e52e9a07ae0 100644
--- a/spec/frontend/pipelines/test_reports/stores/getters_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/stores/getters_spec.js
@@ -1,10 +1,10 @@
import testReports from 'test_fixtures/pipelines/test_report.json';
-import * as getters from '~/pipelines/stores/test_reports/getters';
+import * as getters from '~/ci/pipeline_details/stores/test_reports/getters';
import {
iconForTestStatus,
formatFilePath,
formattedTime,
-} from '~/pipelines/stores/test_reports/utils';
+} from '~/ci/pipeline_details/stores/test_reports/utils';
describe('Getters TestReports Store', () => {
let state;
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/ci/pipeline_details/test_reports/stores/mutations_spec.js
index 685ac6ea3e5..d58515dcc6d 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/stores/mutations_spec.js
@@ -1,6 +1,6 @@
import testReports from 'test_fixtures/pipelines/test_report.json';
-import * as types from '~/pipelines/stores/test_reports/mutation_types';
-import mutations from '~/pipelines/stores/test_reports/mutations';
+import * as types from '~/ci/pipeline_details/stores/test_reports/mutation_types';
+import mutations from '~/ci/pipeline_details/stores/test_reports/mutations';
import { createAlert } from '~/alert';
jest.mock('~/alert');
diff --git a/spec/frontend/pipelines/test_reports/stores/utils_spec.js b/spec/frontend/ci/pipeline_details/test_reports/stores/utils_spec.js
index 703fe69026c..c0ffc2b34fb 100644
--- a/spec/frontend/pipelines/test_reports/stores/utils_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/stores/utils_spec.js
@@ -1,4 +1,4 @@
-import { formatFilePath, formattedTime } from '~/pipelines/stores/test_reports/utils';
+import { formatFilePath, formattedTime } from '~/ci/pipeline_details/stores/test_reports/utils';
describe('Test reports utils', () => {
describe('formatFilePath', () => {
diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/ci/pipeline_details/test_reports/test_case_details_spec.js
index f8663408817..0f651b9d456 100644
--- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/test_case_details_spec.js
@@ -1,7 +1,7 @@
import { GlModal, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
+import TestCaseDetails from '~/ci/pipeline_details/test_reports/test_case_details.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/ci/pipeline_details/test_reports/test_reports_spec.js
index de16f496eff..8ff060026da 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/test_reports_spec.js
@@ -5,11 +5,11 @@ import Vue from 'vue';
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import EmptyState from '~/pipelines/components/test_reports/empty_state.vue';
-import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
-import TestSummary from '~/pipelines/components/test_reports/test_summary.vue';
-import TestSummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
-import * as getters from '~/pipelines/stores/test_reports/getters';
+import EmptyState from '~/ci/pipeline_details/test_reports/empty_state.vue';
+import TestReports from '~/ci/pipeline_details/test_reports/test_reports.vue';
+import TestSummary from '~/ci/pipeline_details/test_reports/test_summary.vue';
+import TestSummaryTable from '~/ci/pipeline_details/test_reports/test_summary_table.vue';
+import * as getters from '~/ci/pipeline_details/stores/test_reports/getters';
Vue.use(Vuex);
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/ci/pipeline_details/test_reports/test_suite_table_spec.js
index 08b430fa703..5bdea6bbcbf 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/test_suite_table_spec.js
@@ -4,11 +4,11 @@ import Vue from 'vue';
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import SuiteTable, { i18n } from '~/pipelines/components/test_reports/test_suite_table.vue';
-import { TestStatus } from '~/pipelines/constants';
-import * as getters from '~/pipelines/stores/test_reports/getters';
-import { formatFilePath } from '~/pipelines/stores/test_reports/utils';
-import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from '~/pipelines/stores/test_reports/constants';
+import SuiteTable, { i18n } from '~/ci/pipeline_details/test_reports/test_suite_table.vue';
+import { TestStatus } from '~/ci/pipeline_details/constants';
+import * as getters from '~/ci/pipeline_details/stores/test_reports/getters';
+import { formatFilePath } from '~/ci/pipeline_details/stores/test_reports/utils';
+import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from '~/ci/pipeline_details/stores/test_reports/constants';
import skippedTestCases from './mock_data';
Vue.use(Vuex);
diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/ci/pipeline_details/test_reports/test_summary_spec.js
index 7eed6671fb9..f9182d52c8a 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/test_summary_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import testReports from 'test_fixtures/pipelines/test_report.json';
-import Summary from '~/pipelines/components/test_reports/test_summary.vue';
-import { formattedTime } from '~/pipelines/stores/test_reports/utils';
+import Summary from '~/ci/pipeline_details/test_reports/test_summary.vue';
+import { formattedTime } from '~/ci/pipeline_details/stores/test_reports/utils';
describe('Test reports summary', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/ci/pipeline_details/test_reports/test_summary_table_spec.js
index a45946d5a03..bb62fbcb32c 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
+++ b/spec/frontend/ci/pipeline_details/test_reports/test_summary_table_spec.js
@@ -3,8 +3,8 @@ import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import testReports from 'test_fixtures/pipelines/test_report.json';
-import SummaryTable from '~/pipelines/components/test_reports/test_summary_table.vue';
-import * as getters from '~/pipelines/stores/test_reports/getters';
+import SummaryTable from '~/ci/pipeline_details/test_reports/test_summary_table.vue';
+import * as getters from '~/ci/pipeline_details/stores/test_reports/getters';
Vue.use(Vuex);
diff --git a/spec/frontend/pipelines/pipeline_graph/utils_spec.js b/spec/frontend/ci/pipeline_details/utils/index_spec.js
index 96b18fcf96f..61230cb52e6 100644
--- a/spec/frontend/pipelines/pipeline_graph/utils_spec.js
+++ b/spec/frontend/ci/pipeline_details/utils/index_spec.js
@@ -1,5 +1,9 @@
-import { createJobsHash, generateJobNeedsDict, getPipelineDefaultTab } from '~/pipelines/utils';
-import { validPipelineTabNames, pipelineTabName } from '~/pipelines/constants';
+import {
+ createJobsHash,
+ generateJobNeedsDict,
+ getPipelineDefaultTab,
+} from '~/ci/pipeline_details/utils';
+import { validPipelineTabNames, pipelineTabName } from '~/ci/pipeline_details/constants';
describe('utils functions', () => {
const jobName1 = 'build_1';
diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/ci/pipeline_details/utils/parsing_utils_spec.js
index 286d79edc6c..9390f076d3d 100644
--- a/spec/frontend/pipelines/utils_spec.js
+++ b/spec/frontend/ci/pipeline_details/utils/parsing_utils_spec.js
@@ -1,5 +1,5 @@
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
-import { createSankey } from '~/pipelines/components/dag/drawing_utils';
+import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
import {
makeLinksFromNodes,
filterByAncestors,
@@ -9,13 +9,13 @@ import {
parseData,
removeOrphanNodes,
getMaxNodes,
-} from '~/pipelines/components/parsing_utils';
-import { createNodeDict } from '~/pipelines/utils';
+} from '~/ci/pipeline_details/utils/parsing_utils';
+import { createNodeDict } from '~/ci/pipeline_details/utils';
-import { mockDownstreamPipelinesRest } from '../vue_merge_request_widget/mock_data';
-import { mockDownstreamPipelinesGraphql } from '../commit/mock_data';
-import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
-import { generateResponse } from './graph/mock_data';
+import { mockDownstreamPipelinesRest } from '../../../vue_merge_request_widget/mock_data';
+import { mockDownstreamPipelinesGraphql } from '../../../commit/mock_data';
+import { mockParsedGraphQLNodes, missingJob } from '../dag/mock_data';
+import { generateResponse } from '../graph/mock_data';
describe('DAG visualization parsing utilities', () => {
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
diff --git a/spec/frontend/pipelines/unwrapping_utils_spec.js b/spec/frontend/ci/pipeline_details/utils/unwrapping_utils_spec.js
index a6ce7d4049f..99ee2eff1e4 100644
--- a/spec/frontend/pipelines/unwrapping_utils_spec.js
+++ b/spec/frontend/ci/pipeline_details/utils/unwrapping_utils_spec.js
@@ -2,7 +2,7 @@ import {
unwrapGroups,
unwrapNodesWithName,
unwrapStagesWithNeeds,
-} from '~/pipelines/components/unwrapping_utils';
+} from '~/ci/pipeline_details/utils/unwrapping_utils';
const groupsArray = [
{
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/ci/pipeline_editor/components/graph/mock_data.js
index db77e0a0573..db77e0a0573 100644
--- a/spec/frontend/pipelines/pipeline_graph/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/components/graph/mock_data.js
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/graph/pipeline_graph_spec.js
index 123f2e011c3..95edfb01cf0 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/graph/pipeline_graph_spec.js
@@ -2,11 +2,11 @@ import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
import { CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants';
-import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
-import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue';
-import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import StageName from '~/pipelines/components/pipeline_graph/stage_name.vue';
+import LinksInner from '~/ci/pipeline_details/graph/components/links_inner.vue';
+import LinksLayer from '~/ci/common/private/job_links_layer.vue';
+import JobPill from '~/ci/pipeline_editor/components/graph/job_pill.vue';
+import PipelineGraph from '~/ci/pipeline_editor/components/graph/pipeline_graph.vue';
+import StageName from '~/ci/pipeline_editor/components/graph/stage_name.vue';
import { pipelineData, singleStageData } from './mock_data';
describe('pipeline graph component', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
index a651664851e..655bfe538c6 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlCard } from '@gitlab/ui';
import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineStatus from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
import ValidationSegment from '~/ci/pipeline_editor/components/header/validation_segment.vue';
@@ -20,6 +21,9 @@ describe('Pipeline editor header', () => {
isNewCiConfigFile: false,
...props,
},
+ stubs: {
+ GlCard,
+ },
});
};
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
index f5e0b65d615..4ec1dd4b605 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -4,8 +4,8 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index 3bbe14adb88..1a2ed60a6f4 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -6,8 +6,9 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineStatus, { i18n } from '~/ci/pipeline_editor/components/header/pipeline_status.vue';
import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
+import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import { mockCommitSha, mockProjectPipeline, mockProjectFullPath } from '../../mock_data';
Vue.use(VueApollo);
@@ -21,6 +22,16 @@ describe('Pipeline Status', () => {
const handlers = [[getPipelineQuery, mockPipelineQuery]];
mockApollo = createMockApollo(handlers);
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: getPipelineEtag,
+ data: {
+ etags: {
+ __typename: 'EtagValues',
+ pipeline: 'pipelines/1',
+ },
+ },
+ });
+
wrapper = shallowMount(PipelineStatus, {
apolloProvider: mockApollo,
propsData: {
diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 77252a5c0b6..69e91f11309 100644
--- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -19,7 +19,7 @@ import {
VALIDATE_TAB,
VALIDATE_TAB_BADGE_DISMISSED_KEY,
} from '~/ci/pipeline_editor/constants';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import PipelineGraph from '~/ci/pipeline_editor/components/graph/pipeline_graph.vue';
import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
import {
mockBlobContentQueryResponse,
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 007abde939f..e08c35f1555 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -1,5 +1,5 @@
import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants';
-import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
+import { unwrapStagesWithNeeds } from '~/ci/pipeline_details/utils/unwrapping_utils';
import { DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility';
export const commonOptions = {
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js b/spec/frontend/ci/pipeline_mini_graph/job_item_spec.js
index b89f27e5c05..9c14e75caa4 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/job_item_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/job_item_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import JobItem from '~/pipelines/components/pipeline_mini_graph/job_item.vue';
+import JobItem from '~/ci/pipeline_mini_graph/job_item.vue';
describe('JobItem', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js
index 6661bb079d2..916f3053153 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_mini_graph_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import { pipelines } from 'test_fixtures/pipelines/pipelines.json';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import PipelineStages from '~/ci/pipeline_mini_graph/pipeline_stages.vue';
import mockLinkedPipelines from './linked_pipelines_mock_data';
const mockStages = pipelines[0].details.stages;
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
index 3697eaeea1a..30a0b868c5f 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
@@ -5,10 +5,10 @@ import MockAdapter from 'axios-mock-adapter';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import LegacyPipelineStage from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue';
-import eventHub from '~/pipelines/event_hub';
+import LegacyPipelineStage from '~/ci/pipeline_mini_graph/legacy_pipeline_stage.vue';
+import eventHub from '~/ci/event_hub';
import waitForPromises from 'helpers/wait_for_promises';
-import { stageReply } from '../../mock_data';
+import { stageReply } from './mock_data';
const dropdownPath = 'path.json';
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
index a4ecb9041c9..0396029cdaf 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import LinkedPipelinesMiniList from '~/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue';
+import LinkedPipelinesMiniList from '~/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue';
import mockData from './linked_pipelines_mock_data';
describe('Linked pipeline mini list', () => {
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mock_data.js
index 117c7f2ae52..117c7f2ae52 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js
+++ b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mock_data.js
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js b/spec/frontend/ci/pipeline_mini_graph/mock_data.js
index 1c13e9eb62b..231375b40dd 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/mock_data.js
+++ b/spec/frontend/ci/pipeline_mini_graph/mock_data.js
@@ -148,3 +148,105 @@ export const mockUpstreamDownstreamQueryResponse = {
export const linkedPipelinesFetchError = 'There was a problem fetching linked pipelines.';
export const stagesFetchError = 'There was a problem fetching the pipeline stages.';
+
+export const stageReply = {
+ name: 'deploy',
+ title: 'deploy: running',
+ latest_statuses: [
+ {
+ id: 928,
+ name: 'stop staging',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/928',
+ cancel_path: '/twitter/flight/-/jobs/928/cancel',
+ playable: false,
+ created_at: '2018-04-04T20:02:02.728Z',
+ updated_at: '2018-04-04T20:02:02.766Z',
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/928',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico',
+ action: {
+ icon: 'cancel',
+ title: 'Cancel',
+ path: '/twitter/flight/-/jobs/928/cancel',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 926,
+ name: 'production',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/926',
+ retry_path: '/twitter/flight/-/jobs/926/retry',
+ play_path: '/twitter/flight/-/jobs/926/play',
+ playable: true,
+ created_at: '2018-04-04T20:00:57.202Z',
+ updated_at: '2018-04-04T20:11:13.110Z',
+ status: {
+ icon: 'status_canceled',
+ text: 'canceled',
+ label: 'manual play action',
+ group: 'canceled',
+ tooltip: 'canceled',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/926',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/twitter/flight/-/jobs/926/play',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 217,
+ name: 'staging',
+ started: '2018-03-07T08:41:46.234Z',
+ build_path: '/twitter/flight/-/jobs/217',
+ retry_path: '/twitter/flight/-/jobs/217/retry',
+ playable: false,
+ created_at: '2018-03-07T14:41:58.093Z',
+ updated_at: '2018-03-07T14:41:58.093Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/217',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/twitter/flight/-/jobs/217/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ tooltip: 'running',
+ has_details: true,
+ details_path: '/twitter/flight/pipelines/13#deploy',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ path: '/twitter/flight/pipelines/13#deploy',
+ dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy',
+};
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js b/spec/frontend/ci/pipeline_mini_graph/pipeline_mini_graph_spec.js
index b3e157f75f6..6833726a297 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/pipeline_mini_graph_spec.js
@@ -7,10 +7,10 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import * as sharedGraphQlUtils from '~/graphql_shared/utils';
import {
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/ci/pipeline_mini_graph/pipeline_stage_spec.js
index 1989aad12b0..96966bcbb84 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/pipeline_stage_spec.js
@@ -4,8 +4,8 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
-import getPipelineStageQuery from '~/pipelines/graphql/queries/get_pipeline_stage.query.graphql';
-import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
+import getPipelineStageQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage.query.graphql';
+import PipelineStage from '~/ci/pipeline_mini_graph/pipeline_stage.vue';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/ci/pipeline_mini_graph/pipeline_stages_spec.js
index c212087b7e3..bbd39c6fcd9 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/pipeline_stages_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { pipelines } from 'test_fixtures/pipelines/pipelines.json';
-import LegacyPipelineStage from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue';
-import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue';
+import LegacyPipelineStage from '~/ci/pipeline_mini_graph/legacy_pipeline_stage.vue';
+import PipelineStages from '~/ci/pipeline_mini_graph/pipeline_stages.vue';
const mockStages = pipelines[0].details.stages;
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index 79a0cfa0dc9..33cf24c9ed1 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -97,6 +97,7 @@ describe('Pipeline schedules form', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
// Variables
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
+ const findVariableTypes = () => wrapper.findAllByTestId('pipeline-form-ci-variable-type');
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
const findHiddenValueInputs = () =>
@@ -182,6 +183,16 @@ describe('Pipeline schedules form', () => {
mock.restore();
});
+ it('changes variable type', async () => {
+ expect(findVariableTypes().at(0).props('selected')).toBe('ENV_VAR');
+
+ findVariableTypes().at(0).vm.$emit('select', 'FILE');
+
+ await nextTick();
+
+ expect(findVariableTypes().at(0).props('selected')).toBe('FILE');
+ });
+
it('creates blank variable on input change event', async () => {
expect(findVariableRows()).toHaveLength(1);
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
index 5cc3829efbd..70b4c7a5224 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js
@@ -1,5 +1,6 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { s__ } from '~/locale';
import PipelineScheduleTarget from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
@@ -20,18 +21,35 @@ describe('Pipeline schedule target', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findLink = () => wrapper.findComponent(GlLink);
+ const findTarget = () => wrapper.findComponent('[data-testid="pipeline-schedule-target"]');
- beforeEach(() => {
- createComponent();
- });
+ describe('with ref', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays icon', () => {
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('fork');
+ });
- it('displays icon', () => {
- expect(findIcon().exists()).toBe(true);
- expect(findIcon().props('name')).toBe('fork');
+ it('displays ref link', () => {
+ expect(findLink().attributes('href')).toBe(defaultProps.schedule.refPath);
+ expect(findLink().text()).toBe(defaultProps.schedule.refForDisplay);
+ });
});
- it('displays ref link', () => {
- expect(findLink().attributes('href')).toBe(defaultProps.schedule.refPath);
- expect(findLink().text()).toBe(defaultProps.schedule.refForDisplay);
+ describe('without refPath', () => {
+ beforeEach(() => {
+ createComponent({
+ schedule: { ...mockPipelineScheduleNodes[0], refPath: null, refForDisplay: null },
+ });
+ });
+
+ it('displays none for the target', () => {
+ expect(findIcon().exists()).toBe(false);
+ expect(findLink().exists()).toBe(false);
+ expect(findTarget().text()).toBe(s__('PipelineSchedules|None'));
+ });
});
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
deleted file mode 100644
index e4ff9a0545b..00000000000
--- a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
-
-describe('Take ownership modal', () => {
- let wrapper;
- const url = `/root/job-log-tester/-/pipeline_schedules/3/take_ownership`;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMountExtended(TakeOwnershipModalLegacy, {
- propsData: {
- ownershipUrl: url,
- ...props,
- },
- });
- };
-
- const findModal = () => wrapper.findComponent(GlModal);
-
- beforeEach(() => {
- createComponent();
- });
-
- it('has a primary action set to a url and a post data-method', () => {
- const actionPrimary = findModal().props('actionPrimary');
-
- expect(actionPrimary.attributes).toEqual(
- expect.objectContaining({
- category: 'primary',
- variant: 'confirm',
- href: url,
- 'data-method': 'post',
- }),
- );
- });
-
- it('shows a take ownership message', () => {
- expect(findModal().text()).toBe(
- 'Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
- );
- });
-});
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 8d4e0f1bea6..711b120c61e 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -1,8 +1,8 @@
// Fixture located at spec/frontend/fixtures/pipeline_schedules.rb
+import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json';
import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json';
-import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json';
const {
data: {
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/ci_templates_spec.js
index b560eea4882..980a8be24ea 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/empty_state/ci_templates_spec.js
@@ -1,6 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue';
+import CiTemplates from '~/ci/pipelines_page/components/empty_state/ci_templates.vue';
const pipelineEditorPath = '/-/ci/editor';
const suggestedCiTemplates = [
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js
index 700be076e0c..8620d41886e 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/empty_state/ios_templates_spec.js
@@ -3,8 +3,8 @@ import { nextTick } from 'vue';
import { GlPopover, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import IosTemplates from '~/pipelines/components/pipelines_list/empty_state/ios_templates.vue';
-import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue';
+import IosTemplates from '~/ci/pipelines_page/components/empty_state/ios_templates.vue';
+import CiTemplates from '~/ci/pipelines_page/components/empty_state/ci_templates.vue';
const pipelineEditorPath = '/-/ci/editor';
const registrationToken = 'SECRET_TOKEN';
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
index 5465e4d77da..0c42723f753 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/empty_state/no_ci_empty_state_spec.js
@@ -2,10 +2,10 @@ import '~/commons';
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import { stubExperiments } from 'helpers/experimentation_helper';
-import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
+import EmptyState from '~/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
-import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
-import IosTemplates from '~/pipelines/components/pipelines_list/empty_state/ios_templates.vue';
+import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
+import IosTemplates from '~/ci/pipelines_page/components/empty_state/ios_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/ci/pipelines_page/components/empty_state/pipelines_ci_templates_spec.js
index 4bf4257f462..fbef4aa08eb 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/empty_state/pipelines_ci_templates_spec.js
@@ -1,8 +1,8 @@
import '~/commons';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
-import CiTemplates from '~/pipelines/components/pipelines_list/empty_state/ci_templates.vue';
+import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
+import CiTemplates from '~/ci/pipelines_page/components/empty_state/ci_templates.vue';
const pipelineEditorPath = '/-/ci/editor';
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js b/spec/frontend/ci/pipelines_page/components/failure_widget/failed_job_details_spec.js
index 479ee854ecf..6967a369338 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/failure_widget/failed_job_details_spec.js
@@ -6,9 +6,9 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import FailedJobDetails from '~/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue';
-import RetryMrFailedJobMutation from '~/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql';
-import { BRIDGE_KIND } from '~/pipelines/components/graph/constants';
+import FailedJobDetails from '~/ci/pipelines_page/components/failure_widget/failed_job_details.vue';
+import RetryMrFailedJobMutation from '~/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql';
+import { BRIDGE_KIND } from '~/ci/pipeline_details/graph/constants';
import { job } from './mock';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js b/spec/frontend/ci/pipelines_page/components/failure_widget/failed_jobs_list_spec.js
index 967812cc627..af075b02b64 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/failure_widget/failed_jobs_list_spec.js
@@ -6,10 +6,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import FailedJobsList from '~/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue';
-import FailedJobDetails from '~/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue';
-import * as utils from '~/pipelines/components/pipelines_list/failure_widget/utils';
-import getPipelineFailedJobs from '~/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql';
+import FailedJobsList from '~/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue';
+import FailedJobDetails from '~/ci/pipelines_page/components/failure_widget/failed_job_details.vue';
+import * as utils from '~/ci/pipelines_page/components/failure_widget/utils';
+import getPipelineFailedJobs from '~/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql';
import { failedJobsMock, failedJobsMock2, failedJobsMockEmpty, activeFailedJobsMock } from './mock';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js b/spec/frontend/ci/pipelines_page/components/failure_widget/mock.js
index 318d787a984..318d787a984 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js
+++ b/spec/frontend/ci/pipelines_page/components/failure_widget/mock.js
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js b/spec/frontend/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget_spec.js
index 5bbb874edb0..e52b62feb23 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlCard, GlIcon, GlPopover } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
-import FailedJobsList from '~/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue';
+import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
+import FailedJobsList from '~/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue';
jest.mock('~/alert');
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/utils_spec.js b/spec/frontend/ci/pipelines_page/components/failure_widget/utils_spec.js
index 44f16478151..5755cd846ac 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/utils_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/failure_widget/utils_spec.js
@@ -1,7 +1,4 @@
-import {
- isFailedJob,
- sortJobsByStatus,
-} from '~/pipelines/components/pipelines_list/failure_widget/utils';
+import { isFailedJob, sortJobsByStatus } from '~/ci/pipelines_page/components/failure_widget/utils';
describe('isFailedJob', () => {
describe('when the job argument is undefined', () => {
diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/ci/pipelines_page/components/nav_controls_spec.js
index 15de7dc51f1..f4858ac27ea 100644
--- a/spec/frontend/pipelines/nav_controls_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/nav_controls_spec.js
@@ -1,5 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import NavControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
+import NavControls from '~/ci/pipelines_page/components/nav_controls.vue';
describe('Pipelines Nav Controls', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/pipeline_labels_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
index 6a37e36352b..b5c9a3030e0 100644
--- a/spec/frontend/pipelines/pipeline_labels_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
@@ -1,7 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
-import PipelineLabelsComponent from '~/pipelines/components/pipelines_list/pipeline_labels.vue';
-import { mockPipeline } from './mock_data';
+import PipelineLabelsComponent from '~/ci/pipelines_page/components/pipeline_labels.vue';
+import { mockPipeline } from 'jest/ci/pipeline_details/mock_data';
const projectPath = 'test/test';
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_multi_actions_spec.js
index 0fdc45a5931..7ae21db8815 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_multi_actions_spec.js
@@ -1,5 +1,12 @@
import { nextTick } from 'vue';
-import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlSprintf,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -10,13 +17,12 @@ import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PipelineMultiActions, {
i18n,
-} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
-import { TRACKING_CATEGORIES } from '~/pipelines/constants';
+} from '~/ci/pipelines_page/components/pipeline_multi_actions.vue';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
describe('Pipeline Multi Actions Dropdown', () => {
let wrapper;
let mockAxios;
- const focusInputMock = jest.fn();
const artifacts = [
{
@@ -58,26 +64,27 @@ describe('Pipeline Multi Actions Dropdown', () => {
pipelineId,
},
stubs: {
+ GlAlert,
GlSprintf,
- GlDropdown,
- GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
- methods: { focusInput: focusInputMock },
- }),
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType),
},
}),
);
};
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAlert = () => wrapper.findByTestId('artifacts-fetch-error');
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findAllArtifactItems = () => wrapper.findAllByTestId(artifactItemTestId);
const findFirstArtifactItem = () => wrapper.findByTestId(artifactItemTestId);
const findAllArtifactItemsData = () =>
- wrapper.findAllByTestId(artifactItemTestId).wrappers.map((x) => ({
- path: x.attributes('href'),
- name: x.text(),
- }));
+ findDropdown()
+ .props('items')
+ .map(({ text, href }) => ({
+ name: text,
+ path: href,
+ }));
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
const findEmptyMessage = () => wrapper.findByTestId('artifacts-empty-message');
const findWarning = () => wrapper.findByTestId('artifacts-fetch-warning');
@@ -108,7 +115,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
it('should render a loading spinner and no empty message', async () => {
createComponent();
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
@@ -123,7 +130,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
createComponent();
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await waitForPromises();
});
@@ -135,13 +142,29 @@ describe('Pipeline Multi Actions Dropdown', () => {
it('should focus the search box when opened with artifacts', () => {
findDropdown().vm.$emit('shown');
- expect(focusInputMock).toHaveBeenCalled();
+ expect(findSearchBox().attributes('autofocus')).not.toBe(undefined);
});
- it('should render all the provided artifacts when search query is empty', () => {
+ it('should clear searchQuery when dropdown is closed', async () => {
+ findDropdown().vm.$emit('shown');
+ findSearchBox().vm.$emit('input', 'job-2');
+ await waitForPromises();
+
+ expect(findSearchBox().vm.value).toBe('job-2');
+
+ findDropdown().vm.$emit('hidden');
+ await waitForPromises();
+
+ expect(findSearchBox().vm.value).toBe('');
+ });
+
+ it('should render all the provided artifacts when search query is empty', async () => {
findSearchBox().vm.$emit('input', '');
+ await waitForPromises();
- expect(findAllArtifactItems()).toHaveLength(artifacts.length);
+ expect(findAllArtifactItemsData()).toEqual(
+ artifacts.map(({ name, path }) => ({ name, path })),
+ );
expect(findEmptyMessage().exists()).toBe(false);
});
@@ -149,7 +172,12 @@ describe('Pipeline Multi Actions Dropdown', () => {
findSearchBox().vm.$emit('input', 'job-2');
await waitForPromises();
- expect(findAllArtifactItems()).toHaveLength(1);
+ expect(findAllArtifactItemsData()).toEqual([
+ {
+ name: 'job-2 my-artifact-2',
+ path: '/download/path-two',
+ },
+ ]);
expect(findEmptyMessage().exists()).toBe(false);
});
@@ -164,12 +192,12 @@ describe('Pipeline Multi Actions Dropdown', () => {
mockAxios.resetHistory();
mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts: newArtifacts });
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await nextTick();
});
it('should hide list and render a loading spinner on dropdown click', () => {
- expect(findAllArtifactItems()).toHaveLength(0);
+ expect(findAllArtifactItemsData()).toHaveLength(0);
expect(findLoadingIcon().exists()).toBe(true);
});
@@ -189,7 +217,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
beforeEach(async () => {
mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await waitForPromises();
});
@@ -217,7 +245,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
beforeEach(async () => {
mockAxios.onGet(newEndpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await waitForPromises();
});
@@ -227,7 +255,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
});
it('should clear list', () => {
- expect(findAllArtifactItems()).toHaveLength(0);
+ expect(findAllArtifactItemsData()).toHaveLength(0);
});
});
});
@@ -241,7 +269,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
it('should render empty message and no search box when no artifacts are found', async () => {
createComponent();
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await waitForPromises();
expect(findEmptyMessage().exists()).toBe(true);
@@ -258,7 +286,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
it('should render an error message', async () => {
createComponent();
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await waitForPromises();
const error = findAlert();
@@ -278,7 +306,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
createComponent();
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_artifacts_dropdown', {
label: TRACKING_CATEGORIES.table,
diff --git a/spec/frontend/pipelines/pipeline_operations_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
index b2191453824..d2eab64b317 100644
--- a/spec/frontend/pipelines/pipeline_operations_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
@@ -1,8 +1,8 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
-import PipelineMultiActions from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
-import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
-import eventHub from '~/pipelines/event_hub';
+import PipelinesManualActions from '~/ci/pipelines_page/components/pipelines_manual_actions.vue';
+import PipelineMultiActions from '~/ci/pipelines_page/components/pipeline_multi_actions.vue';
+import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
+import eventHub from '~/ci/event_hub';
describe('Pipeline operations', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
index 249126390f1..4d78a923542 100644
--- a/spec/frontend/pipelines/components/pipelines_list/pipieline_stop_modal_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
-import PipelineStopModal from '~/pipelines/components/pipelines_list/pipeline_stop_modal.vue';
-import { mockPipelineHeader } from '../../mock_data';
+import { mockPipelineHeader } from 'jest/ci/pipeline_details/mock_data';
+import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
describe('PipelineStopModal', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js
index 856c0484075..cb04171f031 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_triggerer_spec.js
@@ -1,6 +1,6 @@
import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import pipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
+import pipelineTriggerer from '~/ci/pipelines_page/components/pipeline_triggerer.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
describe('Pipelines Triggerer', () => {
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_url_spec.js
index 797ec676ccc..0ee22dda826 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_url_spec.js
@@ -1,10 +1,14 @@
import { merge } from 'lodash';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
+import PipelineUrlComponent from '~/ci/pipelines_page/components/pipeline_url.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { TRACKING_CATEGORIES } from '~/pipelines/constants';
-import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
+import {
+ mockPipeline,
+ mockPipelineBranch,
+ mockPipelineTag,
+} from 'jest/ci/pipeline_details/mock_data';
const projectPath = 'test/test';
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/ci/pipelines_page/components/pipelines_artifacts_spec.js
index 1abc2887682..557403b3de9 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipelines_artifacts_spec.js
@@ -5,7 +5,7 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
+import PipelineArtifacts from '~/ci/pipelines_page/components/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/ci/pipelines_page/components/pipelines_filtered_search_spec.js
index 51a4487a3ef..4cd85b86e31 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipelines_filtered_search_spec.js
@@ -5,13 +5,13 @@ import { nextTick } from 'vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
-import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
+import PipelinesFilteredSearch from '~/ci/pipelines_page/components/pipelines_filtered_search.vue';
import {
FILTERED_SEARCH_TERM,
OPERATORS_IS,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import { TRACKING_CATEGORIES } from '~/pipelines/constants';
-import { users, mockSearch, branches, tags } from '../mock_data';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
+import { users, mockSearch, branches, tags } from 'jest/ci/pipeline_details/mock_data';
describe('Pipelines filtered search', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/pipelines_manual_actions_spec.js b/spec/frontend/ci/pipelines_page/components/pipelines_manual_actions_spec.js
index 82cab88c9eb..a24e136f1ff 100644
--- a/spec/frontend/pipelines/pipelines_manual_actions_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipelines_manual_actions_spec.js
@@ -11,9 +11,9 @@ import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
-import getPipelineActionsQuery from '~/pipelines/graphql/queries/get_pipeline_actions.query.graphql';
-import { TRACKING_CATEGORIES } from '~/pipelines/constants';
+import PipelinesManualActions from '~/ci/pipelines_page/components/pipelines_manual_actions.vue';
+import getPipelineActionsQuery from '~/ci/pipelines_page/graphql/queries/get_pipeline_actions.query.graphql';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
Vue.use(VueApollo);
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/ci/pipelines_page/components/time_ago_spec.js
index d2aa340a980..f7203f8d1b4 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/time_ago_spec.js
@@ -1,7 +1,7 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
+import TimeAgo from '~/ci/pipelines_page/components/time_ago.vue';
describe('Timeago component', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/ci/pipelines_page/pipelines_spec.js
index cc85d6d99e0..5d1f431e57c 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/ci/pipelines_page/pipelines_spec.js
@@ -24,12 +24,12 @@ import { createAlert, VARIANT_WARNING } from '~/alert';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
-import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
-import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
-import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
-import { RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/pipelines/constants';
-import Store from '~/pipelines/stores/pipelines_store';
+import NavigationControls from '~/ci/pipelines_page/components/nav_controls.vue';
+import PipelinesComponent from '~/ci/pipelines_page/pipelines.vue';
+import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
+import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import { RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/ci/constants';
+import Store from '~/ci/pipeline_details/stores/pipelines_store';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import {
@@ -37,7 +37,8 @@ import {
setIdTypePreferenceMutationResponseWithErrors,
} from 'jest/issues/list/mock_data';
-import { stageReply, users, mockSearch, branches } from './mock_data';
+import { stageReply } from 'jest/ci/pipeline_mini_graph/mock_data';
+import { users, mockSearch, branches } from '../pipeline_details/mock_data';
jest.mock('@sentry/browser');
jest.mock('~/alert');
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/ci/pipelines_page/tokens/pipeline_branch_name_token_spec.js
index d518519a424..ea615d85c4b 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/ci/pipelines_page/tokens/pipeline_branch_name_token_spec.js
@@ -3,8 +3,8 @@ import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import PipelineBranchNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue';
-import { branches, mockBranchesAfterMap } from '../mock_data';
+import PipelineBranchNameToken from '~/ci/pipelines_page/tokens/pipeline_branch_name_token.vue';
+import { branches, mockBranchesAfterMap } from 'jest/ci/pipeline_details/mock_data';
describe('Pipeline Branch Name Token', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js b/spec/frontend/ci/pipelines_page/tokens/pipeline_source_token_spec.js
index 60abb63a7e0..0ea2b641b33 100644
--- a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js
+++ b/spec/frontend/ci/pipelines_page/tokens/pipeline_source_token_spec.js
@@ -1,8 +1,8 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { PIPELINE_SOURCES } from 'ee_else_ce/pipelines/components/pipelines_list/tokens/constants';
+import { PIPELINE_SOURCES } from 'ee_else_ce/ci/pipelines_page/tokens/constants';
import { stubComponent } from 'helpers/stub_component';
-import PipelineSourceToken from '~/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue';
+import PipelineSourceToken from '~/ci/pipelines_page/tokens/pipeline_source_token.vue';
describe('Pipeline Source Token', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/ci/pipelines_page/tokens/pipeline_status_token_spec.js
index cf4ccb5ce43..b8f98666438 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/ci/pipelines_page/tokens/pipeline_status_token_spec.js
@@ -1,7 +1,7 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
-import PipelineStatusToken from '~/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue';
+import PipelineStatusToken from '~/ci/pipelines_page/tokens/pipeline_status_token.vue';
import {
TOKEN_TITLE_STATUS,
TOKEN_TYPE_STATUS,
diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/ci/pipelines_page/tokens/pipeline_tag_name_token_spec.js
index 88c88d8f16f..d23d9f07df3 100644
--- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
+++ b/spec/frontend/ci/pipelines_page/tokens/pipeline_tag_name_token_spec.js
@@ -1,8 +1,8 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Api from '~/api';
-import PipelineTagNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue';
-import { tags, mockTagsAfterMap } from '../mock_data';
+import PipelineTagNameToken from '~/ci/pipelines_page/tokens/pipeline_tag_name_token.vue';
+import { tags, mockTagsAfterMap } from 'jest/ci/pipeline_details/mock_data';
describe('Pipeline Branch Name Token', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/ci/pipelines_page/tokens/pipeline_trigger_author_token_spec.js
index e9ec684a350..eccb90b0c94 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/ci/pipelines_page/tokens/pipeline_trigger_author_token_spec.js
@@ -2,8 +2,8 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import Api from '~/api';
-import PipelineTriggerAuthorToken from '~/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue';
-import { users } from '../mock_data';
+import PipelineTriggerAuthorToken from '~/ci/pipelines_page/tokens/pipeline_trigger_author_token.vue';
+import { users } from 'jest/ci/pipeline_details/mock_data';
describe('Pipeline Trigger Author Token', () => {
let wrapper;
diff --git a/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
index b5a4cb42463..2de634a6209 100644
--- a/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
+++ b/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
@@ -2,10 +2,9 @@
exports[`IssueStatusIcon renders "failed" state correctly 1`] = `
<div
- class="report-block-list-icon failed"
+ class="failed report-block-list-icon"
>
<gl-icon-stub
- data-qa-selector="status_failed_icon"
name="status_failed_borderless"
size="24"
/>
@@ -14,10 +13,9 @@ exports[`IssueStatusIcon renders "failed" state correctly 1`] = `
exports[`IssueStatusIcon renders "neutral" state correctly 1`] = `
<div
- class="report-block-list-icon neutral"
+ class="neutral report-block-list-icon"
>
<gl-icon-stub
- data-qa-selector="status_neutral_icon"
name="dash"
size="24"
/>
@@ -29,7 +27,6 @@ exports[`IssueStatusIcon renders "success" state correctly 1`] = `
class="report-block-list-icon success"
>
<gl-icon-stub
- data-qa-selector="status_success_icon"
name="status_success_borderless"
size="24"
/>
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index ad20d7682ed..bc77b7b89dd 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -13,7 +13,6 @@ import {
INSTANCE_TYPE,
I18N_INSTANCE_TYPE,
PROJECT_TYPE,
- I18N_NO_DESCRIPTION,
I18N_CREATED_AT_LABEL,
I18N_CREATED_AT_BY_LABEL,
} from '~/ci/runner/constants';
@@ -102,15 +101,6 @@ describe('RunnerTypeCell', () => {
it('Displays the runner description', () => {
expect(wrapper.text()).toContain(mockRunner.description);
- expect(wrapper.findByText(I18N_NO_DESCRIPTION).exists()).toBe(false);
- });
-
- it('Displays "No description" for missing runner description', () => {
- createComponent({
- runner: { description: null },
- });
-
- expect(wrapper.findByText(I18N_NO_DESCRIPTION).classes()).toContain('gl-text-secondary');
});
it('Displays last contact', () => {
diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js
index c452e32b0e4..3c5f8c4d6a9 100644
--- a/spec/frontend/ci/runner/components/runner_create_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js
@@ -61,6 +61,7 @@ describe('RunnerCreateForm', () => {
createComponent();
expect(findRunnerFormFields().props('value')).toEqual(defaultRunnerModel);
+ expect(findRunnerFormFields().props('runnerType')).toEqual(INSTANCE_TYPE);
});
it('shows a submit button', () => {
diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
index 93be4d9d35e..7e39a6b72f9 100644
--- a/spec/frontend/ci/runner/components/runner_form_fields_spec.js
+++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
@@ -132,8 +132,8 @@ describe('RunnerFormFields', () => {
it('when runner is of project type, locked checkbox can be checked', async () => {
createComponent({
+ runnerType: PROJECT_TYPE,
value: {
- runnerType: PROJECT_TYPE,
locked: false,
},
});
@@ -144,7 +144,6 @@ describe('RunnerFormFields', () => {
expect(wrapper.emitted('input').at(-1)).toEqual([
{
- runnerType: PROJECT_TYPE,
locked: true,
},
]);
diff --git a/spec/frontend/ci/runner/components/runner_managers_table_spec.js b/spec/frontend/ci/runner/components/runner_managers_table_spec.js
index cde6ee6eea0..d5782e21a2f 100644
--- a/spec/frontend/ci/runner/components/runner_managers_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_managers_table_spec.js
@@ -60,8 +60,8 @@ describe('RunnerJobs', () => {
it('shows status', () => {
createComponent();
- expect(findCellText({ field: 'status', i: 0 })).toBe(s__('Runners|Online'));
- expect(findCellText({ field: 'status', i: 1 })).toBe(s__('Runners|Online'));
+ expect(findCellText({ field: 'status', i: 0 })).toContain(s__('Runners|Online'));
+ expect(findCellText({ field: 'status', i: 0 })).toContain(s__('Runners|Idle'));
});
it('shows version', () => {
diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js
index 5851078a8d3..2ba1c31fe52 100644
--- a/spec/frontend/ci/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -15,6 +15,7 @@ import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
import runnerUpdateMutation from '~/ci/runner/graphql/edit/runner_update.mutation.graphql';
import { captureException } from '~/ci/runner/sentry_utils';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
+import { INSTANCE_TYPE } from '~/ci/runner/constants';
import { runnerFormData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
@@ -119,6 +120,7 @@ describe('RunnerUpdateForm', () => {
it('shows runner fields', () => {
expect(findRunnerFormFields().props('value')).toEqual(runnerToModel(mockRunner));
+ expect(findRunnerFormFields().props('runnerType')).toEqual(INSTANCE_TYPE);
});
it('form has not been submitted', () => {
diff --git a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
index 79194c20ff5..d0c1987829f 100644
--- a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
+++ b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
@@ -13,22 +13,16 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
<div
data-testid="slot-default"
>
-
<table
aria-busy=""
aria-colcount="2"
- class="table b-table gl-table"
+ class="b-table gl-table table"
role="table"
>
- <!---->
- <!---->
<thead
- class=""
role="rowgroup"
>
- <!---->
<tr
- class=""
role="row"
>
<th
@@ -56,14 +50,11 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
<tbody
role="rowgroup"
>
- <!---->
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -72,21 +63,16 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- Apple Distribution: Team Name (ABC123XYZ)
-
+ Apple Distribution: Team Name (ABC123XYZ)
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -95,21 +81,16 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- 33669367788748363528491290218354043267
-
+ 33669367788748363528491290218354043267
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -118,21 +99,16 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- Team Name (ABC123XYZ)
-
+ Team Name (ABC123XYZ)
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -141,21 +117,16 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- Apple Worldwide Developer Relations Certification Authority - G3
-
+ Apple Worldwide Developer Relations Certification Authority - G3
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -164,18 +135,12 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- April 26, 2023 at 7:20:39 PM GMT
-
+ April 26, 2023 at 7:20:39 PM GMT
</td>
</tr>
- <!---->
- <!---->
</tbody>
- <!---->
</table>
</div>
</div>
@@ -194,22 +159,16 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
<div
data-testid="slot-default"
>
-
<table
aria-busy=""
aria-colcount="2"
- class="table b-table gl-table"
+ class="b-table gl-table table"
role="table"
>
- <!---->
- <!---->
<thead
- class=""
role="rowgroup"
>
- <!---->
<tr
- class=""
role="row"
>
<th
@@ -237,14 +196,11 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
<tbody
role="rowgroup"
>
- <!---->
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -253,21 +209,16 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- 6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf
-
+ 6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -276,21 +227,16 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- iOS
-
+ iOS
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -299,21 +245,16 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- Team Name (ABC123XYZ)
-
+ Team Name (ABC123XYZ)
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -322,21 +263,16 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- iOS Demo - match Development com.gitlab.ios-demo
-
+ iOS Demo - match Development com.gitlab.ios-demo
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -345,21 +281,16 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- 33669367788748363528491290218354043267
-
+ 33669367788748363528491290218354043267
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
<strong>
@@ -368,18 +299,12 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
-
- August 1, 2023 at 11:15:13 PM GMT
-
+ August 1, 2023 at 11:15:13 PM GMT
</td>
</tr>
- <!---->
- <!---->
</tbody>
- <!---->
</table>
</div>
</div>
diff --git a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
index 21ffda8578a..f90acb5cb22 100644
--- a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
@@ -1,9 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NewCluster renders the cluster component correctly 1`] = `
-"<div class=\\"gl-pt-4\\">
- <h4>Enter your Kubernetes cluster certificate details</h4>
- <p>Enter details about your cluster. <b-link-stub href=\\"/help/user/project/clusters/add_existing_cluster\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub>
+<div
+ class="gl-pt-4"
+>
+ <h4>
+ Enter your Kubernetes cluster certificate details
+ </h4>
+ <p>
+ Enter details about your cluster.
+ <b-link-stub
+ class="gl-link"
+ href="/help/user/project/clusters/add_existing_cluster"
+ >
+ How do I use a certificate to connect to my cluster?
+ </b-link-stub>
</p>
-</div>"
+</div>
`;
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index 67b0ecdf7eb..b5fc3247165 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -5,42 +5,27 @@ exports[`Remove cluster confirmation modal renders buttons with modal included 1
class="gl-display-flex"
>
<button
- class="btn gl-mr-3 btn-danger btn-md gl-button"
+ class="btn btn-danger btn-md gl-button gl-mr-3"
data-testid="remove-integration-and-resources-button"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- Remove integration and resources
-
+ Remove integration and resources
</span>
</button>
-
<button
- class="btn btn-danger btn-md gl-button btn-danger-secondary"
+ class="btn btn-danger btn-danger-secondary btn-md gl-button"
data-testid="remove-integration-button"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- Remove integration
-
+ Remove integration
</span>
</button>
-
- <!---->
</div>
`;
@@ -49,63 +34,44 @@ exports[`Remove cluster confirmation modal two buttons open modal with "cleanup"
class="gl-display-flex"
>
<button
- class="btn gl-mr-3 btn-danger btn-md gl-button"
+ class="btn btn-danger btn-md gl-button gl-mr-3"
data-testid="remove-integration-and-resources-button"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- Remove integration and resources
-
+ Remove integration and resources
</span>
</button>
-
<button
- class="btn btn-danger btn-md gl-button btn-danger-secondary"
+ class="btn btn-danger btn-danger-secondary btn-md gl-button"
data-testid="remove-integration-button"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- Remove integration
-
+ Remove integration
</span>
</button>
-
<div
kind="danger"
>
<p>
You are about to remove your cluster integration and all GitLab-created resources associated with this cluster.
</p>
-
<div>
-
This will permanently delete the following resources:
-
<ul>
<li>
Any project namespaces
</li>
-
<li>
<code>
clusterroles
</code>
</li>
-
<li>
<code>
clusterrolebindings
@@ -113,15 +79,13 @@ exports[`Remove cluster confirmation modal two buttons open modal with "cleanup"
</li>
</ul>
</div>
-
<strong>
- To remove your integration and resources, type
+ To remove your integration and resources, type
<code>
my-test-cluster
</code>
- to confirm:
+ to confirm:
</strong>
-
<form
action="clusterPath"
class="gl-mb-5"
@@ -132,27 +96,23 @@ exports[`Remove cluster confirmation modal two buttons open modal with "cleanup"
type="hidden"
value="delete"
/>
-
<input
name="authenticity_token"
type="hidden"
/>
-
<input
name="cleanup"
type="hidden"
value="true"
/>
-
<input
autocomplete="off"
- class="gl-form-input form-control"
- id="__BVID__14"
+ class="form-control gl-form-input"
+ id="reference-0"
name="confirm_cluster_name_input"
type="text"
/>
</form>
-
<span>
If you do not wish to delete all associated GitLab resources, you can simply remove the integration.
</span>
@@ -165,58 +125,40 @@ exports[`Remove cluster confirmation modal two buttons open modal without "clean
class="gl-display-flex"
>
<button
- class="btn gl-mr-3 btn-danger btn-md gl-button"
+ class="btn btn-danger btn-md gl-button gl-mr-3"
data-testid="remove-integration-and-resources-button"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- Remove integration and resources
-
+ Remove integration and resources
</span>
</button>
-
<button
- class="btn btn-danger btn-md gl-button btn-danger-secondary"
+ class="btn btn-danger btn-danger-secondary btn-md gl-button"
data-testid="remove-integration-button"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- Remove integration
-
+ Remove integration
</span>
</button>
-
<div
kind="danger"
>
<p>
You are about to remove your cluster integration.
</p>
-
- <!---->
-
<strong>
- To remove your integration, type
+ To remove your integration, type
<code>
my-test-cluster
</code>
- to confirm:
+ to confirm:
</strong>
-
<form
action="clusterPath"
class="gl-mb-5"
@@ -227,28 +169,23 @@ exports[`Remove cluster confirmation modal two buttons open modal without "clean
type="hidden"
value="delete"
/>
-
<input
name="authenticity_token"
type="hidden"
/>
-
<input
name="cleanup"
type="hidden"
value="true"
/>
-
<input
autocomplete="off"
- class="gl-form-input form-control"
- id="__BVID__21"
+ class="form-control gl-form-input"
+ id="reference-0"
name="confirm_cluster_name_input"
type="text"
/>
</form>
-
- <!---->
</div>
</div>
`;
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
index 36d2c2cabc5..1c2bdd2f8bc 100644
--- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -2,14 +2,13 @@
exports[`Code navigation popover component renders popover 1`] = `
<div
- class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show"
+ class="bs-popover-bottom code-navigation-popover gl-popover popover popover-font-size-normal show"
style="left: 0px; top: 0px;"
>
<div
class="arrow"
style="left: 0px;"
/>
-
<gl-tabs-stub
contentclass="gl-py-0"
navclass="gl-hidden"
@@ -21,13 +20,11 @@ exports[`Code navigation popover component renders popover 1`] = `
titlelinkclass=""
>
<div
- class="overflow-auto code-navigation-popover-container"
+ class="code-navigation-popover-container overflow-auto"
>
- <div
- class=""
- >
+ <div>
<pre
- class="border-0 bg-transparent m-0 code highlight text-wrap"
+ class="bg-transparent border-0 code highlight m-0 text-wrap"
>
<span
class="line"
@@ -39,9 +36,8 @@ exports[`Code navigation popover component renders popover 1`] = `
function
</span>
<span>
- main() {
+ main() {
</span>
-
<br />
</span>
<span
@@ -51,15 +47,13 @@ exports[`Code navigation popover component renders popover 1`] = `
<span>
}
</span>
-
<br />
</span>
</pre>
</div>
</div>
-
<div
- class="popover-body border-top"
+ class="border-top popover-body"
>
<gl-button-stub
buttontextclasses=""
@@ -72,25 +66,19 @@ exports[`Code navigation popover component renders popover 1`] = `
target="_blank"
variant="default"
>
-
Go to definition
-
</gl-button-stub>
</div>
</gl-tab-stub>
-
<gl-tab-stub
class="py-2"
data-testid="references-tab"
titlelinkclass=""
>
-
<p
class="gl-my-4 gl-px-4"
>
-
No references found
-
</p>
</gl-tab-stub>
</gl-tabs-stub>
diff --git a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
index 60c87aa10eb..24b2677f497 100644
--- a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
+++ b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
@@ -13,23 +13,20 @@ exports[`Comment templates list item component renders list item 1`] = `
>
test
</h6>
-
<div
class="gl-ml-auto"
>
<div
- class="gl-new-dropdown gl-disclosure-dropdown"
+ class="gl-disclosure-dropdown gl-new-dropdown"
>
<button
- aria-controls="base-dropdown-7"
- aria-labelledby="actions-toggle-3"
- class="btn btn-default btn-md gl-button btn-default-tertiary gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
+ aria-controls="reference-1"
+ aria-labelledby="reference-0"
+ class="btn btn-default btn-default-tertiary btn-md gl-button gl-new-dropdown-icon-only gl-new-dropdown-toggle gl-new-dropdown-toggle-no-caret"
data-testid="base-dropdown-toggle"
- id="actions-toggle-3"
+ id="reference-0"
type="button"
>
- <!---->
-
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
@@ -40,36 +37,29 @@ exports[`Comment templates list item component renders list item 1`] = `
href="file-mock#ellipsis_v"
/>
</svg>
-
<span
class="gl-button-text"
>
<span
class="gl-new-dropdown-button-text gl-sr-only"
>
-
- Comment template actions
-
+ Comment template actions
</span>
-
- <!---->
</span>
</button>
-
<div
class="gl-new-dropdown-panel gl-w-31!"
data-testid="base-dropdown-menu"
- id="base-dropdown-7"
+ id="reference-1"
>
<div
class="gl-new-dropdown-inner"
>
-
<ul
- aria-labelledby="actions-toggle-3"
+ aria-labelledby="reference-0"
class="gl-new-dropdown-contents"
data-testid="disclosure-content"
- id="disclosure-4"
+ id="reference-2"
tabindex="-1"
>
<li
@@ -86,9 +76,7 @@ exports[`Comment templates list item component renders list item 1`] = `
<span
class="gl-new-dropdown-item-text-wrapper"
>
-
- Edit
-
+ Edit
</span>
</button>
</li>
@@ -106,36 +94,25 @@ exports[`Comment templates list item component renders list item 1`] = `
<span
class="gl-new-dropdown-item-text-wrapper"
>
-
- Delete
-
+ Delete
</span>
</button>
</li>
</ul>
-
</div>
</div>
</div>
-
<div
class="gl-tooltip"
>
-
Comment template actions
-
</div>
</div>
</div>
-
<div
- class="gl-font-monospace gl-white-space-pre-line gl-font-sm gl-mt-n5"
+ class="gl-font-monospace gl-font-sm gl-mt-n5 gl-white-space-pre-line"
>
-
/assign_reviewer
-
</div>
-
- <!---->
</li>
`;
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index 3b3e5098857..891cd0a6b83 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -7,11 +7,11 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import CommitBoxPipelineMiniGraph from '~/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import { COMMIT_BOX_POLL_INTERVAL } from '~/projects/commit_box/info/constants';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql';
import * as sharedGraphQlUtils from '~/graphql_shared/utils';
import {
mockDownstreamQueryResponse,
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js
index 009ec68ddcf..4af292e3588 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js
@@ -7,7 +7,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
+import LegacyPipelinesTableWraper from '~/commit/pipelines/legacy_pipelines_table_wrapper.vue';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
@@ -15,7 +15,7 @@ import {
HTTP_STATUS_UNAUTHORIZED,
} from '~/lib/utils/http_status';
import { createAlert } from '~/alert';
-import { TOAST_MESSAGE } from '~/pipelines/constants';
+import { TOAST_MESSAGE } from '~/ci/pipeline_details/constants';
import axios from '~/lib/utils/axios_utils';
const $toast = {
@@ -42,7 +42,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
const createComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
- mount(PipelinesTable, {
+ mount(LegacyPipelinesTableWraper, {
propsData: {
endpoint: 'endpoint.json',
emptyStateSvgPath: 'foo',
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index d9f161b47b1..b17987dad89 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -7,17 +7,13 @@ exports[`Confidential merge request project form group component renders empty s
<label>
Project
</label>
-
<div>
- <!---->
-
<p
- class="gl-text-gray-600 gl-mt-1 gl-mb-0"
+ class="gl-mb-0 gl-mt-1 gl-text-gray-600"
>
-
- No forks are available to you.
+ No forks are available to you.
<br />
- To protect this issue's confidentiality,
+ To protect this issue's confidentiality,
<a
class="help-link"
href="https://test.com"
@@ -25,9 +21,9 @@ exports[`Confidential merge request project form group component renders empty s
>
fork this project
</a>
- and set the fork's visibility to private.
+ and set the fork's visibility to private.
<gl-link-stub
- class="gl-w-auto gl-p-0 gl-display-inline-block gl-bg-transparent"
+ class="gl-bg-transparent gl-display-inline-block gl-p-0 gl-w-auto"
href="/help"
target="_blank"
>
@@ -36,7 +32,6 @@ exports[`Confidential merge request project form group component renders empty s
>
Read more
</span>
-
<gl-icon-stub
name="question-o"
size="16"
@@ -54,21 +49,17 @@ exports[`Confidential merge request project form group component renders fork dr
<label>
Project
</label>
-
<div>
<dropdown-stub
projects="[object Object],[object Object]"
selectedproject="[object Object]"
/>
-
<p
- class="gl-text-gray-600 gl-mt-1 gl-mb-0"
+ class="gl-mb-0 gl-mt-1 gl-text-gray-600"
>
-
- To protect this issue's confidentiality, a private fork of this project was selected.
-
+ To protect this issue's confidentiality, a private fork of this project was selected.
<gl-link-stub
- class="gl-w-auto gl-p-0 gl-display-inline-block gl-bg-transparent"
+ class="gl-bg-transparent gl-display-inline-block gl-p-0 gl-w-auto"
href="/help"
target="_blank"
>
@@ -77,7 +68,6 @@ exports[`Confidential merge request project form group component renders fork dr
>
Read more
</span>
-
<gl-icon-stub
name="question-o"
size="16"
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
index a328f79e4e7..a708f7d5f47 100644
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
+++ b/spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap
@@ -1,9 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`content_editor/components/toolbar_button displays tertiary, medium button with a provided label and icon 1`] = `
-"<b-button-stub size=\\"sm\\" tag=\\"button\\" type=\\"button\\" variant=\\"default\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-mr-3 gl-button btn-default-tertiary btn-icon\\">
- <!---->
- <gl-icon-stub name=\\"bold\\" size=\\"16\\" class=\\"gl-button-icon\\"></gl-icon-stub>
- <!---->
-</b-button-stub>"
+<b-button-stub
+ aria-label="Bold"
+ class="btn-default-tertiary btn-icon gl-button gl-mr-3"
+ size="sm"
+ tag="button"
+ title="Bold"
+ type="button"
+ variant="default"
+>
+ <gl-icon-stub
+ class="gl-button-icon"
+ name="bold"
+ size="16"
+ />
+</b-button-stub>
`;
diff --git a/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap b/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap
index a9d42769789..e058f05fec4 100644
--- a/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap
+++ b/spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap
@@ -2,23 +2,18 @@
exports[`content/components/wrappers/table_of_contents collects all headings and renders a nested list of headings 1`] = `
<div
- class="table-of-contents gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5 gl-p-4!"
+ class="gl-border-1 gl-border-gray-100 gl-border-solid gl-mb-5 gl-p-4! table-of-contents"
data-testid="table-of-contents"
>
-
Table of contents
-
<li
dir="auto"
>
<a
href="#"
>
-
- Heading 1
-
+ Heading 1
</a>
-
<ul
dir="auto"
>
@@ -28,11 +23,8 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<a
href="#"
>
-
- Heading 1.1
-
+ Heading 1.1
</a>
-
<ul
dir="auto"
>
@@ -42,12 +34,8 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<a
href="#"
>
-
- Heading 1.1.1
-
+ Heading 1.1.1
</a>
-
- <!---->
</li>
</ul>
</li>
@@ -57,11 +45,8 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<a
href="#"
>
-
- Heading 1.2
-
+ Heading 1.2
</a>
-
<ul
dir="auto"
>
@@ -71,12 +56,8 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<a
href="#"
>
-
- Heading 1.2.1
-
+ Heading 1.2.1
</a>
-
- <!---->
</li>
</ul>
</li>
@@ -86,12 +67,8 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<a
href="#"
>
-
- Heading 1.3
-
+ Heading 1.3
</a>
-
- <!---->
</li>
<li
dir="auto"
@@ -99,11 +76,8 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<a
href="#"
>
-
- Heading 1.4
-
+ Heading 1.4
</a>
-
<ul
dir="auto"
>
@@ -113,12 +87,8 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<a
href="#"
>
-
- Heading 1.4.1
-
+ Heading 1.4.1
</a>
-
- <!---->
</li>
</ul>
</li>
@@ -130,12 +100,8 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<a
href="#"
>
-
- Heading 2
-
+ Heading 2
</a>
-
- <!---->
</li>
</div>
`;
diff --git a/spec/frontend/content_editor/components/wrappers/code_block_spec.js b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
index e802681dfc6..0093393eceb 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -11,6 +11,9 @@ import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vu
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
import { emitEditorEvent, createTestEditor, mockChainedCommands } from '../../test_utils';
+// Disabled due to eslint reporting errors for inline snapshots
+/* eslint-disable no-irregular-whitespace */
+
const SAMPLE_README_CONTENT = `# Sample README
This is a sample README.
@@ -212,12 +215,20 @@ describe('content/components/wrappers/code_block', () => {
it('shows a code suggestion block', () => {
expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 5');
- expect(findCodeDeleted()).toMatchInlineSnapshot(
- `"<code data-line-number=\\"5\\">## Usage\u200b</code>"`,
- );
- expect(findCodeAdded()).toMatchInlineSnapshot(
- `"<code data-line-number=\\"5\\">\u200b</code>"`,
- );
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ <code
+ data-line-number="5"
+ >
+ ## Usage​
+ </code>
+ `);
+ expect(findCodeAdded()).toMatchInlineSnapshot(`
+ <code
+ data-line-number="5"
+ >
+ ​
+ </code>
+ `);
});
describe('decrement line start button', () => {
@@ -232,9 +243,11 @@ describe('content/components/wrappers/code_block', () => {
expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 4 to 5');
expect(findCodeDeleted()).toMatchInlineSnapshot(`
- "<code data-line-number=\\"4\\">\u200b
+ <code
+ data-line-number="4"
+ >
+ ​
</code>
- <code data-line-number=\\"5\\">## Usage\u200b</code>"
`);
});
@@ -248,15 +261,11 @@ describe('content/components/wrappers/code_block', () => {
expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 1 to 5');
expect(findCodeDeleted()).toMatchInlineSnapshot(`
- "<code data-line-number=\\"1\\"># Sample README\u200b
- </code>
- <code data-line-number=\\"2\\">\u200b
- </code>
- <code data-line-number=\\"3\\">This is a sample README.\u200b
- </code>
- <code data-line-number=\\"4\\">\u200b
+ <code
+ data-line-number="1"
+ >
+ # Sample README​
</code>
- <code data-line-number=\\"5\\">## Usage\u200b</code>"
`);
expect(button.attributes('disabled')).toBe('disabled');
@@ -291,9 +300,11 @@ describe('content/components/wrappers/code_block', () => {
expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 4 to 5');
expect(findCodeDeleted()).toMatchInlineSnapshot(`
- "<code data-line-number=\\"4\\">\u200b
+ <code
+ data-line-number="4"
+ >
+ ​
</code>
- <code data-line-number=\\"5\\">## Usage\u200b</code>"
`);
});
});
@@ -326,9 +337,11 @@ describe('content/components/wrappers/code_block', () => {
expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 6');
expect(findCodeDeleted()).toMatchInlineSnapshot(`
- "<code data-line-number=\\"5\\">## Usage\u200b
+ <code
+ data-line-number="5"
+ >
+ ## Usage​
</code>
- <code data-line-number=\\"6\\">\u200b</code>"
`);
});
});
@@ -345,9 +358,11 @@ describe('content/components/wrappers/code_block', () => {
expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 6');
expect(findCodeDeleted()).toMatchInlineSnapshot(`
- "<code data-line-number=\\"5\\">## Usage\u200b
+ <code
+ data-line-number="5"
+ >
+ ## Usage​
</code>
- <code data-line-number=\\"6\\">\u200b</code>"
`);
});
@@ -361,15 +376,11 @@ describe('content/components/wrappers/code_block', () => {
expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 9');
expect(findCodeDeleted()).toMatchInlineSnapshot(`
- "<code data-line-number=\\"5\\">## Usage\u200b
- </code>
- <code data-line-number=\\"6\\">\u200b
- </code>
- <code data-line-number=\\"7\\">\`\`\`yaml\u200b
- </code>
- <code data-line-number=\\"8\\">foo: bar\u200b
+ <code
+ data-line-number="5"
+ >
+ ## Usage​
</code>
- <code data-line-number=\\"9\\">\`\`\`\u200b</code>"
`);
expect(button.attributes('disabled')).toBe('disabled');
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 7be8114902a..3eb00f69345 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -175,7 +175,7 @@ describe('markdownSerializer', () => {
inlineDiff({ type: 'deletion' }, '-10 lines'),
),
),
- ).toBe('{+\\+30 lines+}{-\\-10 lines-}');
+ ).toBe('{++30 lines+}{--10 lines-}');
});
it('correctly serializes highlight', () => {
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_created_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_created_spec.js
index 4be4aa50dfc..50b12244a55 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_created_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_created_spec.js
@@ -23,15 +23,15 @@ describe('ContributionEventCreated', () => {
};
describe.each`
- event | expectedMessage | expectedIconName | expectedIconClass
- ${eventProjectCreated()} | ${'Created project %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
- ${eventMilestoneCreated()} | ${'Opened milestone %{targetLink} in %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
- ${eventIssueCreated()} | ${'Opened issue %{targetLink} in %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
- ${eventMergeRequestCreated()} | ${'Opened merge request %{targetLink} in %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
- ${eventWikiPageCreated()} | ${'Created wiki page %{targetLink} in %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
- ${eventDesignCreated()} | ${'Added design %{targetLink} in %{resourceParentLink}.'} | ${'upload'} | ${null}
- ${{ resource_parent: { type: 'unsupported type' } }} | ${'Created resource.'} | ${'status_open'} | ${'gl-text-green-500'}
- ${{ target: { type: 'unsupported type' } }} | ${'Created resource.'} | ${'status_open'} | ${'gl-text-green-500'}
+ event | expectedMessage | expectedIconName | expectedIconClass
+ ${eventProjectCreated()} | ${'Created project %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
+ ${eventMilestoneCreated()} | ${'Opened milestone %{targetLink} in %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
+ ${eventIssueCreated()} | ${'Opened issue %{targetLink} in %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
+ ${eventMergeRequestCreated()} | ${'Opened merge request %{targetLink} in %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
+ ${eventWikiPageCreated()} | ${'Created wiki page %{targetLink} in %{resourceParentLink}.'} | ${'status_open'} | ${'gl-text-green-500'}
+ ${eventDesignCreated()} | ${'Added design %{targetLink} in %{resourceParentLink}.'} | ${'upload'} | ${null}
+ ${{ resource_parent: { type: 'unsupported type' }, target: { type: null } }} | ${'Created resource.'} | ${'status_open'} | ${'gl-text-green-500'}
+ ${{ target: { type: 'unsupported type' } }} | ${'Created resource.'} | ${'status_open'} | ${'gl-text-green-500'}
`(
'when event target type is $event.target.type',
({ event, expectedMessage, expectedIconName, expectedIconClass }) => {
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_destroyed_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_destroyed_spec.js
new file mode 100644
index 00000000000..b296b75ce0a
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_destroyed_spec.js
@@ -0,0 +1,32 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventDestroyed from '~/contribution_events/components/contribution_event/contribution_event_destroyed.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventDesignDestroyed, eventWikiPageDestroyed, eventMilestoneDestroyed } from '../../utils';
+
+describe('ContributionEventDestroyed', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData }) => {
+ wrapper = shallowMountExtended(ContributionEventDestroyed, {
+ propsData,
+ });
+ };
+
+ describe.each`
+ event | expectedMessage | iconName
+ ${eventDesignDestroyed()} | ${'Archived design in %{resourceParentLink}.'} | ${'archive'}
+ ${eventWikiPageDestroyed()} | ${'Deleted wiki page in %{resourceParentLink}.'} | ${'remove'}
+ ${eventMilestoneDestroyed()} | ${'Deleted milestone in %{resourceParentLink}.'} | ${'remove'}
+ ${{ target: { type: 'unsupported type' } }} | ${'Deleted resource.'} | ${'remove'}
+ `('when event target type is $event.target.type', ({ event, expectedMessage, iconName }) => {
+ it('renders `ContributionEventBase` with correct props', () => {
+ createComponent({ propsData: { event } });
+
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event,
+ message: expectedMessage,
+ iconName,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_updated_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_updated_spec.js
new file mode 100644
index 00000000000..e8e25b24dc9
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_updated_spec.js
@@ -0,0 +1,31 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventUpdated from '~/contribution_events/components/contribution_event/contribution_event_updated.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventDesignUpdated, eventWikiPageUpdated } from '../../utils';
+
+describe('ContributionEventUpdated', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData }) => {
+ wrapper = shallowMountExtended(ContributionEventUpdated, {
+ propsData,
+ });
+ };
+
+ describe.each`
+ event | expectedMessage
+ ${eventDesignUpdated()} | ${'Updated design %{targetLink} in %{resourceParentLink}.'}
+ ${eventWikiPageUpdated()} | ${'Updated wiki page %{targetLink} in %{resourceParentLink}.'}
+ ${{ target: { type: 'unsupported type' } }} | ${'Updated resource.'}
+ `('when event target type is $event.target.type', ({ event, expectedMessage }) => {
+ it('renders `ContributionEventBase` with correct props', () => {
+ createComponent({ propsData: { event } });
+
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event,
+ message: expectedMessage,
+ iconName: 'pencil',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_events_spec.js b/spec/frontend/contribution_events/components/contribution_events_spec.js
index 7493d248e2b..dc460a698bd 100644
--- a/spec/frontend/contribution_events/components/contribution_events_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_events_spec.js
@@ -11,6 +11,8 @@ import ContributionEventCreated from '~/contribution_events/components/contribut
import ContributionEventClosed from '~/contribution_events/components/contribution_event/contribution_event_closed.vue';
import ContributionEventReopened from '~/contribution_events/components/contribution_event/contribution_event_reopened.vue';
import ContributionEventCommented from '~/contribution_events/components/contribution_event/contribution_event_commented.vue';
+import ContributionEventUpdated from '~/contribution_events/components/contribution_event/contribution_event_updated.vue';
+import ContributionEventDestroyed from '~/contribution_events/components/contribution_event/contribution_event_destroyed.vue';
import {
eventApproved,
eventExpired,
@@ -23,6 +25,8 @@ import {
eventClosed,
eventReopened,
eventCommented,
+ eventUpdated,
+ eventDestroyed,
} from '../utils';
describe('ContributionEvents', () => {
@@ -43,6 +47,8 @@ describe('ContributionEvents', () => {
eventClosed(),
eventReopened(),
eventCommented(),
+ eventUpdated(),
+ eventDestroyed(),
],
},
});
@@ -61,6 +67,8 @@ describe('ContributionEvents', () => {
${ContributionEventClosed} | ${eventClosed()}
${ContributionEventReopened} | ${eventReopened()}
${ContributionEventCommented} | ${eventCommented()}
+ ${ContributionEventUpdated} | ${eventUpdated()}
+ ${ContributionEventDestroyed} | ${eventDestroyed()}
`(
'renders `$expectedComponent.name` component and passes expected event',
({ expectedComponent, expectedEvent }) => {
diff --git a/spec/frontend/contribution_events/components/target_link_spec.js b/spec/frontend/contribution_events/components/target_link_spec.js
index 40650b3585c..968a9d3bd3d 100644
--- a/spec/frontend/contribution_events/components/target_link_spec.js
+++ b/spec/frontend/contribution_events/components/target_link_spec.js
@@ -49,7 +49,7 @@ describe('TargetLink', () => {
});
});
- describe('when target is not defined', () => {
+ describe('when target type is not defined', () => {
beforeEach(() => {
createComponent({ propsData: { event: eventJoined() } });
});
diff --git a/spec/frontend/contribution_events/utils.js b/spec/frontend/contribution_events/utils.js
index 8b34506c6ac..f91a4dd800b 100644
--- a/spec/frontend/contribution_events/utils.js
+++ b/spec/frontend/contribution_events/utils.js
@@ -10,6 +10,8 @@ import {
EVENT_TYPE_CLOSED,
EVENT_TYPE_REOPENED,
EVENT_TYPE_COMMENTED,
+ EVENT_TYPE_UPDATED,
+ EVENT_TYPE_DESTROYED,
PUSH_EVENT_REF_TYPE_BRANCH,
PUSH_EVENT_REF_TYPE_TAG,
EVENT_TYPE_CREATED,
@@ -32,22 +34,12 @@ import {
COMMIT_NOTEABLE_TYPE,
} from '~/notes/constants';
+// Private finders
const findEventByAction = (action) => () => events.find((event) => event.action === action);
const findEventByActionAndTargetType = (action, targetType) => () =>
events.find((event) => event.action === action && event.target?.type === targetType);
const findEventByActionAndIssueType = (action, issueType) => () =>
events.find((event) => event.action === action && event.target.issue_type === issueType);
-
-export const eventApproved = findEventByAction(EVENT_TYPE_APPROVED);
-
-export const eventExpired = findEventByAction(EVENT_TYPE_EXPIRED);
-
-export const eventJoined = findEventByAction(EVENT_TYPE_JOINED);
-
-export const eventLeft = findEventByAction(EVENT_TYPE_LEFT);
-
-export const eventMerged = findEventByAction(EVENT_TYPE_MERGED);
-
const findPushEvent = ({
isNew = false,
isRemoved = false,
@@ -62,6 +54,45 @@ const findPushEvent = ({
ref.type === refType &&
commit.count === commitCount,
);
+const findEventByActionAndNoteableType = (action, noteableType) => () =>
+ events.find((event) => event.action === action && event.noteable?.type === noteableType);
+const findCommentedSnippet = (resourceParentType) => () =>
+ events.find(
+ (event) =>
+ event.action === EVENT_TYPE_COMMENTED &&
+ event.noteable?.type === SNIPPET_NOTEABLE_TYPE &&
+ event.resource_parent?.type === resourceParentType,
+ );
+const findUpdatedEvent = (targetType) =>
+ findEventByActionAndTargetType(EVENT_TYPE_UPDATED, targetType);
+const findDestroyedEvent = (targetType) =>
+ findEventByActionAndTargetType(EVENT_TYPE_DESTROYED, targetType);
+
+// Finders that are used by EE
+export const findCreatedEvent = (targetType) =>
+ findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
+export const findWorkItemCreatedEvent = (issueType) =>
+ findEventByActionAndIssueType(EVENT_TYPE_CREATED, issueType);
+export const findClosedEvent = (targetType) =>
+ findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
+export const findWorkItemClosedEvent = (issueType) =>
+ findEventByActionAndIssueType(EVENT_TYPE_CLOSED, issueType);
+export const findReopenedEvent = (targetType) =>
+ findEventByActionAndTargetType(EVENT_TYPE_REOPENED, targetType);
+export const findWorkItemReopenedEvent = (issueType) =>
+ findEventByActionAndIssueType(EVENT_TYPE_REOPENED, issueType);
+export const findCommentedEvent = (noteableType) =>
+ findEventByActionAndNoteableType(EVENT_TYPE_COMMENTED, noteableType);
+
+export const eventApproved = findEventByAction(EVENT_TYPE_APPROVED);
+
+export const eventExpired = findEventByAction(EVENT_TYPE_EXPIRED);
+
+export const eventJoined = findEventByAction(EVENT_TYPE_JOINED);
+
+export const eventLeft = findEventByAction(EVENT_TYPE_LEFT);
+
+export const eventMerged = findEventByAction(EVENT_TYPE_MERGED);
export const eventPushedNewBranch = findPushEvent({ isNew: true });
export const eventPushedNewTag = findPushEvent({ isNew: true, refType: PUSH_EVENT_REF_TYPE_TAG });
@@ -77,13 +108,7 @@ export const eventBulkPushedBranch = findPushEvent({ commitCount: 5 });
export const eventPrivate = () => ({ ...events[0], action: EVENT_TYPE_PRIVATE });
export const eventCreated = findEventByAction(EVENT_TYPE_CREATED);
-
-export const findCreatedEvent = (targetType) =>
- findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
-export const findWorkItemCreatedEvent = (issueType) =>
- findEventByActionAndIssueType(EVENT_TYPE_CREATED, issueType);
-
-export const eventProjectCreated = findCreatedEvent(undefined);
+export const eventProjectCreated = findCreatedEvent(null);
export const eventMilestoneCreated = findCreatedEvent(TARGET_TYPE_MILESTONE);
export const eventIssueCreated = findCreatedEvent(TARGET_TYPE_ISSUE);
export const eventMergeRequestCreated = findCreatedEvent(TARGET_TYPE_MERGE_REQUEST);
@@ -93,12 +118,6 @@ export const eventTaskCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_TA
export const eventIncidentCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
export const eventClosed = findEventByAction(EVENT_TYPE_CLOSED);
-
-export const findClosedEvent = (targetType) =>
- findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
-export const findWorkItemClosedEvent = (issueType) =>
- findEventByActionAndIssueType(EVENT_TYPE_CLOSED, issueType);
-
export const eventMilestoneClosed = findClosedEvent(TARGET_TYPE_MILESTONE);
export const eventIssueClosed = findClosedEvent(TARGET_TYPE_ISSUE);
export const eventMergeRequestClosed = findClosedEvent(TARGET_TYPE_MERGE_REQUEST);
@@ -108,12 +127,6 @@ export const eventTaskClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_TASK
export const eventIncidentClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
export const eventReopened = findEventByAction(EVENT_TYPE_REOPENED);
-
-export const findReopenedEvent = (targetType) =>
- findEventByActionAndTargetType(EVENT_TYPE_REOPENED, targetType);
-export const findWorkItemReopenedEvent = (issueType) =>
- findEventByActionAndIssueType(EVENT_TYPE_REOPENED, issueType);
-
export const eventMilestoneReopened = findReopenedEvent(TARGET_TYPE_MILESTONE);
export const eventMergeRequestReopened = findReopenedEvent(TARGET_TYPE_MERGE_REQUEST);
export const eventWikiPageReopened = findReopenedEvent(TARGET_TYPE_WIKI);
@@ -123,19 +136,6 @@ export const eventTaskReopened = findWorkItemReopenedEvent(WORK_ITEM_ISSUE_TYPE_
export const eventIncidentReopened = findWorkItemReopenedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
export const eventCommented = findEventByAction(EVENT_TYPE_COMMENTED);
-
-const findEventByActionAndNoteableType = (action, noteableType) => () =>
- events.find((event) => event.action === action && event.noteable?.type === noteableType);
-export const findCommentedEvent = (noteableType) =>
- findEventByActionAndNoteableType(EVENT_TYPE_COMMENTED, noteableType);
-export const findCommentedSnippet = (resourceParentType) => () =>
- events.find(
- (event) =>
- event.action === EVENT_TYPE_COMMENTED &&
- event.noteable?.type === SNIPPET_NOTEABLE_TYPE &&
- event.resource_parent?.type === resourceParentType,
- );
-
export const eventCommentedIssue = findCommentedEvent(ISSUE_NOTEABLE_TYPE);
export const eventCommentedMergeRequest = findCommentedEvent(MERGE_REQUEST_NOTEABLE_TYPE);
export const eventCommentedSnippet = findCommentedEvent(SNIPPET_NOTEABLE_TYPE);
@@ -153,3 +153,12 @@ export const eventCommentedCommit = () => ({
first_line_in_markdown: '\u003cp\u003eMy title 9\u003c/p\u003e',
},
});
+
+export const eventUpdated = findEventByAction(EVENT_TYPE_UPDATED);
+export const eventDesignUpdated = findUpdatedEvent(TARGET_TYPE_DESIGN);
+export const eventWikiPageUpdated = findUpdatedEvent(TARGET_TYPE_WIKI);
+
+export const eventDestroyed = findEventByAction(EVENT_TYPE_DESTROYED);
+export const eventDesignDestroyed = findDestroyedEvent(TARGET_TYPE_DESIGN);
+export const eventWikiPageDestroyed = findDestroyedEvent(TARGET_TYPE_WIKI);
+export const eventMilestoneDestroyed = findDestroyedEvent(TARGET_TYPE_MILESTONE);
diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
index 5cfb4702be7..8b76a627c1e 100644
--- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
+++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Contributors charts should render charts and a RefSelector when loading completed and there is chart data 1`] = `
<div>
<div
- class="gl-border-b gl-border-gray-100 gl-mb-6 gl-bg-gray-10 gl-p-5"
+ class="gl-bg-gray-10 gl-border-b gl-border-gray-100 gl-mb-6 gl-p-5"
>
<div
class="gl-display-flex"
@@ -20,26 +20,19 @@ exports[`Contributors charts should render charts and a RefSelector when loading
value="main"
/>
</div>
-
<a
class="btn btn-default btn-md gl-button"
data-testid="history-button"
href="some/path"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
History
-
</span>
</a>
</div>
</div>
-
<div
data-testid="contributors-charts"
>
@@ -48,11 +41,9 @@ exports[`Contributors charts should render charts and a RefSelector when loading
>
Commits to main
</h4>
-
<span>
Excluding merge commits. Limited to 6,000 commits.
</span>
-
<glareachart-stub
annotations=""
class="gl-mb-5"
@@ -70,27 +61,22 @@ exports[`Contributors charts should render charts and a RefSelector when loading
thresholds=""
width="auto"
/>
-
<div
class="row"
>
<div
- class="col-lg-6 col-12 gl-my-5"
+ class="col-12 col-lg-6 gl-my-5"
>
<h4
class="gl-mb-2 gl-mt-0"
>
John
</h4>
-
<p
class="gl-mb-3"
>
-
- 2 commits (jawnnypoo@gmail.com)
-
+ 2 commits (jawnnypoo@gmail.com)
</p>
-
<glareachart-stub
annotations=""
data="[object Object]"
diff --git a/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap b/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap
index 4e87d4d8192..c69547deb1c 100644
--- a/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/custom_emoji/components/__snapshots__/list_spec.js.snap
@@ -3,14 +3,11 @@
exports[`Custom emoji settings list component renders table of custom emoji 1`] = `
<div>
<div
- class="tabs gl-tabs"
+ class="gl-tabs tabs"
>
- <!---->
- <div
- class=""
- >
+ <div>
<ul
- class="nav gl-tabs-nav"
+ class="gl-tabs-nav nav"
role="tablist"
>
<div
@@ -23,22 +20,12 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
href="/new"
to="/new"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- New custom emoji
-
+ New custom emoji
</span>
</a>
-
- <!---->
-
- <!---->
</div>
<div
class="gl-actions-tabs-end"
@@ -50,30 +37,19 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
href="/new"
to="/new"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
-
- New custom emoji
-
+ New custom emoji
</span>
</a>
-
- <!---->
-
- <!---->
</div>
</ul>
</div>
<div
- class="tab-content gl-pt-0 gl-tab-content"
+ class="gl-pt-0 gl-tab-content tab-content"
>
<transition-stub
- css="true"
enteractiveclass=""
enterclass=""
entertoclass="show"
@@ -89,14 +65,12 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
role="tabpanel"
style="display: none;"
>
-
<table
aria-busy=""
aria-colcount="4"
- class="table b-table gl-table gl-table-layout-fixed"
+ class="b-table gl-table gl-table-layout-fixed table"
role="table"
>
- <!---->
<colgroup>
<col
style="width: 70px;"
@@ -110,12 +84,9 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
/>
</colgroup>
<thead
- class=""
role="rowgroup"
>
- <!---->
<tr
- class=""
role="row"
>
<th
@@ -162,9 +133,7 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
<tbody
role="rowgroup"
>
- <!---->
<tr
- class=""
role="row"
>
<td
@@ -180,7 +149,7 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
</td>
<td
aria-colindex="2"
- class="gl-vertical-align-middle! gl-font-monospace"
+ class="gl-font-monospace gl-vertical-align-middle!"
role="cell"
>
<strong
@@ -194,26 +163,17 @@ exports[`Custom emoji settings list component renders table of custom emoji 1`]
class="gl-vertical-align-middle!"
role="cell"
>
-
- created-at
-
+ created-at
</td>
<td
aria-colindex="4"
- class=""
role="cell"
/>
</tr>
- <!---->
- <!---->
</tbody>
- <!---->
</table>
-
- <!---->
</div>
</transition-stub>
- <!---->
</div>
</div>
</div>
diff --git a/spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap
index 560533891c9..8560b80ac9c 100644
--- a/spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap
+++ b/spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap
@@ -2,17 +2,16 @@
exports[`Design management design presentation component currentCommentForm is equal to current annotation position when isAnnotating is true 1`] = `
<div
- class="gl-h-full gl-w-full gl-p-5 overflow-auto gl-relative"
+ class="gl-h-full gl-p-5 gl-relative gl-w-full overflow-auto"
>
<div
- class="gl-h-full gl-w-full gl-display-flex gl-align-items-center gl-relative"
+ class="gl-align-items-center gl-display-flex gl-h-full gl-relative gl-w-full"
>
<design-image-stub
image="test.jpg"
name="test"
scale="1"
/>
-
<design-overlay-stub
currentcommentform="[object Object]"
dimensions="[object Object]"
@@ -25,17 +24,16 @@ exports[`Design management design presentation component currentCommentForm is e
exports[`Design management design presentation component currentCommentForm is null when isAnnotating is false 1`] = `
<div
- class="gl-h-full gl-w-full gl-p-5 overflow-auto gl-relative"
+ class="gl-h-full gl-p-5 gl-relative gl-w-full overflow-auto"
>
<div
- class="gl-h-full gl-w-full gl-display-flex gl-align-items-center gl-relative"
+ class="gl-align-items-center gl-display-flex gl-h-full gl-relative gl-w-full"
>
<design-image-stub
image="test.jpg"
name="test"
scale="1"
/>
-
<design-overlay-stub
dimensions="[object Object]"
notes=""
@@ -47,17 +45,16 @@ exports[`Design management design presentation component currentCommentForm is n
exports[`Design management design presentation component currentCommentForm is null when isAnnotating is true but annotation position is falsey 1`] = `
<div
- class="gl-h-full gl-w-full gl-p-5 overflow-auto gl-relative"
+ class="gl-h-full gl-p-5 gl-relative gl-w-full overflow-auto"
>
<div
- class="gl-h-full gl-w-full gl-display-flex gl-align-items-center gl-relative"
+ class="gl-align-items-center gl-display-flex gl-h-full gl-relative gl-w-full"
>
<design-image-stub
image="test.jpg"
name="test"
scale="1"
/>
-
<design-overlay-stub
dimensions="[object Object]"
notes=""
@@ -69,31 +66,26 @@ exports[`Design management design presentation component currentCommentForm is n
exports[`Design management design presentation component renders empty state when no image provided 1`] = `
<div
- class="gl-h-full gl-w-full gl-p-5 overflow-auto gl-relative"
+ class="gl-h-full gl-p-5 gl-relative gl-w-full overflow-auto"
>
<div
- class="gl-h-full gl-w-full gl-display-flex gl-align-items-center gl-relative"
- >
- <!---->
-
- <!---->
- </div>
+ class="gl-align-items-center gl-display-flex gl-h-full gl-relative gl-w-full"
+ />
</div>
`;
exports[`Design management design presentation component renders image and overlay when image provided 1`] = `
<div
- class="gl-h-full gl-w-full gl-p-5 overflow-auto gl-relative"
+ class="gl-h-full gl-p-5 gl-relative gl-w-full overflow-auto"
>
<div
- class="gl-h-full gl-w-full gl-display-flex gl-align-items-center gl-relative"
+ class="gl-align-items-center gl-display-flex gl-h-full gl-relative gl-w-full"
>
<design-image-stub
image="test.jpg"
name="test"
scale="1"
/>
-
<design-overlay-stub
dimensions="[object Object]"
notes=""
diff --git a/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
index 1f4e579f075..573ad5f872f 100644
--- a/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
+++ b/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
@@ -4,11 +4,9 @@ exports[`Design management large image component renders SVG with proper height
<div
class="gl-mx-auto gl-my-auto js-design-image"
>
- <!---->
-
<img
alt="test"
- class="mh-100 img-fluid"
+ class="img-fluid mh-100"
src="mockImage.svg"
/>
</div>
@@ -18,11 +16,9 @@ exports[`Design management large image component renders image 1`] = `
<div
class="gl-mx-auto gl-my-auto js-design-image"
>
- <!---->
-
<img
alt="test"
- class="mh-100 img-fluid"
+ class="img-fluid mh-100"
src="test.jpg"
/>
</div>
@@ -33,12 +29,10 @@ exports[`Design management large image component renders loading state 1`] = `
class="gl-mx-auto gl-my-auto js-design-image"
isloading="true"
>
- <!---->
-
<img
alt=""
- class="mh-100 img-fluid"
- src=""
+ class="img-fluid mh-100"
+ src="null"
/>
</div>
`;
@@ -55,8 +49,6 @@ exports[`Design management large image component sets correct classes and styles
<div
class="gl-mx-auto gl-my-auto js-design-image"
>
- <!---->
-
<img
alt="test"
class="mh-100"
@@ -70,8 +62,6 @@ exports[`Design management large image component zoom sets image style when zoom
<div
class="gl-mx-auto gl-my-auto js-design-image"
>
- <!---->
-
<img
alt="test"
class="mh-100"
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
index ab37cb90bd3..8bc9bce7e80 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_signed_out_spec.js.snap
@@ -4,19 +4,19 @@ exports[`DesignNoteSignedOut renders message containing register and sign-in lin
<div
class="disabled-comment text-center"
>
- Please
+ Please
<gl-link-stub
href="/users/sign_up?redirect_to_referer=yes"
>
register
</gl-link-stub>
- or
+ or
<gl-link-stub
href="/users/sign_in?redirect_to_referer=yes"
>
sign in
</gl-link-stub>
- to reply.
+ to reply.
</div>
`;
@@ -24,18 +24,18 @@ exports[`DesignNoteSignedOut renders message containing register and sign-in lin
<div
class="disabled-comment text-center"
>
- Please
+ Please
<gl-link-stub
href="/users/sign_up?redirect_to_referer=yes"
>
register
</gl-link-stub>
- or
+ or
<gl-link-stub
href="/users/sign_in?redirect_to_referer=yes"
>
sign in
</gl-link-stub>
- to start a new discussion.
+ to start a new discussion.
</div>
`;
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
index 4dc8eaea174..206187c3530 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
@@ -1,17 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = `
-"<button data-track-action=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn gl-mr-3 gl-w-auto! btn-confirm btn-md disabled gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">
- Comment
- </span></button>"
+<button
+ class="btn btn-confirm btn-md disabled gl-button gl-mr-3 gl-w-auto!"
+ data-qa-selector="save_comment_button"
+ data-track-action="click_button"
+ disabled=""
+ type="submit"
+>
+ <span
+ class="gl-button-text"
+ >
+ Comment
+ </span>
+</button>
`;
exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = `
-"<button data-track-action=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn gl-mr-3 gl-w-auto! btn-confirm btn-md disabled gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">
- Save comment
- </span></button>"
+<button
+ class="btn btn-confirm btn-md disabled gl-button gl-mr-3 gl-w-auto!"
+ data-qa-selector="save_comment_button"
+ data-track-action="click_button"
+ disabled=""
+ type="submit"
+>
+ <span
+ class="gl-button-text"
+ >
+ Save comment
+ </span>
+</button>
`;
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 0bbb44bb517..53359b02b4c 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -9,36 +9,27 @@ exports[`Design management list item component when item appears in view after i
`;
exports[`Design management list item component with notes renders item with multiple comments 1`] = `
-<router-link-stub
- ariacurrentvalue="page"
- class="card gl-cursor-pointer text-plain js-design-list-item design-list-item gl-mb-0"
- event="click"
- tag="a"
- to="[object Object]"
+<a
+ class="card design-list-item gl-cursor-pointer gl-mb-0 js-design-list-item text-plain"
>
<div
- class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative gl-rounded-top-base"
+ class="card-body gl-align-items-center gl-display-flex gl-justify-content-center gl-overflow-hidden gl-p-0 gl-relative gl-rounded-top-base"
>
- <!---->
-
<gl-intersection-observer-stub
class="gl-flex-grow-1"
>
- <!---->
-
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
+ class="design-img gl-display-block gl-max-h-full gl-max-w-full gl-mx-auto gl-w-auto"
data-qa-filename="test"
data-qa-selector="design_image"
data-testid="design-img-1"
- src=""
+ src="null"
/>
</gl-intersection-observer-stub>
</div>
-
<div
- class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4"
+ class="card-footer gl-bg-white gl-display-flex gl-px-4 gl-py-3 gl-w-full"
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
@@ -51,12 +42,10 @@ exports[`Design management list item component with notes renders item with mult
>
test
</span>
-
<span
class="str-truncated-100"
>
-
- Updated
+ Updated
<timeago-stub
cssclass=""
datetimeformat="DATE_WITH_TIME_FORMAT"
@@ -65,60 +54,47 @@ exports[`Design management list item component with notes renders item with mult
/>
</span>
</div>
-
<div
- class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
+ class="gl-align-items-center gl-display-flex gl-ml-auto gl-text-gray-500"
>
<gl-icon-stub
class="gl-ml-2"
name="comments"
size="16"
/>
-
<span
aria-label="2 comments"
class="gl-ml-2"
>
-
2
-
</span>
</div>
</div>
-</router-link-stub>
+</a>
`;
exports[`Design management list item component with notes renders item with single comment 1`] = `
-<router-link-stub
- ariacurrentvalue="page"
- class="card gl-cursor-pointer text-plain js-design-list-item design-list-item gl-mb-0"
- event="click"
- tag="a"
- to="[object Object]"
+<a
+ class="card design-list-item gl-cursor-pointer gl-mb-0 js-design-list-item text-plain"
>
<div
- class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative gl-rounded-top-base"
+ class="card-body gl-align-items-center gl-display-flex gl-justify-content-center gl-overflow-hidden gl-p-0 gl-relative gl-rounded-top-base"
>
- <!---->
-
<gl-intersection-observer-stub
class="gl-flex-grow-1"
>
- <!---->
-
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
+ class="design-img gl-display-block gl-max-h-full gl-max-w-full gl-mx-auto gl-w-auto"
data-qa-filename="test"
data-qa-selector="design_image"
data-testid="design-img-1"
- src=""
+ src="null"
/>
</gl-intersection-observer-stub>
</div>
-
<div
- class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4"
+ class="card-footer gl-bg-white gl-display-flex gl-px-4 gl-py-3 gl-w-full"
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
@@ -131,12 +107,10 @@ exports[`Design management list item component with notes renders item with sing
>
test
</span>
-
<span
class="str-truncated-100"
>
-
- Updated
+ Updated
<timeago-stub
cssclass=""
datetimeformat="DATE_WITH_TIME_FORMAT"
@@ -145,25 +119,21 @@ exports[`Design management list item component with notes renders item with sing
/>
</span>
</div>
-
<div
- class="gl-ml-auto gl-display-flex gl-align-items-center gl-text-gray-500"
+ class="gl-align-items-center gl-display-flex gl-ml-auto gl-text-gray-500"
>
<gl-icon-stub
class="gl-ml-2"
name="comments"
size="16"
/>
-
<span
aria-label="1 comment"
class="gl-ml-2"
>
-
1
-
</span>
</div>
</div>
-</router-link-stub>
+</a>
`;
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index 4a0ad5a045b..14e8a5579ba 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -1,5 +1,5 @@
import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueRouter from 'vue-router';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -49,7 +49,7 @@ describe('Design management list item component', () => {
imageLoading,
};
},
- stubs: ['router-link'],
+ stubs: { RouterLink: RouterLinkStub },
}),
);
}
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
index 3c4aa0f4d3c..cf8aac22f67 100644
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
@@ -2,14 +2,14 @@
exports[`Design management toolbar component renders design and updated data 1`] = `
<header
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-bg-white gl-py-4 gl-pl-4 js-design-header"
+ class="gl-align-items-center gl-bg-white gl-display-flex gl-justify-content-space-between gl-pl-4 gl-py-4 js-design-header"
>
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<a
aria-label="Go back to designs"
- class="gl-mr-5 gl-display-flex gl-align-items-center gl-justify-content-center text-plain"
+ class="gl-align-items-center gl-display-flex gl-justify-content-center gl-mr-5 text-plain"
data-testid="close-design"
>
<gl-icon-stub
@@ -17,16 +17,14 @@ exports[`Design management toolbar component renders design and updated data 1`]
size="16"
/>
</a>
-
<div
- class="gl-overflow-hidden gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex gl-overflow-hidden"
>
<h2
- class="gl-m-0 str-truncated-100 gl-font-base"
+ class="gl-font-base gl-m-0 str-truncated-100"
>
test.jpg
</h2>
-
<small
class="gl-text-gray-500"
>
@@ -34,12 +32,10 @@ exports[`Design management toolbar component renders design and updated data 1`]
</small>
</div>
</div>
-
<design-navigation-stub
- class="gl-ml-auto gl-flex-shrink-0"
- id="1"
+ class="gl-flex-shrink-0 gl-ml-auto"
+ id="reference-0"
/>
-
<gl-button-stub
aria-label="Download design"
buttontextclasses=""
@@ -50,7 +46,6 @@ exports[`Design management toolbar component renders design and updated data 1`]
title="Download design"
variant="default"
/>
-
<delete-button-stub
buttoncategory="secondary"
buttonclass=""
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
index 191bcc2d484..e6a74c49d50 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
@@ -12,15 +12,12 @@ exports[`Design management upload button component renders inverted upload desig
title="Adding a design with the same filename replaces the file in a new version."
variant="confirm"
>
-
Upload designs
-
</gl-button-stub>
-
<input
accept="image/*"
class="gl-display-none"
- multiple="multiple"
+ multiple=""
name="design_file"
type="file"
/>
@@ -37,15 +34,12 @@ exports[`Design management upload button component renders upload design button
title="Adding a design with the same filename replaces the file in a new version."
variant="confirm"
>
-
Upload designs
-
</gl-button-stub>
-
<input
accept="image/*"
class="gl-display-none"
- multiple="multiple"
+ multiple=""
name="design_file"
type="file"
/>
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index f0615f61059..224e35e9f5e 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -2,86 +2,70 @@
exports[`Design management design index page renders design index 1`] = `
<div
- class="design-detail js-design-detail fixed-top gl-w-full gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
+ class="design-detail fixed-top gl-display-flex gl-flex-direction-column gl-justify-content-center gl-lg-flex-direction-row gl-w-full js-design-detail"
>
<div
- class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
+ class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden gl-relative"
>
<div
iid="1"
project-path="project-path"
/>
-
- <!---->
-
<design-presentation-stub
discussions="[object Object],[object Object]"
image="test.jpg"
imagename="test.jpg"
scale="1"
/>
-
<div
- class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
+ class="design-scaler-wrapper gl-absolute gl-align-items-center gl-display-flex gl-justify-content-center gl-mb-6"
>
<design-scaler-stub
maxscale="2"
/>
</div>
</div>
-
<div
- class="image-notes gl-pt-0"
+ class="gl-pt-0 image-notes"
>
<div
- class="gl-py-4 gl-mb-4 gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ class="gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-display-flex gl-justify-content-space-between gl-mb-4 gl-py-4"
>
<span>
To Do
</span>
-
<design-todo-button-stub
design="[object Object]"
/>
</div>
-
<h2
class="gl-font-weight-bold gl-mt-0"
>
-
- My precious issue
-
+ My precious issue
</h2>
-
<a
- class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
+ class="gl-display-block gl-mb-6 gl-text-decoration-none gl-text-gray-400"
href="full-issue-url"
>
ull-issue-path
</a>
-
<description-form-stub
design="[object Object]"
designvariables="[object Object]"
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
/>
-
<participants-stub
class="gl-mb-4"
lazy="true"
numberoflessparticipants="8"
participants="[object Object]"
/>
-
- <!---->
-
<design-note-signed-out-stub
class="gl-mb-4"
isadddiscussion="true"
registerpath=""
signinpath=""
/>
-
<design-discussion-stub
data-testid="unresolved-discussion"
designid="gid::/gitlab/Design/1"
@@ -92,7 +76,6 @@ exports[`Design management design index page renders design index 1`] = `
registerpath=""
signinpath=""
/>
-
<gl-accordion-stub
class="gl-mb-5"
headerlevel="3"
@@ -113,23 +96,21 @@ exports[`Design management design index page renders design index 1`] = `
/>
</gl-accordion-item-stub>
</gl-accordion-stub>
-
</div>
</div>
`;
exports[`Design management design index page with error GlAlert is rendered in correct position with correct content 1`] = `
<div
- class="design-detail js-design-detail fixed-top gl-w-full gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
+ class="design-detail fixed-top gl-display-flex gl-flex-direction-column gl-justify-content-center gl-lg-flex-direction-row gl-w-full js-design-detail"
>
<div
- class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
+ class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden gl-relative"
>
<div
iid="1"
project-path="project-path"
/>
-
<div
class="gl-p-5"
>
@@ -144,82 +125,64 @@ exports[`Design management design index page with error GlAlert is rendered in c
title=""
variant="danger"
>
-
woops
-
</gl-alert-stub>
</div>
-
<design-presentation-stub
discussions=""
image="test.jpg"
imagename="test.jpg"
scale="1"
/>
-
<div
- class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
+ class="design-scaler-wrapper gl-absolute gl-align-items-center gl-display-flex gl-justify-content-center gl-mb-6"
>
<design-scaler-stub
maxscale="2"
/>
</div>
</div>
-
<div
- class="image-notes gl-pt-0"
+ class="gl-pt-0 image-notes"
>
<div
- class="gl-py-4 gl-mb-4 gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ class="gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-display-flex gl-justify-content-space-between gl-mb-4 gl-py-4"
>
<span>
To Do
</span>
-
<design-todo-button-stub
design="[object Object]"
/>
</div>
-
<h2
class="gl-font-weight-bold gl-mt-0"
>
-
- My precious issue
-
+ My precious issue
</h2>
-
<a
- class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
+ class="gl-display-block gl-mb-6 gl-text-decoration-none gl-text-gray-400"
href="full-issue-url"
>
ull-issue-path
</a>
-
<description-form-stub
design="[object Object]"
designvariables="[object Object]"
markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
/>
-
<participants-stub
class="gl-mb-4"
lazy="true"
numberoflessparticipants="8"
participants="[object Object]"
/>
-
- <!---->
-
<design-note-signed-out-stub
class="gl-mb-4"
isadddiscussion="true"
registerpath=""
signinpath=""
/>
-
- <!---->
-
</div>
</div>
`;
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index c1f0966f9c6..e10aad6214c 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -11,7 +11,7 @@ import CommitWidget from '~/diffs/components/commit_widget.vue';
import CompareVersions from '~/diffs/components/compare_versions.vue';
import DiffFile from '~/diffs/components/diff_file.vue';
import NoChanges from '~/diffs/components/no_changes.vue';
-import findingsDrawer from '~/diffs/components/shared/findings_drawer.vue';
+import FindingsDrawer from '~/diffs/components/shared/findings_drawer.vue';
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
@@ -251,6 +251,11 @@ describe('diffs/components/app', () => {
await nextTick();
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
});
+
+ it('renders findings-drawer', () => {
+ createComponent();
+ expect(wrapper.findComponent(FindingsDrawer).exists()).toBe(true);
+ });
});
it('marks current diff file based on currently highlighted row', async () => {
@@ -755,20 +760,4 @@ describe('diffs/components/app', () => {
);
});
});
-
- describe('findings-drawer', () => {
- it('does not render findings-drawer when codeQualityInlineDrawer flag is off', () => {
- createComponent();
- expect(wrapper.findComponent(findingsDrawer).exists()).toBe(false);
- });
-
- it('does render findings-drawer when codeQualityInlineDrawer flag is on', () => {
- createComponent({}, () => {}, {
- glFeatures: {
- codeQualityInlineDrawer: true,
- },
- });
- expect(wrapper.findComponent(findingsDrawer).exists()).toBe(true);
- });
- });
});
diff --git a/spec/frontend/diffs/components/diff_inline_findings_item_spec.js b/spec/frontend/diffs/components/diff_inline_findings_item_spec.js
index 72d96d3435f..cda3273d51e 100644
--- a/spec/frontend/diffs/components/diff_inline_findings_item_spec.js
+++ b/spec/frontend/diffs/components/diff_inline_findings_item_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlLink } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffInlineFindingsItem from '~/diffs/components/diff_inline_findings_item.vue';
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
@@ -8,19 +8,13 @@ let wrapper;
const [codeQualityFinding] = multipleFindingsArrCodeQualityScale;
const findIcon = () => wrapper.findComponent(GlIcon);
-const findButton = () => wrapper.findComponent(GlLink);
const findDescriptionPlainText = () => wrapper.findByTestId('description-plain-text');
-const findDescriptionLinkSection = () => wrapper.findByTestId('description-button-section');
describe('DiffCodeQuality', () => {
- const createWrapper = ({ glFeatures = {}, link = true } = {}) => {
+ const createWrapper = () => {
return shallowMountExtended(DiffInlineFindingsItem, {
propsData: {
finding: codeQualityFinding,
- link,
- },
- provide: {
- glFeatures,
},
});
};
@@ -36,42 +30,9 @@ describe('DiffCodeQuality', () => {
});
});
- describe('with codeQualityInlineDrawer flag false', () => {
- it('should render severity + description in plain text', () => {
- wrapper = createWrapper({
- glFeatures: {
- codeQualityInlineDrawer: false,
- },
- });
- expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.severity);
- expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.description);
- });
- });
-
- describe('with codeQualityInlineDrawer flag true', () => {
- const [{ description, severity }] = multipleFindingsArrCodeQualityScale;
- const renderedText = `${severity} - ${description}`;
- it('when link prop is true, should render gl-link', () => {
- wrapper = createWrapper({
- glFeatures: {
- codeQualityInlineDrawer: true,
- },
- });
-
- expect(findButton().exists()).toBe(true);
- expect(findButton().text()).toBe(renderedText);
- });
-
- it('when link prop is false, should not render gl-link', () => {
- wrapper = createWrapper({
- glFeatures: {
- codeQualityInlineDrawer: true,
- },
- link: false,
- });
-
- expect(findButton().exists()).toBe(false);
- expect(findDescriptionLinkSection().text()).toBe(renderedText);
- });
+ it('should render severity + description in plain text', () => {
+ wrapper = createWrapper();
+ expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.severity);
+ expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.description);
});
});
diff --git a/spec/frontend/diffs/components/diff_inline_findings_spec.js b/spec/frontend/diffs/components/diff_inline_findings_spec.js
index 65b2abe7dd5..f654a2e2d4f 100644
--- a/spec/frontend/diffs/components/diff_inline_findings_spec.js
+++ b/spec/frontend/diffs/components/diff_inline_findings_spec.js
@@ -2,7 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
import DiffInlineFindingsItem from '~/diffs/components/diff_inline_findings_item.vue';
import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
-import { multipleCodeQualityNoSast } from '../mock_data/inline_findings';
+import { multipleFindingsArrCodeQualityScale } from '../mock_data/inline_findings';
let wrapper;
const heading = () => wrapper.findByTestId('diff-inline-findings-heading');
@@ -13,7 +13,7 @@ describe('DiffInlineFindings', () => {
return shallowMountExtended(DiffInlineFindings, {
propsData: {
title: NEW_CODE_QUALITY_FINDINGS,
- findings: multipleCodeQualityNoSast.codeQuality,
+ findings: multipleFindingsArrCodeQualityScale,
},
});
};
@@ -25,7 +25,7 @@ describe('DiffInlineFindings', () => {
it('renders the correct number of DiffInlineFindingsItem components with correct props', () => {
wrapper = createWrapper();
- expect(diffInlineFindingsItems()).toHaveLength(multipleCodeQualityNoSast.codeQuality.length);
+ expect(diffInlineFindingsItems()).toHaveLength(multipleFindingsArrCodeQualityScale.length);
expect(diffInlineFindingsItems().wrappers[0].props('finding')).toEqual(
wrapper.props('findings')[0],
);
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index 8a67d7b152c..30510958704 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -71,7 +71,8 @@ describe('DiffRow', () => {
const hits = coverageFileData[file]?.[line];
if (hits) {
return { text: `Test coverage: ${hits} hits`, class: 'coverage' };
- } else if (hits === 0) {
+ }
+ if (hits === 0) {
return { text: 'No test coverage', class: 'no-coverage' };
}
diff --git a/spec/frontend/diffs/components/inline_findings_spec.js b/spec/frontend/diffs/components/inline_findings_spec.js
index 71cc6ae49fd..102287a23b6 100644
--- a/spec/frontend/diffs/components/inline_findings_spec.js
+++ b/spec/frontend/diffs/components/inline_findings_spec.js
@@ -2,7 +2,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import InlineFindings from '~/diffs/components/inline_findings.vue';
import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
-import { threeCodeQualityFindingsRaw } from '../mock_data/inline_findings';
+import { threeCodeQualityFindings } from '../mock_data/inline_findings';
let wrapper;
@@ -12,7 +12,7 @@ describe('InlineFindings', () => {
const createWrapper = () => {
return mountExtended(InlineFindings, {
propsData: {
- codeQuality: threeCodeQualityFindingsRaw,
+ codeQuality: threeCodeQualityFindings,
},
});
};
@@ -28,6 +28,6 @@ describe('InlineFindings', () => {
it('renders diff inline findings component with correct props for codequality array', () => {
wrapper = createWrapper();
expect(diffInlineFindings().props('title')).toBe(NEW_CODE_QUALITY_FINDINGS);
- expect(diffInlineFindings().props('findings')).toBe(threeCodeQualityFindingsRaw);
+ expect(diffInlineFindings().props('findings')).toBe(threeCodeQualityFindings);
});
});
diff --git a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
index 51bd8f380ee..afa2a7d9678 100644
--- a/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
+++ b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
@@ -9,15 +9,13 @@ exports[`FindingsDrawer matches the snapshot 1`] = `
zindex="252"
>
<h2
- class="gl-font-size-h2 gl-mt-0 gl-mb-0"
+ class="gl-font-size-h2 gl-mb-0 gl-mt-0"
data-testid="findings-drawer-heading"
>
-
- Unused method argument - \`c\`. If it's necessary, use \`_\` or \`_c\` as an argument name to indicate that it won't be used.
-
+ Unused method argument - \`c\`. If it's necessary, use \`_\` or \`_c\` as an argument name to indicate that it won't be used.
</h2>
<ul
- class="gl-list-style-none gl-border-b-initial gl-mb-0 gl-pb-0!"
+ class="gl-border-b-initial gl-list-style-none gl-mb-0 gl-pb-0!"
>
<li
class="gl-mb-4"
@@ -28,19 +26,14 @@ exports[`FindingsDrawer matches the snapshot 1`] = `
>
Severity:
</span>
-
<gl-icon-stub
- class="inline-findings-severity-icon gl-text-orange-300"
+ class="gl-text-orange-300 inline-findings-severity-icon"
data-testid="findings-drawer-severity-icon"
name="severity-low"
size="12"
/>
-
-
- minor
-
+ minor
</li>
-
<li
class="gl-mb-4"
data-testid="findings-drawer-engine"
@@ -50,11 +43,8 @@ exports[`FindingsDrawer matches the snapshot 1`] = `
>
Engine:
</span>
-
- testengine name
-
+ testengine name
</li>
-
<li
class="gl-mb-4"
data-testid="findings-drawer-category"
@@ -64,21 +54,17 @@ exports[`FindingsDrawer matches the snapshot 1`] = `
>
Category:
</span>
-
- testcategory 1
-
+ testcategory 1
</li>
-
<li
class="gl-mb-4"
data-testid="findings-drawer-other-locations"
>
<span
- class="gl-font-weight-bold gl-mb-3 gl-display-block"
+ class="gl-display-block gl-font-weight-bold gl-mb-3"
>
Other locations:
</span>
-
<ul
class="gl-pl-6"
>
@@ -115,7 +101,6 @@ exports[`FindingsDrawer matches the snapshot 1`] = `
</ul>
</li>
</ul>
-
<span
class="drawer-body gl-display-block gl-px-3 gl-py-0!"
data-testid="findings-drawer-body"
diff --git a/spec/frontend/diffs/mock_data/inline_findings.js b/spec/frontend/diffs/mock_data/inline_findings.js
index 85fb48b86d5..ae1ae909238 100644
--- a/spec/frontend/diffs/mock_data/inline_findings.js
+++ b/spec/frontend/diffs/mock_data/inline_findings.js
@@ -4,6 +4,7 @@ export const multipleFindingsArrCodeQualityScale = [
description: 'mocked minor Issue',
line: 2,
scale: 'codeQuality',
+ text: 'mocked minor Issue',
},
{
severity: 'major',
@@ -43,6 +44,7 @@ export const multipleFindingsArrSastScale = [
description: 'mocked low Issue',
line: 2,
scale: 'sast',
+ text: 'mocked low Issue',
},
{
severity: 'medium',
@@ -76,48 +78,6 @@ export const multipleFindingsArrSastScale = [
},
];
-export const multipleCodeQualityNoSast = {
- codeQuality: multipleFindingsArrCodeQualityScale,
- sast: [],
-};
-
-export const multipleSastNoCodeQuality = {
- codeQuality: [],
- sast: multipleFindingsArrSastScale,
-};
-
-export const fiveCodeQualityFindings = {
- filePath: 'index.js',
- codequality: multipleFindingsArrCodeQualityScale.slice(0, 5),
-};
-
-export const threeCodeQualityFindings = {
- filePath: 'index.js',
- codequality: multipleFindingsArrCodeQualityScale.slice(0, 3),
-};
-export const threeCodeQualityFindingsRaw = [multipleFindingsArrCodeQualityScale.slice(0, 3)];
-
-export const singularCodeQualityFinding = {
- filePath: 'index.js',
- codequality: [multipleFindingsArrCodeQualityScale[0]],
-};
-
-export const singularFindingSast = {
- filePath: 'index.js',
- sast: [multipleFindingsArrSastScale[0]],
-};
-
-export const threeSastFindings = {
- filePath: 'index.js',
- sast: multipleFindingsArrSastScale.slice(0, 3),
-};
-
-export const oneCodeQualityTwoSastFindings = {
- filePath: 'index.js',
- sast: multipleFindingsArrSastScale.slice(0, 2),
- codequality: [multipleFindingsArrCodeQualityScale[0]],
-};
-
export const diffCodeQuality = {
diffFile: { file_hash: '123' },
diffLines: [
@@ -151,3 +111,19 @@ export const diffCodeQuality = {
},
],
};
+
+export const singularCodeQualityFinding = [multipleFindingsArrCodeQualityScale[0]];
+export const singularSastFinding = [multipleFindingsArrSastScale[0]];
+export const twoSastFindings = multipleFindingsArrSastScale.slice(0, 2);
+export const fiveCodeQualityFindings = multipleFindingsArrCodeQualityScale.slice(0, 5);
+export const threeCodeQualityFindings = multipleFindingsArrCodeQualityScale.slice(0, 3);
+
+export const filePath = 'testPath';
+export const scale = 'exampleScale';
+
+export const dropdownIcon = {
+ id: 'noise.rb-2',
+ key: 'mockedkey',
+ name: 'severity-medium',
+ class: 'gl-text-orange-400',
+};
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js
index 5a77b9d4689..0b863edc13b 100644
--- a/spec/frontend/drawio/drawio_editor_spec.js
+++ b/spec/frontend/drawio/drawio_editor_spec.js
@@ -5,6 +5,7 @@ import {
DRAWIO_IFRAME_TIMEOUT,
DIAGRAM_MAX_SIZE,
} from '~/drawio/constants';
+import { base64EncodeUnicode } from '~/lib/utils/text_utility';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
const DRAWIO_EDITOR_URL =
@@ -19,8 +20,8 @@ describe('drawio/drawio_editor', () => {
let editorFacade;
let drawioIFrameReceivedMessages;
const diagramURL = `${window.location.origin}/uploads/diagram.drawio.svg`;
- const testSvg = '<svg></svg>';
- const testEncodedSvg = `data:image/svg+xml;base64,${btoa(testSvg)}`;
+ const testSvg = '<svg>😀</svg>';
+ const testEncodedSvg = `data:image/svg+xml;base64,${base64EncodeUnicode(testSvg)}`;
const filename = 'diagram.drawio.svg';
const findDrawioIframe = () => document.getElementById(DRAWIO_FRAME_ID);
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
index 909911debf1..3076105ffde 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/include.yml
@@ -33,6 +33,17 @@ include:
rules:
- exists:
- file.md
+ - local: builds.yml
+ rules:
+ - if: $INCLUDE_BUILDS == "true"
+ changes:
+ - 'test.yml'
+ - local: builds.yml
+ rules:
+ - changes:
+ paths:
+ - 'test.yml'
+ compare_to: 'master'
# valid trigger:include
trigger:include accepts project and file properties:
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index 1b948cce73a..1a12bd303f1 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -134,9 +134,11 @@ describe('emoji', () => {
const emojiKey = 'bomb';
const markup = glEmojiTag(emojiKey);
- expect(trimText(markup)).toMatchInlineSnapshot(
- `"<gl-emoji data-name=\\"bomb\\"></gl-emoji>"`,
- );
+ expect(trimText(markup)).toMatchInlineSnapshot(`
+ <gl-emoji
+ data-name="bomb"
+ />
+ `);
});
it('bomb emoji with sprite fallback readiness', () => {
@@ -144,9 +146,12 @@ describe('emoji', () => {
const markup = glEmojiTag(emojiKey, {
sprite: true,
});
- expect(trimText(markup)).toMatchInlineSnapshot(
- `"<gl-emoji data-fallback-sprite-class=\\"emoji-bomb\\" data-name=\\"bomb\\"></gl-emoji>"`,
- );
+ expect(trimText(markup)).toMatchInlineSnapshot(`
+ <gl-emoji
+ data-fallback-sprite-class="emoji-bomb"
+ data-name="bomb"
+ />
+ `);
});
});
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index b55bbb34c65..9989c946800 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -7,7 +7,6 @@ import EditEnvironment from '~/environments/components/edit_environment.vue';
import { createAlert } from '~/alert';
import { visitUrl } from '~/lib/utils/url_utility';
import getEnvironment from '~/environments/graphql/queries/environment.query.graphql';
-import getEnvironmentWithFluxResource from '~/environments/graphql/queries/environment_with_flux_resource.query.graphql';
import updateEnvironment from '~/environments/graphql/mutations/update_environment.mutation.graphql';
import { __ } from '~/locale';
import createMockApollo from '../__helpers__/mock_apollo_helper';
@@ -44,9 +43,6 @@ describe('~/environments/components/edit.vue', () => {
let wrapper;
const getEnvironmentQuery = jest.fn().mockResolvedValue({ data: resolvedEnvironment });
- const getEnvironmentWithFluxResourceQuery = jest
- .fn()
- .mockResolvedValue({ data: resolvedEnvironment });
const updateEnvironmentSuccess = jest
.fn()
@@ -60,24 +56,17 @@ describe('~/environments/components/edit.vue', () => {
const mocks = [
[getEnvironment, getEnvironmentQuery],
- [getEnvironmentWithFluxResource, getEnvironmentWithFluxResourceQuery],
[updateEnvironment, mutationHandler],
];
return createMockApollo(mocks);
};
- const createWrapperWithApollo = async ({
- mutationHandler = updateEnvironmentSuccess,
- fluxResourceForEnvironment = false,
- } = {}) => {
+ const createWrapperWithApollo = async ({ mutationHandler = updateEnvironmentSuccess } = {}) => {
wrapper = mountExtended(EditEnvironment, {
propsData: { environment: {} },
provide: {
...provide,
- glFeatures: {
- fluxResourceForEnvironment,
- },
},
apolloProvider: createMockApolloProvider(mutationHandler),
});
@@ -170,11 +159,4 @@ describe('~/environments/components/edit.vue', () => {
});
});
});
-
- describe('when `fluxResourceForEnvironment` is enabled', () => {
- it('calls the `getEnvironmentWithFluxResource` query', () => {
- createWrapperWithApollo({ fluxResourceForEnvironment: true });
- expect(getEnvironmentWithFluxResourceQuery).toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index 1b80b596db7..22dd7437d82 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -53,11 +53,7 @@ describe('~/environments/components/form.vue', () => {
},
});
- const createWrapperWithApollo = ({
- propsData = {},
- fluxResourceForEnvironment = false,
- queryResult = null,
- } = {}) => {
+ const createWrapperWithApollo = ({ propsData = {}, queryResult = null } = {}) => {
Vue.use(VueApollo);
const requestHandlers = [
@@ -83,9 +79,6 @@ describe('~/environments/components/form.vue', () => {
return mountExtended(EnvironmentForm, {
provide: {
...PROVIDE,
- glFeatures: {
- fluxResourceForEnvironment,
- },
},
propsData: {
...DEFAULT_PROPS,
@@ -422,39 +415,30 @@ describe('~/environments/components/form.vue', () => {
});
describe('flux resource selector', () => {
- it("doesn't render if `fluxResourceForEnvironment` feature flag is disabled", () => {
+ beforeEach(() => {
wrapper = createWrapperWithApollo();
+ });
+
+ it("doesn't render flux resource selector by default", () => {
expect(findFluxResourceSelector().exists()).toBe(false);
});
- describe('when `fluxResourceForEnvironment` feature flag is enabled', () => {
- beforeEach(() => {
- wrapper = createWrapperWithApollo({
- fluxResourceForEnvironment: true,
- });
+ describe('when the agent was selected', () => {
+ beforeEach(async () => {
+ await selectAgent();
});
- it("doesn't render flux resource selector by default", () => {
+ it("doesn't render flux resource selector", () => {
expect(findFluxResourceSelector().exists()).toBe(false);
});
- describe('when the agent was selected', () => {
- beforeEach(async () => {
- await selectAgent();
- });
-
- it("doesn't render flux resource selector", () => {
- expect(findFluxResourceSelector().exists()).toBe(false);
- });
-
- it('renders the flux resource selector when the namespace is selected', async () => {
- await findNamespaceSelector().vm.$emit('select', 'agent');
+ it('renders the flux resource selector when the namespace is selected', async () => {
+ await findNamespaceSelector().vm.$emit('select', 'agent');
- expect(findFluxResourceSelector().props()).toEqual({
- namespace: 'agent',
- fluxResourcePath: '',
- configuration,
- });
+ expect(findFluxResourceSelector().props()).toEqual({
+ namespace: 'agent',
+ fluxResourcePath: '',
+ configuration,
});
});
});
@@ -522,7 +506,6 @@ describe('~/environments/components/form.vue', () => {
beforeEach(() => {
wrapper = createWrapperWithApollo({
propsData: { environment: environmentWithAgentAndNamespace },
- fluxResourceForEnvironment: true,
});
});
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index bfcc4f4ebb6..7ee31bf2c62 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -13,7 +13,6 @@ import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import getEnvironmentClusterAgent from '~/environments/graphql/queries/environment_cluster_agent.query.graphql';
-import getEnvironmentClusterAgentWithFluxResource from '~/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql';
import {
resolvedEnvironment,
rolloutStatus,
@@ -27,7 +26,6 @@ Vue.use(VueApollo);
describe('~/environments/components/new_environment_item.vue', () => {
let wrapper;
let queryResponseHandler;
- let queryWithFluxResourceResponseHandler;
const projectPath = '/1';
@@ -39,27 +37,14 @@ describe('~/environments/components/new_environment_item.vue', () => {
environment: {
id: '1',
kubernetesNamespace: 'default',
+ fluxResourcePath: fluxResourcePathMock,
clusterAgent,
},
},
},
};
queryResponseHandler = jest.fn().mockResolvedValue(response);
- queryWithFluxResourceResponseHandler = jest.fn().mockResolvedValue({
- data: {
- project: {
- id: response.data.project.id,
- environment: {
- ...response.data.project.environment,
- fluxResourcePath: fluxResourcePathMock,
- },
- },
- },
- });
- return createMockApollo([
- [getEnvironmentClusterAgent, queryResponseHandler],
- [getEnvironmentClusterAgentWithFluxResource, queryWithFluxResourceResponseHandler],
- ]);
+ return createMockApollo([[getEnvironmentClusterAgent, queryResponseHandler]]);
};
const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) =>
@@ -554,25 +539,6 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
});
- it('should request agent data with Flux resource when `fluxResourceForEnvironment` feature flag is enabled', async () => {
- wrapper = createWrapper({
- propsData: { environment: resolvedEnvironment },
- provideData: {
- glFeatures: {
- fluxResourceForEnvironment: true,
- },
- },
- apolloProvider: createApolloProvider(agent),
- });
-
- await expandCollapsedSection();
-
- expect(queryWithFluxResourceResponseHandler).toHaveBeenCalledWith({
- environmentName: resolvedEnvironment.name,
- projectFullPath: projectPath,
- });
- });
-
it('should render if the environment has an agent associated', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
@@ -588,14 +554,9 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
});
- it('should render with the namespace if `fluxResourceForEnvironment` feature flag is enabled and the environment has an agent associated', async () => {
+ it('should render with the namespace if the environment has an agent associated', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
- provideData: {
- glFeatures: {
- fluxResourceForEnvironment: true,
- },
- },
apolloProvider: createApolloProvider(agent),
});
diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
index 6156addd63f..b503a6f829e 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -1,7 +1,6 @@
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -13,87 +12,78 @@ describe('New Environments Dropdown', () => {
let wrapper;
let axiosMock;
- beforeEach(() => {
+ const createWrapper = (axiosResult = []) => {
axiosMock = new MockAdapter(axios);
- wrapper = shallowMount(NewEnvironmentsDropdown, {
+ axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, axiosResult);
+
+ wrapper = shallowMountExtended(NewEnvironmentsDropdown, {
provide: { environmentsEndpoint: TEST_HOST },
+ stubs: {
+ GlCollapsibleListbox,
+ },
});
- });
+ };
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findCreateEnvironmentButton = () => wrapper.findByTestId('add-environment-button');
afterEach(() => {
axiosMock.restore();
});
describe('before results', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
it('should show a loading icon', () => {
- axiosMock.onGet(TEST_HOST).reply(() => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
- return axios.waitForAll();
+ expect(findListbox().props('searching')).toBe(true);
});
it('should not show any dropdown items', () => {
- axiosMock.onGet(TEST_HOST).reply(() => {
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(0);
- });
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
- return axios.waitForAll();
+ expect(findListbox().props('items')).toEqual([]);
});
});
describe('with empty results', () => {
- let item;
beforeEach(async () => {
- axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, []);
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH);
+ createWrapper();
+ findListbox().vm.$emit('search', TEST_SEARCH);
await axios.waitForAll();
- await nextTick();
- item = wrapper.findComponent(GlDropdownItem);
});
it('should display a Create item label', () => {
- expect(item.text()).toBe('Create production');
- });
-
- it('should display that no matching items are found', () => {
- expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(true);
+ expect(findCreateEnvironmentButton().text()).toBe(`Create ${TEST_SEARCH}`);
});
it('should emit a new scope when selected', () => {
- item.vm.$emit('click');
+ findCreateEnvironmentButton().vm.$emit('click');
expect(wrapper.emitted('add')).toEqual([[TEST_SEARCH]]);
});
});
describe('with results', () => {
- let items;
- beforeEach(() => {
- axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, ['prod', 'production']);
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'prod');
- return axios.waitForAll().then(() => {
- items = wrapper.findAllComponents(GlDropdownItem);
- });
+ beforeEach(async () => {
+ createWrapper(['prod', 'production']);
+ findListbox().vm.$emit('search', TEST_SEARCH);
+ await axios.waitForAll();
});
- it('should display one item per result', () => {
- expect(items).toHaveLength(2);
+ it('should populate results properly', () => {
+ expect(findListbox().props().items).toHaveLength(2);
});
- it('should emit an add if an item is clicked', () => {
- items.at(0).vm.$emit('click');
+ it('should emit an add on selection', () => {
+ findListbox().vm.$emit('select', ['prod']);
expect(wrapper.emitted('add')).toEqual([['prod']]);
});
- it('should not display a create label', () => {
- items = items.filter((i) => i.text().startsWith('Create'));
- expect(items).toHaveLength(0);
- });
-
it('should not display a message about no results', () => {
expect(wrapper.findComponent({ ref: 'noResults' }).exists()).toBe(false);
});
+
+ it('should not display a footer with the create button', () => {
+ expect(findCreateEnvironmentButton().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js
index ca6e338ac6c..90021829212 100644
--- a/spec/frontend/feature_flags/components/strategy_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_spec.js
@@ -1,11 +1,14 @@
import { GlAlert, GlFormSelect, GlLink, GlToken, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import { last } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import Api from '~/api';
+import axios from '~/lib/utils/axios_utils';
import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Strategy from '~/feature_flags/components/strategy.vue';
import StrategyParameters from '~/feature_flags/components/strategy_parameters.vue';
import {
@@ -22,16 +25,18 @@ import { userList } from '../mock_data';
jest.mock('~/api');
+const TEST_HOST = '/test';
const provide = {
strategyTypeDocsPagePath: 'link-to-strategy-docs',
environmentsScopeDocsPath: 'link-scope-docs',
- environmentsEndpoint: '',
+ environmentsEndpoint: TEST_HOST,
};
Vue.use(Vuex);
describe('Feature flags strategy', () => {
let wrapper;
+ let axiosMock;
const findStrategyParameters = () => wrapper.findComponent(StrategyParameters);
const findDocsLinks = () => wrapper.findAllComponents(GlLink);
@@ -45,6 +50,8 @@ describe('Feature flags strategy', () => {
provide,
},
) => {
+ axiosMock = new MockAdapter(axios);
+ axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, []);
wrapper = mount(Strategy, { store: createStore({ projectId: '1' }), ...opts });
};
@@ -52,6 +59,10 @@ describe('Feature flags strategy', () => {
Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [userList] });
});
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
describe('helper links', () => {
const propsData = { strategy: {}, index: 0, userLists: [userList] };
factory({ propsData, provide });
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 8c16ff100eb..c55099d89d9 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -24,7 +24,7 @@ describe('Filtered Search Manager', () => {
let manager;
let tokensContainer;
const page = 'issues';
- const placeholder = 'Search or filter results...';
+ const placeholder = 'Search or filter results…';
function dispatchBackspaceEvent(element, eventType) {
const event = new Event(eventType);
diff --git a/spec/frontend/fixtures/abuse_reports.rb b/spec/frontend/fixtures/abuse_reports.rb
deleted file mode 100644
index ad0fb9be8dc..00000000000
--- a/spec/frontend/fixtures/abuse_reports.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do
- include JavaScriptFixturesHelpers
- include AdminModeHelper
-
- let(:admin) { create(:admin) }
- let!(:abuse_report) { create(:abuse_report) }
- let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') }
- let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) }
-
- render_views
-
- before do
- stub_feature_flags(abuse_reports_list: false)
-
- sign_in(admin)
- enable_admin_mode!(admin)
- end
-
- it 'abuse_reports/abuse_reports_list.html' do
- get :index
-
- expect(response).to be_successful
- end
-end
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index 73594ddf686..9e6fcea2d17 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', :with_license, type: :controller do
include JavaScriptFixturesHelpers
- let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
+ let(:user) { create(:user, :no_super_sidebar, feed_token: 'feedtoken:coldfeed') }
let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') }
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 6c0b87c5a68..1502999ac9c 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -89,28 +89,28 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
end
end
- it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs.query.graphql' do
+ it_behaves_like 'graphql queries', 'ci/jobs_page/graphql/queries', 'get_jobs.query.graphql' do
let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } }
let(:success_path) { %w[project jobs] }
end
- it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs_count.query.graphql', true do
+ it_behaves_like 'graphql queries', 'ci/jobs_page/graphql/queries', 'get_jobs_count.query.graphql', true do
let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } }
let(:success_path) { %w[project jobs] }
end
- it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs.query.graphql' do
+ it_behaves_like 'graphql queries', 'ci/admin/jobs_table/graphql/queries', 'get_all_jobs.query.graphql' do
let(:user) { create(:admin) }
let(:success_path) { 'jobs' }
end
- it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_cancelable_jobs_count.query.graphql', true do
+ it_behaves_like 'graphql queries', 'ci/admin/jobs_table/graphql/queries', 'get_cancelable_jobs_count.query.graphql', true do
let(:variables) { { statuses: %w[PENDING RUNNING] } }
let(:user) { create(:admin) }
let(:success_path) { %w[cancelable count] }
end
- it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs_count.query.graphql', true do
+ it_behaves_like 'graphql queries', 'ci/admin/jobs_table/graphql/queries', 'get_all_jobs_count.query.graphql', true do
let(:user) { create(:admin) }
let(:success_path) { 'jobs' }
end
diff --git a/spec/frontend/fixtures/pipeline_header.rb b/spec/frontend/fixtures/pipeline_header.rb
index 3fdc45b1194..744df18a403 100644
--- a/spec/frontend/fixtures/pipeline_header.rb
+++ b/spec/frontend/fixtures/pipeline_header.rb
@@ -12,7 +12,7 @@ RSpec.describe "GraphQL Pipeline Header", '(JavaScript fixtures)', type: :reques
let_it_be(:user) { project.first_owner }
let_it_be(:commit) { create(:commit, project: project) }
- let(:query_path) { 'pipelines/graphql/queries/get_pipeline_header_data.query.graphql' }
+ let(:query_path) { 'ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql' }
context 'with successful pipeline' do
let_it_be(:pipeline) do
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index 7bba7910b87..4c95e7ecd20 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -16,35 +16,6 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) }
let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) }
- describe Projects::PipelineSchedulesController, type: :controller do
- render_views
-
- before do
- sign_in(user)
- stub_feature_flags(pipeline_schedules_vue: false)
- end
-
- it 'pipeline_schedules/edit.html' do
- get :edit, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: pipeline_schedule.id
- }
-
- expect(response).to be_successful
- end
-
- it 'pipeline_schedules/edit_with_variables.html' do
- get :edit, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: pipeline_schedule_populated.id
- }
-
- expect(response).to be_successful
- end
- end
-
describe GraphQL::Query, type: :request do
before do
pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 24a6f6f7de6..151d4a763c0 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -71,7 +71,7 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
end
let_it_be(:query) do
- get_graphql_query_as_string("pipelines/graphql/queries/#{get_pipeline_actions_query}")
+ get_graphql_query_as_string("ci/pipelines_page/graphql/queries/#{get_pipeline_actions_query}")
end
it "#{fixtures_path}#{get_pipeline_actions_query}.json" do
diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb
index 0510746a944..23df89a244c 100644
--- a/spec/frontend/fixtures/snippet.rb
+++ b/spec/frontend/fixtures/snippet.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'frontend-fixtures') }
+ let(:user) { create(:user, :no_super_sidebar) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures', owner: user) }
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
- let(:user) { project.first_owner }
let(:snippet) { create(:personal_snippet, :public, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: user) }
render_views
diff --git a/spec/frontend/groups/components/empty_states/groups_dashboard_empty_state_spec.js b/spec/frontend/groups/components/empty_states/groups_dashboard_empty_state_spec.js
new file mode 100644
index 00000000000..d2afbad802c
--- /dev/null
+++ b/spec/frontend/groups/components/empty_states/groups_dashboard_empty_state_spec.js
@@ -0,0 +1,29 @@
+import { GlEmptyState } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GroupsDashboardEmptyState from '~/groups/components/empty_states/groups_dashboard_empty_state.vue';
+
+let wrapper;
+
+const defaultProvide = {
+ groupsEmptyStateIllustration: '/assets/illustrations/empty-state/empty-groups-md.svg',
+};
+
+const createComponent = () => {
+ wrapper = shallowMountExtended(GroupsDashboardEmptyState, {
+ provide: defaultProvide,
+ });
+};
+
+describe('GroupsDashboardEmptyState', () => {
+ it('renders empty state', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: 'A group is a collection of several projects',
+ description:
+ "If you organize your projects under a group, it works like a folder. You can manage your group member's permissions and access to each project in the group.",
+ svgPath: defaultProvide.groupsEmptyStateIllustration,
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/empty_states/groups_explore_empty_state_spec.js b/spec/frontend/groups/components/empty_states/groups_explore_empty_state_spec.js
new file mode 100644
index 00000000000..f4c425902f5
--- /dev/null
+++ b/spec/frontend/groups/components/empty_states/groups_explore_empty_state_spec.js
@@ -0,0 +1,27 @@
+import { GlEmptyState } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GroupsExploreEmptyState from '~/groups/components/empty_states/groups_explore_empty_state.vue';
+
+let wrapper;
+
+const defaultProvide = {
+ groupsEmptyStateIllustration: '/assets/illustrations/empty-state/empty-groups-md.svg',
+};
+
+const createComponent = () => {
+ wrapper = shallowMountExtended(GroupsExploreEmptyState, {
+ provide: defaultProvide,
+ });
+};
+
+describe('GroupsExploreEmptyState', () => {
+ it('renders empty state', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: 'No public groups',
+ svgPath: defaultProvide.groupsEmptyStateIllustration,
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js
deleted file mode 100644
index 9ccdaf8b916..00000000000
--- a/spec/frontend/ide/components/file_templates/dropdown_spec.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import $ from 'jquery';
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import Dropdown from '~/ide/components/file_templates/dropdown.vue';
-
-Vue.use(Vuex);
-
-describe('IDE file templates dropdown component', () => {
- let wrapper;
- let element;
- let fetchTemplateTypesMock;
-
- const defaultProps = {
- label: 'label',
- };
-
- const findItemButtons = () => wrapper.findAll('button');
- const findSearch = () => wrapper.find('input[type="search"]');
- const triggerDropdown = () => $(element).trigger('show.bs.dropdown');
-
- const createComponent = ({ props, state } = {}) => {
- fetchTemplateTypesMock = jest.fn();
- const fakeStore = new Vuex.Store({
- modules: {
- fileTemplates: {
- namespaced: true,
- state: {
- templates: [],
- isLoading: false,
- ...state,
- },
- actions: {
- fetchTemplateTypes: fetchTemplateTypesMock,
- },
- },
- },
- });
-
- wrapper = shallowMount(Dropdown, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- store: fakeStore,
- });
-
- ({ element } = wrapper);
- };
-
- it('calls clickItem on click', async () => {
- const itemData = { name: 'test.yml ' };
- createComponent({ props: { data: [itemData] } });
- const item = findItemButtons().at(0);
- item.trigger('click');
-
- await nextTick();
- expect(wrapper.emitted().click[0][0]).toBe(itemData);
- });
-
- it('renders dropdown title', () => {
- const title = 'Test title';
- createComponent({ props: { title } });
-
- expect(wrapper.find('.dropdown-title').text()).toContain(title);
- });
-
- describe('in async mode', () => {
- const defaultAsyncProps = { ...defaultProps, isAsyncData: true };
-
- it('calls `fetchTemplateTypes` on dropdown event', () => {
- createComponent({ props: defaultAsyncProps });
-
- triggerDropdown();
-
- expect(fetchTemplateTypesMock).toHaveBeenCalled();
- });
-
- it('does not call `fetchTemplateTypes` on dropdown event if destroyed', () => {
- createComponent({ props: defaultAsyncProps });
- wrapper.destroy();
-
- triggerDropdown();
-
- expect(fetchTemplateTypesMock).not.toHaveBeenCalled();
- });
-
- it('shows loader when isLoading is true', () => {
- createComponent({ props: defaultAsyncProps, state: { isLoading: true } });
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
-
- it('renders templates', () => {
- const templates = [{ name: 'file-1' }, { name: 'file-2' }];
- createComponent({
- props: { ...defaultAsyncProps, data: [{ name: 'should-never-appear ' }] },
- state: {
- templates,
- },
- });
- const items = findItemButtons();
-
- expect(items.wrappers.map((x) => x.text())).toEqual(templates.map((x) => x.name));
- });
-
- it('searches template data', async () => {
- const templates = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }];
- const matches = ['match 1', 'match 2'];
- createComponent({
- props: { ...defaultAsyncProps, data: matches, searchable: true },
- state: { templates },
- });
- findSearch().setValue('match');
- await nextTick();
- const items = findItemButtons();
-
- expect(items.length).toBe(matches.length);
- expect(items.wrappers.map((x) => x.text())).toEqual(matches);
- });
-
- it('does not render input when `searchable` is true & `showLoading` is true', () => {
- createComponent({
- props: { ...defaultAsyncProps, searchable: true },
- state: { isLoading: true },
- });
-
- expect(findSearch().exists()).toBe(false);
- });
- });
-
- describe('in sync mode', () => {
- it('renders props data', () => {
- const data = [{ name: 'file-1' }, { name: 'file-2' }];
- createComponent({
- props: { data },
- state: {
- templates: [{ name: 'should-never-appear ' }],
- },
- });
-
- const items = findItemButtons();
-
- expect(items.length).toBe(data.length);
- expect(items.wrappers.map((x) => x.text())).toEqual(data.map((x) => x.name));
- });
-
- it('renders input when `searchable` is true', () => {
- createComponent({ props: { searchable: true } });
-
- expect(findSearch().exists()).toBe(true);
- });
-
- it('searches data', async () => {
- const data = [{ name: 'match 1' }, { name: 'other' }, { name: 'match 2' }];
- const matches = ['match 1', 'match 2'];
- createComponent({ props: { searchable: true, data } });
- findSearch().setValue('match');
- await nextTick();
- const items = findItemButtons();
-
- expect(items.length).toBe(matches.length);
- expect(items.wrappers.map((x) => x.text())).toEqual(matches);
- });
- });
-});
diff --git a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
index 069b6927bac..f7b690fb3a4 100644
--- a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
@@ -4,10 +4,8 @@ exports[`IDE pipelines list when loaded renders empty state when no latestPipeli
<div
class="ide-pipeline"
>
- <!---->
-
<div
- class="gl-h-full gl-display-flex gl-flex-direction-column gl-justify-content-center"
+ class="gl-display-flex gl-flex-direction-column gl-h-full gl-justify-content-center"
>
<empty-state-stub />
</div>
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index f8af8459025..efbbd6c7514 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -18,6 +18,7 @@ jest.mock('~/lib/utils/csrf', () => ({
const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
+const TEST_USERNAME = 'lipsum';
const TEST_PROJECT_PATH = 'group1/project1';
const TEST_BRANCH_NAME = '12345-foo-patch';
const TEST_USER_PREFERENCES_PATH = '/user/preferences';
@@ -69,6 +70,7 @@ describe('ide/init_gitlab_web_ide', () => {
};
beforeEach(() => {
+ gon.current_username = TEST_USERNAME;
process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH;
confirmAction.mockImplementation(
@@ -100,6 +102,7 @@ describe('ide/init_gitlab_web_ide', () => {
mrId: TEST_MR_ID,
mrTargetProject: '',
forkInfo: null,
+ username: gon.current_username,
gitlabUrl: TEST_HOST,
nonce: TEST_NONCE,
httpHeaders: {
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js
index 35cf41b31f5..011f2564cec 100644
--- a/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js
+++ b/spec/frontend/ide/lib/gitlab_web_ide/setup_root_element_spec.js
@@ -24,8 +24,8 @@ describe('~/ide/lib/gitlab_web_ide/setup_root_element', () => {
expect(result).toBe(findIDERoot());
expect(result).toMatchInlineSnapshot(`
<div
- class="gl--flex-center gl-relative gl-h-full"
- id="ide-test-root"
+ class="gl--flex-center gl-h-full gl-relative"
+ id="reference-0"
/>
`);
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index dae5671777c..03d0920994c 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -317,7 +317,7 @@ describe('import table', () => {
});
it('updates page size when selected in Dropdown', async () => {
- const otherOption = findPaginationDropdown().findAll('li p').at(1);
+ const otherOption = findPaginationDropdown().findAll('.gl-new-dropdown-item-content').at(1);
expect(otherOption.text()).toMatchInterpolatedText('50 items per page');
bulkImportSourceGroupsQueryMock.mockResolvedValue({
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index a0710ddb06c..470d63e7c2a 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -46,7 +46,7 @@ describe('Incidents List', () => {
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findTimeAgo = () => wrapper.findAllComponents(TimeAgoTooltip);
const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]');
- const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
+ const findCreateIncidentBtn = () => wrapper.find('[data-testid="create-incident-button"]');
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findSeverity = () => wrapper.findAllComponents(SeverityToken);
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
index b5f8f0023f9..f8a7c47e634 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
@@ -2,14 +2,11 @@
exports[`Alert integration settings form should match the default snapshot 1`] = `
<div>
- <!---->
-
<p>
<gl-sprintf-stub
message="Create a GitLab incident for each PagerDuty incident by %{linkStart}configuring a webhook in PagerDuty%{linkEnd}"
/>
</p>
-
<form>
<gl-form-group-stub
class="col-8 col-md-9 gl-p-0"
@@ -17,13 +14,12 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
optionaltext="(optional)"
>
<gl-toggle-stub
- id="active"
+ id="reference-0"
label="Active"
labelposition="top"
value="true"
/>
</gl-form-group-stub>
-
<gl-form-group-stub
class="col-8 col-md-9 gl-p-0"
label="Webhook URL"
@@ -33,7 +29,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
>
<gl-form-input-group-stub
data-testid="webhook-url"
- id="url"
+ id="reference-1"
inputclass=""
predefinedoptions="[object Object]"
readonly=""
@@ -48,7 +44,6 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
variant="default"
/>
</gl-form-input-group-stub>
-
<gl-button-stub
buttontextclasses=""
category="primary"
@@ -60,11 +55,8 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
tabindex="0"
variant="default"
>
-
Reset webhook URL
-
</gl-button-stub>
-
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
@@ -76,9 +68,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
title="Reset webhook URL"
titletag="h4"
>
-
Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty.
-
</gl-modal-stub>
</gl-form-group-stub>
</form>
diff --git a/spec/frontend/integrations/index/mock_data.js b/spec/frontend/integrations/index/mock_data.js
index c07b320c0d3..65c1e5643e9 100644
--- a/spec/frontend/integrations/index/mock_data.js
+++ b/spec/frontend/integrations/index/mock_data.js
@@ -1,6 +1,7 @@
export const mockActiveIntegrations = [
{
active: true,
+ configured: true,
title: 'Asana',
description: 'Asana - Teamwork without email',
updated_at: '2021-03-18T00:27:09.634Z',
@@ -10,6 +11,7 @@ export const mockActiveIntegrations = [
},
{
active: true,
+ configured: true,
title: 'Jira',
description: 'Jira issue tracker',
updated_at: '2021-01-29T06:41:25.806Z',
@@ -22,6 +24,7 @@ export const mockActiveIntegrations = [
export const mockInactiveIntegrations = [
{
active: false,
+ configured: false,
title: 'Webex Teams',
description: 'Receive event notifications in Webex Teams',
updated_at: null,
@@ -31,6 +34,7 @@ export const mockInactiveIntegrations = [
},
{
active: false,
+ configured: false,
title: 'YouTrack',
description: 'YouTrack issue tracker',
updated_at: null,
@@ -40,6 +44,7 @@ export const mockInactiveIntegrations = [
},
{
active: false,
+ configured: false,
title: 'Atlassian Bamboo CI',
description: 'A continuous integration and build server',
updated_at: null,
@@ -49,6 +54,7 @@ export const mockInactiveIntegrations = [
},
{
active: false,
+ configured: false,
title: 'Prometheus',
description: 'A monitoring tool for Kubernetes',
updated_at: null,
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 1a9b0fae52a..526487f6460 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
@@ -12,7 +12,6 @@ import ModalConfetti from '~/invite_members/components/confetti.vue';
import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
import {
- INVITE_MEMBERS_FOR_TASK,
MEMBERS_MODAL_CELEBRATE_INTRO,
MEMBERS_MODAL_CELEBRATE_TITLE,
MEMBERS_PLACEHOLDER,
@@ -31,7 +30,6 @@ import {
HTTP_STATUS_CREATED,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
} from '~/lib/utils/http_status';
-import { getParameterValues } from '~/lib/utils/url_utility';
import {
displaySuccessfulInvitationAlert,
reloadOnInvitationSuccess,
@@ -54,10 +52,6 @@ import {
jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
jest.mock('~/experimentation/experiment_tracking');
-jest.mock('~/lib/utils/url_utility', () => ({
- ...jest.requireActual('~/lib/utils/url_utility'),
- getParameterValues: jest.fn(() => []),
-}));
describe('InviteMembersModal', () => {
let wrapper;
@@ -129,7 +123,6 @@ describe('InviteMembersModal', () => {
});
const findModal = () => wrapper.findComponent(GlModal);
- const findBase = () => wrapper.findComponent(InviteModalBase);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert');
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
@@ -155,10 +148,6 @@ describe('InviteMembersModal', () => {
findMembersFormGroup().attributes('invalid-feedback');
const membersFormGroupDescription = () => findMembersFormGroup().attributes('description');
const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect);
- const findTasksToBeDone = () => wrapper.findByTestId('invite-members-modal-tasks-to-be-done');
- const findTasks = () => wrapper.findByTestId('invite-members-modal-tasks');
- const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
- const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
const findCelebrationEmoji = () => wrapper.findComponent(GlEmoji);
const triggerOpenModal = async ({ mode = 'default', source } = {}) => {
eventHub.$emit('openModal', { mode, source });
@@ -168,131 +157,11 @@ describe('InviteMembersModal', () => {
findMembersSelect().vm.$emit('input', val);
await nextTick();
};
- const triggerTasks = async (val) => {
- findTasks().vm.$emit('input', val);
- await nextTick();
- };
- const triggerAccessLevel = async (val) => {
- findBase().vm.$emit('access-level', val);
- await nextTick();
- };
const removeMembersToken = async (val) => {
findMembersSelect().vm.$emit('token-remove', val);
await nextTick();
};
- describe('rendering the tasks to be done', () => {
- const setupComponent = async (props = {}, urlParameter = ['invite_members_for_task']) => {
- getParameterValues.mockImplementation(() => urlParameter);
- createComponent(props);
-
- await triggerAccessLevel(30);
- };
-
- const setupComponentWithTasks = async (...args) => {
- await setupComponent(...args);
- await triggerTasks(['ci', 'code']);
- };
-
- afterAll(() => {
- getParameterValues.mockImplementation(() => []);
- });
-
- it('renders the tasks to be done', async () => {
- await setupComponent();
-
- expect(findTasksToBeDone().exists()).toBe(true);
- });
-
- describe('when the selected access level is lower than 30', () => {
- it('does not render the tasks to be done', async () => {
- await setupComponent();
- await triggerAccessLevel(20);
-
- expect(findTasksToBeDone().exists()).toBe(false);
- });
- });
-
- describe('when the url does not contain the parameter `open_modal=invite_members_for_task`', () => {
- it('does not render the tasks to be done', async () => {
- await setupComponent({}, []);
-
- expect(findTasksToBeDone().exists()).toBe(false);
- });
- });
-
- describe('rendering the tasks', () => {
- it('renders the tasks', async () => {
- await setupComponent();
-
- expect(findTasks().exists()).toBe(true);
- });
-
- it('does not render an alert', async () => {
- await setupComponent();
-
- expect(findNoProjectsAlert().exists()).toBe(false);
- });
-
- describe('when there are no projects passed in the data', () => {
- it('does not render the tasks', async () => {
- await setupComponent({ projects: [] });
-
- expect(findTasks().exists()).toBe(false);
- });
-
- it('renders an alert with a link to the new projects path', async () => {
- await setupComponent({ projects: [] });
-
- expect(findNoProjectsAlert().exists()).toBe(true);
- expect(findNoProjectsAlert().findComponent(GlLink).attributes('href')).toBe(
- newProjectPath,
- );
- });
- });
- });
-
- describe('rendering the project dropdown', () => {
- it('renders the project select', async () => {
- await setupComponentWithTasks();
-
- expect(findProjectSelect().exists()).toBe(true);
- });
-
- describe('when the modal is shown for a project', () => {
- it('does not render the project select', async () => {
- await setupComponentWithTasks({ isProject: true });
-
- expect(findProjectSelect().exists()).toBe(false);
- });
- });
-
- describe('when no tasks are selected', () => {
- it('does not render the project select', async () => {
- await setupComponent();
-
- expect(findProjectSelect().exists()).toBe(false);
- });
- });
- });
-
- describe('tracking events', () => {
- it('tracks the submit for invite_members_for_task', async () => {
- await setupComponentWithTasks();
-
- await triggerMembersTokenSelect([user1]);
-
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- clickInviteButton();
-
- expectTracking(INVITE_MEMBERS_FOR_TASK.submit, 'selected_tasks_to_be_done', 'ci,code');
-
- unmockTracking();
- });
- });
- });
-
describe('rendering with tracking considerations', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
@@ -624,6 +493,18 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('exceptionState')).not.toBe(false);
});
+
+ it('displays invite limit error message', async () => {
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.INVITE_LIMIT);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(
+ invitationsApiResponse.INVITE_LIMIT.message,
+ );
+ });
});
});
diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js
index e3e2426fcfc..4f773009f37 100644
--- a/spec/frontend/invite_members/mock_data/api_responses.js
+++ b/spec/frontend/invite_members/mock_data/api_responses.js
@@ -47,6 +47,11 @@ const EMAIL_TAKEN = {
status: 'error',
};
+const INVITE_LIMIT = {
+ message: 'Invite limit of 5 per day exceeded.',
+ status: 'error',
+};
+
export const GROUPS_INVITATIONS_PATH = '/api/v4/groups/1/invitations';
export const invitationsApiResponse = {
@@ -56,6 +61,7 @@ export const invitationsApiResponse = {
MULTIPLE_RESTRICTED,
EMAIL_TAKEN,
EXPANDED_RESTRICTED,
+ INVITE_LIMIT,
};
export const IMPORT_PROJECT_MEMBERS_PATH = '/api/v4/projects/1/import_project_members/2';
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 67fb1dcbfbd..8cde13bf69c 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -6,14 +6,6 @@ export const propsData = {
accessLevels: { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 },
defaultAccessLevel: 30,
helpLink: 'https://example.com',
- tasksToBeDoneOptions: [
- { text: 'First task', value: 'first' },
- { text: 'Second task', value: 'second' },
- ],
- projects: [
- { text: 'First project', value: '1' },
- { text: 'Second project', value: '2' },
- ],
};
export const inviteSource = 'unknown';
@@ -51,8 +43,6 @@ export const postData = {
expires_at: undefined,
invite_source: inviteSource,
format: 'json',
- tasks_to_be_done: [],
- tasks_project_id: '',
};
export const emailPostData = {
@@ -60,8 +50,6 @@ export const emailPostData = {
expires_at: undefined,
email: `${user3.name}`,
invite_source: inviteSource,
- tasks_to_be_done: [],
- tasks_project_id: '',
format: 'json',
};
@@ -71,8 +59,6 @@ export const singleUserPostData = {
user_id: `${user1.id}`,
email: `${user3.name}`,
invite_source: inviteSource,
- tasks_to_be_done: [],
- tasks_project_id: '',
format: 'json',
};
diff --git a/spec/frontend/invite_members/utils/member_utils_spec.js b/spec/frontend/invite_members/utils/member_utils_spec.js
index b6fc70038bb..abae43c3dbb 100644
--- a/spec/frontend/invite_members/utils/member_utils_spec.js
+++ b/spec/frontend/invite_members/utils/member_utils_spec.js
@@ -1,10 +1,4 @@
-import {
- memberName,
- triggerExternalAlert,
- qualifiesForTasksToBeDone,
-} from '~/invite_members/utils/member_utils';
-import setWindowLocation from 'helpers/set_window_location_helper';
-import { getParameterValues } from '~/lib/utils/url_utility';
+import { memberName, triggerExternalAlert } from '~/invite_members/utils/member_utils';
jest.mock('~/lib/utils/url_utility');
@@ -24,17 +18,3 @@ describe('Trigger External Alert', () => {
expect(triggerExternalAlert()).toBe(false);
});
});
-
-describe('Qualifies For Tasks To Be Done', () => {
- it.each([
- ['invite_members_for_task', true],
- ['blah', false],
- ])(`returns name from supplied member token: %j`, (value, result) => {
- setWindowLocation(`blah/blah?open_modal=${value}`);
- getParameterValues.mockImplementation(() => {
- return [value];
- });
-
- expect(qualifiesForTasksToBeDone()).toBe(result);
- });
-});
diff --git a/spec/frontend/issuable/components/csv_export_modal_spec.js b/spec/frontend/issuable/components/csv_export_modal_spec.js
index ccd53e64c4d..118ba9ab378 100644
--- a/spec/frontend/issuable/components/csv_export_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_export_modal_spec.js
@@ -53,7 +53,7 @@ describe('CsvExportModal', () => {
href: 'export/csv/path',
variant: 'confirm',
'data-method': 'post',
- 'data-qa-selector': `export_issues_button`,
+ 'data-testid': 'export-issues-button',
'data-track-action': 'click_button',
'data-track-label': dataTrackLabel,
},
diff --git a/spec/frontend/issuable/components/issuable_header_warnings_spec.js b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
deleted file mode 100644
index 34f36bdf6cb..00000000000
--- a/spec/frontend/issuable/components/issuable_header_warnings_spec.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import mrStore from '~/mr_notes/stores';
-import createIssueStore from '~/notes/stores';
-import IssuableHeaderWarnings from '~/issuable/components/issuable_header_warnings.vue';
-
-const ISSUABLE_TYPE_ISSUE = 'issue';
-const ISSUABLE_TYPE_MR = 'merge_request';
-
-jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores'));
-
-describe('IssuableHeaderWarnings', () => {
- let wrapper;
-
- const findConfidentialIcon = () => wrapper.findByTestId('confidential');
- const findLockedIcon = () => wrapper.findByTestId('locked');
- const findHiddenIcon = () => wrapper.findByTestId('hidden');
-
- const renderTestMessage = (renders) => (renders ? 'renders' : 'does not render');
-
- const createComponent = ({ store, provide }) => {
- wrapper = shallowMountExtended(IssuableHeaderWarnings, {
- mocks: {
- $store: store,
- },
- provide,
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
- });
- };
-
- describe.each`
- issuableType
- ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
- `(`when issuableType=$issuableType`, ({ issuableType }) => {
- describe.each`
- lockStatus | confidentialStatus | hiddenStatus
- ${true} | ${true} | ${false}
- ${true} | ${false} | ${false}
- ${false} | ${true} | ${false}
- ${false} | ${false} | ${false}
- ${true} | ${true} | ${true}
- ${true} | ${false} | ${true}
- ${false} | ${true} | ${true}
- ${false} | ${false} | ${true}
- `(
- `when locked=$lockStatus, confidential=$confidentialStatus, and hidden=$hiddenStatus`,
- ({ lockStatus, confidentialStatus, hiddenStatus }) => {
- const store = issuableType === ISSUABLE_TYPE_ISSUE ? createIssueStore() : mrStore;
-
- beforeEach(() => {
- // TODO: simplify to single assignment after issue store is mock
- if (store === mrStore) {
- store.getters.getNoteableData = {};
- }
-
- store.getters.getNoteableData.confidential = confidentialStatus;
- store.getters.getNoteableData.discussion_locked = lockStatus;
- store.getters.getNoteableData.targetType = issuableType;
-
- createComponent({ store, provide: { hidden: hiddenStatus } });
- });
-
- it(`${renderTestMessage(lockStatus)} the locked icon`, () => {
- const lockedIcon = findLockedIcon();
-
- expect(lockedIcon.exists()).toBe(lockStatus);
-
- if (lockStatus) {
- expect(lockedIcon.attributes('title')).toBe(
- `This ${issuableType.replace('_', ' ')} is locked. Only project members can comment.`,
- );
- expect(getBinding(lockedIcon.element, 'gl-tooltip')).not.toBeUndefined();
- }
- });
-
- it(`${renderTestMessage(confidentialStatus)} the confidential icon`, () => {
- const confidentialEl = findConfidentialIcon();
- expect(confidentialEl.exists()).toBe(confidentialStatus);
-
- if (confidentialStatus && !hiddenStatus) {
- expect(confidentialEl.props()).toMatchObject({
- workspaceType: 'project',
- issuableType: 'issue',
- });
- }
- });
-
- it(`${renderTestMessage(confidentialStatus)} the hidden icon`, () => {
- const hiddenIcon = findHiddenIcon();
-
- expect(hiddenIcon.exists()).toBe(hiddenStatus);
-
- if (hiddenStatus) {
- expect(hiddenIcon.attributes('title')).toBe(
- `This ${issuableType.replace('_', ' ')} is hidden because its author has been banned`,
- );
- expect(getBinding(hiddenIcon.element, 'gl-tooltip')).not.toBeUndefined();
- }
- });
- },
- );
- });
-});
diff --git a/spec/frontend/issuable/components/status_badge_spec.js b/spec/frontend/issuable/components/status_badge_spec.js
new file mode 100644
index 00000000000..cdc848626c7
--- /dev/null
+++ b/spec/frontend/issuable/components/status_badge_spec.js
@@ -0,0 +1,43 @@
+import { GlBadge, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import StatusBadge from '~/issuable/components/status_badge.vue';
+
+describe('StatusBadge component', () => {
+ let wrapper;
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(StatusBadge, { propsData });
+ };
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+
+ describe.each`
+ issuableType | badgeText | state | badgeVariant | badgeIcon
+ ${'merge_request'} | ${'Open'} | ${'opened'} | ${'success'} | ${'merge-request-open'}
+ ${'merge_request'} | ${'Closed'} | ${'closed'} | ${'danger'} | ${'merge-request-close'}
+ ${'merge_request'} | ${'Merged'} | ${'merged'} | ${'info'} | ${'merge'}
+ ${'issue'} | ${'Open'} | ${'opened'} | ${'success'} | ${'issues'}
+ ${'issue'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'issue-closed'}
+ ${'epic'} | ${'Open'} | ${'opened'} | ${'success'} | ${'epic'}
+ ${'epic'} | ${'Closed'} | ${'closed'} | ${'info'} | ${'epic-closed'}
+ `(
+ 'when issuableType=$issuableType and state=$state',
+ ({ issuableType, badgeText, state, badgeVariant, badgeIcon }) => {
+ beforeEach(() => {
+ mountComponent({ state, issuableType });
+ });
+
+ it(`renders badge with text '${badgeText}'`, () => {
+ expect(findBadge().text()).toBe(badgeText);
+ });
+
+ it(`sets badge variant as '${badgeVariant}`, () => {
+ expect(findBadge().props('variant')).toBe(badgeVariant);
+ });
+
+ it(`sets badge icon as '${badgeIcon}'`, () => {
+ expect(findBadge().findComponent(GlIcon).props('name')).toBe(badgeIcon);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js
deleted file mode 100644
index 0d47595c9e6..00000000000
--- a/spec/frontend/issuable/components/status_box_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { GlBadge, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import StatusBox from '~/issuable/components/status_box.vue';
-
-let wrapper;
-
-function factory(propsData) {
- wrapper = shallowMount(StatusBox, { propsData, stubs: { GlBadge } });
-}
-
-describe('Merge request status box component', () => {
- const findBadge = () => wrapper.findComponent(GlBadge);
-
- describe.each`
- issuableType | badgeText | initialState | badgeClass | badgeVariant | badgeIcon
- ${'merge_request'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'merge-request-open'}
- ${'merge_request'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'danger'} | ${'merge-request-close'}
- ${'merge_request'} | ${'Merged'} | ${'merged'} | ${'issuable-status-badge-merged'} | ${'info'} | ${'merge'}
- ${'issue'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'issues'}
- ${'issue'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'issue-closed'}
- ${'epic'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'epic'}
- ${'epic'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'epic-closed'}
- `(
- 'with issuableType set to "$issuableType" and state set to "$initialState"',
- ({ issuableType, badgeText, initialState, badgeClass, badgeVariant, badgeIcon }) => {
- beforeEach(() => {
- factory({
- initialState,
- issuableType,
- });
- });
-
- it(`renders badge with text '${badgeText}'`, () => {
- expect(findBadge().text()).toBe(badgeText);
- });
-
- it(`sets badge css class as '${badgeClass}'`, () => {
- expect(findBadge().classes()).toContain(badgeClass);
- });
-
- it(`sets badge variant as '${badgeVariant}`, () => {
- expect(findBadge().props('variant')).toBe(badgeVariant);
- });
-
- it(`sets badge icon as '${badgeIcon}'`, () => {
- expect(findBadge().findComponent(GlIcon).props('name')).toBe(badgeIcon);
- });
- },
- );
-});
diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js
index 0596433ce9a..2db3a83572c 100644
--- a/spec/frontend/issuable/popover/components/issue_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js
@@ -8,7 +8,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
-import StatusBox from '~/issuable/components/status_box.vue';
+import StatusBadge from '~/issuable/components/status_badge.vue';
import IssuePopover from '~/issuable/popover/components/issue_popover.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -52,9 +52,9 @@ describe('Issue Popover', () => {
});
it('shows status badge', () => {
- expect(wrapper.findComponent(StatusBox).props()).toEqual({
+ expect(wrapper.findComponent(StatusBadge).props()).toEqual({
issuableType: 'issue',
- initialState: issueQueryResponse.data.project.issue.state,
+ state: issueQueryResponse.data.project.issue.state,
});
});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 4686a4fe0c4..f6c9fab76d1 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -137,7 +137,6 @@ describe('IssuesDashboardApp component', () => {
issuablesLoading: false,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: i18n.searchPlaceholder,
showPaginationControls: true,
sortOptions: getSortOptions({
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
diff --git a/spec/frontend/issues/dashboard/mock_data.js b/spec/frontend/issues/dashboard/mock_data.js
index adcd4268449..1e3abd5a018 100644
--- a/spec/frontend/issues/dashboard/mock_data.js
+++ b/spec/frontend/issues/dashboard/mock_data.js
@@ -19,6 +19,7 @@ export const issuesQueryResponse = {
reference: 'group/project#123456',
state: 'opened',
title: 'Issue title',
+ titleHtml: 'Issue title',
type: 'issue',
updatedAt: '2021-05-22T04:08:01Z',
upvotes: 3,
diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index e80ffea0591..8286f84b98a 100644
--- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -3,13 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
+import { WIDGET_TYPE_MILESTONE, WIDGET_TYPE_START_AND_DUE_DATE } from '~/work_items/constants';
describe('CE IssueCardTimeInfo component', () => {
useFakeDate(2020, 11, 11); // 2020 Dec 11
let wrapper;
- const issue = {
+ const issueObject = {
milestone: {
dueDate: '2020-12-17',
startDate: '2020-12-10',
@@ -20,22 +21,41 @@ describe('CE IssueCardTimeInfo component', () => {
humanTimeEstimate: '1w',
};
+ const workItemObject = {
+ widgets: [
+ {
+ type: WIDGET_TYPE_MILESTONE,
+ milestone: {
+ dueDate: '2020-12-17',
+ startDate: '2020-12-10',
+ title: 'My milestone',
+ webPath: '/milestone/webPath',
+ },
+ },
+ {
+ type: WIDGET_TYPE_START_AND_DUE_DATE,
+ dueDate: '2020-12-12',
+ },
+ ],
+ };
+
const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]');
const findMilestoneTitle = () => findMilestone().findComponent(GlLink).attributes('title');
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
const mountComponent = ({
+ issue = issueObject,
state = STATUS_OPEN,
- dueDate = issue.dueDate,
- milestoneDueDate = issue.milestone.dueDate,
- milestoneStartDate = issue.milestone.startDate,
+ dueDate = issueObject.dueDate,
+ milestoneDueDate = issueObject.milestone.dueDate,
+ milestoneStartDate = issueObject.milestone.startDate,
} = {}) =>
shallowMount(IssueCardTimeInfo, {
propsData: {
issue: {
...issue,
milestone: {
- ...issue.milestone,
+ ...issueObject.milestone,
dueDate: milestoneDueDate,
startDate: milestoneStartDate,
},
@@ -45,63 +65,70 @@ describe('CE IssueCardTimeInfo component', () => {
},
});
- describe('milestone', () => {
- it('renders', () => {
- wrapper = mountComponent();
+ describe.each`
+ type | obj
+ ${'issue'} | ${issueObject}
+ ${'work item'} | ${workItemObject}
+ `('with $type object', ({ obj }) => {
+ describe('milestone', () => {
+ it('renders', () => {
+ wrapper = mountComponent({ issue: obj });
- const milestone = findMilestone();
+ const milestone = findMilestone();
- expect(milestone.text()).toBe(issue.milestone.title);
- expect(milestone.findComponent(GlIcon).props('name')).toBe('clock');
- expect(milestone.findComponent(GlLink).attributes('href')).toBe(issue.milestone.webPath);
- });
+ expect(milestone.text()).toBe('My milestone');
+ expect(milestone.findComponent(GlIcon).props('name')).toBe('clock');
+ expect(milestone.findComponent(GlLink).attributes('href')).toBe('/milestone/webPath');
+ });
- describe.each`
- time | text | milestoneDueDate | milestoneStartDate | expected
- ${'due date is in past'} | ${'Past due'} | ${'2020-09-09'} | ${null} | ${'Sep 9, 2020 (Past due)'}
- ${'due date is today'} | ${'Today'} | ${'2020-12-11'} | ${null} | ${'Dec 11, 2020 (Today)'}
- ${'start date is in future'} | ${'Upcoming'} | ${'2021-03-01'} | ${'2021-02-01'} | ${'Mar 1, 2021 (Upcoming)'}
- ${'due date is in future'} | ${'2 weeks remaining'} | ${'2020-12-25'} | ${null} | ${'Dec 25, 2020 (2 weeks remaining)'}
- `('when $description', ({ text, milestoneDueDate, milestoneStartDate, expected }) => {
- it(`renders with "${text}"`, () => {
- wrapper = mountComponent({ milestoneDueDate, milestoneStartDate });
-
- expect(findMilestoneTitle()).toBe(expected);
+ describe.each`
+ time | text | milestoneDueDate | milestoneStartDate | expected
+ ${'due date is in past'} | ${'Past due'} | ${'2020-09-09'} | ${null} | ${'Sep 9, 2020 (Past due)'}
+ ${'due date is today'} | ${'Today'} | ${'2020-12-11'} | ${null} | ${'Dec 11, 2020 (Today)'}
+ ${'start date is in future'} | ${'Upcoming'} | ${'2021-03-01'} | ${'2021-02-01'} | ${'Mar 1, 2021 (Upcoming)'}
+ ${'due date is in future'} | ${'2 weeks remaining'} | ${'2020-12-25'} | ${null} | ${'Dec 25, 2020 (2 weeks remaining)'}
+ `('when $description', ({ text, milestoneDueDate, milestoneStartDate, expected }) => {
+ it(`renders with "${text}"`, () => {
+ wrapper = mountComponent({ issue: obj, milestoneDueDate, milestoneStartDate });
+
+ expect(findMilestoneTitle()).toBe(expected);
+ });
});
});
- });
- describe('due date', () => {
- describe('when upcoming', () => {
- it('renders', () => {
- wrapper = mountComponent();
+ describe('due date', () => {
+ describe('when upcoming', () => {
+ it('renders', () => {
+ wrapper = mountComponent({ issue: obj });
- const dueDate = findDueDate();
+ const dueDate = findDueDate();
- expect(dueDate.text()).toBe('Dec 12, 2020');
- expect(dueDate.attributes('title')).toBe('Due date');
- expect(dueDate.findComponent(GlIcon).props('name')).toBe('calendar');
- expect(dueDate.classes()).not.toContain('gl-text-red-500');
+ expect(dueDate.text()).toBe('Dec 12, 2020');
+ expect(dueDate.attributes('title')).toBe('Due date');
+ expect(dueDate.findComponent(GlIcon).props('name')).toBe('calendar');
+ expect(dueDate.classes()).not.toContain('gl-text-red-500');
+ });
});
- });
- describe('when in the past', () => {
- describe('when issue is open', () => {
- it('renders in red', () => {
- wrapper = mountComponent({ dueDate: '2020-10-10' });
+ describe('when in the past', () => {
+ describe('when issue is open', () => {
+ it('renders in red', () => {
+ wrapper = mountComponent({ issue: obj, dueDate: '2020-10-10' });
- expect(findDueDate().classes()).toContain('gl-text-red-500');
+ expect(findDueDate().classes()).toContain('gl-text-red-500');
+ });
});
- });
- describe('when issue is closed', () => {
- it('does not render in red', () => {
- wrapper = mountComponent({
- dueDate: '2020-10-10',
- state: STATUS_CLOSED,
- });
+ describe('when issue is closed', () => {
+ it('does not render in red', () => {
+ wrapper = mountComponent({
+ issue: obj,
+ dueDate: '2020-10-10',
+ state: STATUS_CLOSED,
+ });
- expect(findDueDate().classes()).not.toContain('gl-text-red-500');
+ expect(findDueDate().classes()).not.toContain('gl-text-red-500');
+ });
});
});
});
@@ -112,7 +139,7 @@ describe('CE IssueCardTimeInfo component', () => {
const timeEstimate = wrapper.find('[data-testid="time-estimate"]');
- expect(timeEstimate.text()).toBe(issue.humanTimeEstimate);
+ expect(timeEstimate.text()).toBe(issueObject.humanTimeEstimate);
expect(timeEstimate.attributes('title')).toBe('Estimate');
expect(timeEstimate.findComponent(GlIcon).props('name')).toBe('timer');
});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index de027a21c8f..f830168ce5d 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -237,7 +237,6 @@ describe('CE IssuesListApp component', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder,
sortOptions: getSortOptions({
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index b9a8bc171db..73fda11f38c 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -49,6 +49,7 @@ export const getIssuesQueryResponse = {
moved: false,
state: 'opened',
title: 'Issue title',
+ titleHtml: 'Issue title',
updatedAt: '2021-05-22T04:08:01Z',
closedAt: null,
upvotes: 3,
diff --git a/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap b/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap
index 1a199ed2ee9..a4bd9608e34 100644
--- a/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap
+++ b/spec/frontend/issues/new/components/__snapshots__/type_popover_spec.js.snap
@@ -3,15 +3,14 @@
exports[`Issue type info popover renders 1`] = `
<span
class="gl-ml-2"
- id="popovercontainer"
+ id="reference-0"
>
<gl-icon-stub
class="gl-text-blue-600"
- id="issue-type-info"
+ id="reference-1"
name="question-o"
size="16"
/>
-
<gl-popover-stub
container="popovercontainer"
cssclasses=""
@@ -20,7 +19,7 @@ exports[`Issue type info popover renders 1`] = `
triggers="focus hover"
>
<ul
- class="gl-list-style-none gl-p-0 gl-m-0"
+ class="gl-list-style-none gl-m-0 gl-p-0"
>
<li
class="gl-mb-3"
@@ -30,19 +29,16 @@ exports[`Issue type info popover renders 1`] = `
>
Issue
</div>
-
<span>
For general work
</span>
</li>
-
<li>
<div
class="gl-font-weight-bold"
>
Incident
</div>
-
<span>
For investigating IT service disruptions or outages
</span>
diff --git a/spec/frontend/service_desk/components/empty_state_with_any_issues_spec.js b/spec/frontend/issues/service_desk/components/empty_state_with_any_issues_spec.js
index ce8a78767d4..90f0847f37b 100644
--- a/spec/frontend/service_desk/components/empty_state_with_any_issues_spec.js
+++ b/spec/frontend/issues/service_desk/components/empty_state_with_any_issues_spec.js
@@ -1,13 +1,13 @@
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import EmptyStateWithAnyIssues from '~/service_desk/components/empty_state_with_any_issues.vue';
+import EmptyStateWithAnyIssues from '~/issues/service_desk/components/empty_state_with_any_issues.vue';
import {
noSearchResultsTitle,
noSearchResultsDescription,
infoBannerUserNote,
noOpenIssuesTitle,
noClosedIssuesTitle,
-} from '~/service_desk/constants';
+} from '~/issues/service_desk/constants';
describe('EmptyStateWithAnyIssues component', () => {
let wrapper;
diff --git a/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/service_desk/components/empty_state_without_any_issues_spec.js
index c67f9588ed4..7f281d6fbfe 100644
--- a/spec/frontend/service_desk/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/issues/service_desk/components/empty_state_without_any_issues_spec.js
@@ -1,7 +1,11 @@
import { GlEmptyState, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import EmptyStateWithoutAnyIssues from '~/service_desk/components/empty_state_without_any_issues.vue';
-import { infoBannerTitle, noIssuesSignedOutButtonText, learnMore } from '~/service_desk/constants';
+import EmptyStateWithoutAnyIssues from '~/issues/service_desk/components/empty_state_without_any_issues.vue';
+import {
+ infoBannerTitle,
+ noIssuesSignedOutButtonText,
+ learnMore,
+} from '~/issues/service_desk/constants';
describe('EmptyStateWithoutAnyIssues component', () => {
let wrapper;
diff --git a/spec/frontend/service_desk/components/info_banner_spec.js b/spec/frontend/issues/service_desk/components/info_banner_spec.js
index 7487d5d8b64..593455f5deb 100644
--- a/spec/frontend/service_desk/components/info_banner_spec.js
+++ b/spec/frontend/issues/service_desk/components/info_banner_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlButton } from '@gitlab/ui';
-import InfoBanner from '~/service_desk/components/info_banner.vue';
-import { infoBannerAdminNote, enableServiceDesk } from '~/service_desk/constants';
+import InfoBanner from '~/issues/service_desk/components/info_banner.vue';
+import { infoBannerAdminNote, enableServiceDesk } from '~/issues/service_desk/constants';
describe('InfoBanner', () => {
let wrapper;
diff --git a/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js b/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
new file mode 100644
index 00000000000..d28b4f2fe76
--- /dev/null
+++ b/spec/frontend/issues/service_desk/components/service_desk_list_app_spec.js
@@ -0,0 +1,717 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { cloneDeep } from 'lodash';
+import VueRouter from 'vue-router';
+import * as Sentry from '@sentry/browser';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
+import waitForPromises from 'helpers/wait_for_promises';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { getSortKey, getSortOptions } from '~/issues/list/utils';
+import { STATUS_CLOSED, STATUS_OPEN, STATUS_ALL } from '~/issues/service_desk/constants';
+import getServiceDeskIssuesQuery from 'ee_else_ce/issues/service_desk/queries/get_service_desk_issues.query.graphql';
+import getServiceDeskIssuesCountsQuery from 'ee_else_ce/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql';
+import setSortingPreferenceMutation from '~/issues/service_desk/queries/set_sorting_preference.mutation.graphql';
+import ServiceDeskListApp from '~/issues/service_desk/components/service_desk_list_app.vue';
+import InfoBanner from '~/issues/service_desk/components/info_banner.vue';
+import EmptyStateWithAnyIssues from '~/issues/service_desk/components/empty_state_with_any_issues.vue';
+import EmptyStateWithoutAnyIssues from '~/issues/service_desk/components/empty_state_without_any_issues.vue';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import {
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SEARCH_WITHIN,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ CREATED_DESC,
+ UPDATED_DESC,
+ RELATIVE_POSITION_ASC,
+ RELATIVE_POSITION,
+ urlSortParams,
+} from '~/issues/list/constants';
+import {
+ getServiceDeskIssuesQueryResponse,
+ getServiceDeskIssuesQueryEmptyResponse,
+ getServiceDeskIssuesCountsQueryResponse,
+ setSortPreferenceMutationResponse,
+ setSortPreferenceMutationResponseWithErrors,
+ filteredTokens,
+ urlParams,
+ locationSearch,
+} from '../mock_data';
+
+jest.mock('@sentry/browser');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
+
+describe('CE ServiceDeskListApp', () => {
+ let wrapper;
+ let router;
+ let axiosMock;
+
+ Vue.use(VueApollo);
+ Vue.use(VueRouter);
+
+ const defaultProvide = {
+ releasesPath: 'releases/path',
+ autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
+ hasBlockedIssuesFeature: false,
+ hasIterationsFeature: true,
+ hasIssueWeightsFeature: true,
+ hasIssuableHealthStatusFeature: true,
+ groupPath: 'group/path',
+ emptyStateSvgPath: 'empty-state.svg',
+ isProject: true,
+ isSignedIn: true,
+ fullPath: 'path/to/project',
+ isServiceDeskSupported: true,
+ hasAnyIssues: true,
+ initialSort: CREATED_DESC,
+ isIssueRepositioningDisabled: false,
+ issuablesLoading: false,
+ showPaginationControls: true,
+ useKeysetPagination: true,
+ hasPreviousPage: getServiceDeskIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage,
+ hasNextPage: getServiceDeskIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage,
+ };
+
+ let defaultQueryResponse = getServiceDeskIssuesQueryResponse;
+ if (IS_EE) {
+ defaultQueryResponse = cloneDeep(getServiceDeskIssuesQueryResponse);
+ defaultQueryResponse.data.project.issues.nodes[0].healthStatus = null;
+ defaultQueryResponse.data.project.issues.nodes[0].weight = 5;
+ }
+
+ const mockServiceDeskIssuesQueryResponseHandler = jest
+ .fn()
+ .mockResolvedValue(defaultQueryResponse);
+ const mockServiceDeskIssuesQueryEmptyResponseHandler = jest
+ .fn()
+ .mockResolvedValue(getServiceDeskIssuesQueryEmptyResponse);
+ const mockServiceDeskIssuesCountsQueryResponseHandler = jest
+ .fn()
+ .mockResolvedValue(getServiceDeskIssuesCountsQueryResponse);
+
+ const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findInfoBanner = () => wrapper.findComponent(InfoBanner);
+ const findLabelsToken = () =>
+ findIssuableList()
+ .props('searchTokens')
+ .find((token) => token.type === TOKEN_TYPE_LABEL);
+
+ const createComponent = ({
+ provide = {},
+ serviceDeskIssuesQueryResponseHandler = mockServiceDeskIssuesQueryResponseHandler,
+ serviceDeskIssuesCountsQueryResponseHandler = mockServiceDeskIssuesCountsQueryResponseHandler,
+ sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
+ } = {}) => {
+ const requestHandlers = [
+ [getServiceDeskIssuesQuery, serviceDeskIssuesQueryResponseHandler],
+ [getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponseHandler],
+ [setSortingPreferenceMutation, sortPreferenceMutationResponse],
+ ];
+
+ router = new VueRouter({ mode: 'history' });
+
+ return shallowMount(ServiceDeskListApp, {
+ apolloProvider: createMockApollo(
+ requestHandlers,
+ {},
+ {
+ typePolicies: {
+ Query: {
+ fields: {
+ project: {
+ merge: true,
+ },
+ },
+ },
+ },
+ },
+ ),
+ router,
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ setWindowLocation(TEST_HOST);
+ axiosMock = new AxiosMockAdapter(axios);
+ wrapper = createComponent();
+ return waitForPromises();
+ });
+
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ it('renders the issuable list with skeletons while fetching service desk issues', async () => {
+ wrapper = createComponent();
+ await nextTick();
+
+ expect(findIssuableList().props('issuablesLoading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findIssuableList().props('issuablesLoading')).toBe(false);
+ });
+
+ it('fetches service desk issues and renders them in the issuable list', () => {
+ expect(findIssuableList().props()).toMatchObject({
+ namespace: 'service-desk',
+ recentSearchesStorageKey: 'service-desk-issues',
+ issuables: defaultQueryResponse.data.project.issues.nodes,
+ tabs: issuableListTabs,
+ currentTab: STATUS_OPEN,
+ tabCounts: {
+ opened: 1,
+ closed: 1,
+ all: 1,
+ },
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
+ initialSortBy: CREATED_DESC,
+ isManualOrdering: false,
+ });
+ });
+
+ describe('InfoBanner', () => {
+ it('renders when Service Desk is supported and has any number of issues', () => {
+ expect(findInfoBanner().exists()).toBe(true);
+ });
+
+ it('does not render when Service Desk is not supported and has any number of issues', () => {
+ wrapper = createComponent({ provide: { isServiceDeskSupported: false } });
+
+ expect(findInfoBanner().exists()).toBe(false);
+ });
+
+ it('does not render, when there are no issues', () => {
+ wrapper = createComponent({
+ serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler,
+ });
+
+ expect(findInfoBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('Empty states', () => {
+ describe('when there are issues', () => {
+ it('shows EmptyStateWithAnyIssues component', () => {
+ setWindowLocation(locationSearch);
+ wrapper = createComponent({
+ serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler,
+ });
+
+ expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({
+ hasSearch: true,
+ isOpenTab: true,
+ });
+ });
+ });
+
+ describe('when there are no issues', () => {
+ it('shows EmptyStateWithoutAnyIssues component', () => {
+ wrapper = createComponent({
+ provide: { hasAnyIssues: false },
+ serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler,
+ });
+
+ expect(wrapper.findComponent(EmptyStateWithoutAnyIssues).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('Initial url params', () => {
+ describe('search', () => {
+ it('is set from the url params', () => {
+ setWindowLocation(locationSearch);
+ wrapper = createComponent();
+
+ expect(router.history.current.query).toMatchObject({ search: 'find issues' });
+ });
+ });
+
+ describe('sort', () => {
+ describe('when initial sort value uses old enum values', () => {
+ const oldEnumSortValues = Object.values(urlSortParams);
+
+ it.each(oldEnumSortValues)('initial sort is set with value %s', async (sort) => {
+ wrapper = createComponent({ provide: { initialSort: sort } });
+ await waitForPromises();
+
+ expect(findIssuableList().props('initialSortBy')).toBe(getSortKey(sort));
+ });
+ });
+
+ describe('when initial sort value uses new GraphQL enum values', () => {
+ const graphQLEnumSortValues = Object.keys(urlSortParams);
+
+ it.each(graphQLEnumSortValues)('initial sort is set with value %s', async (sort) => {
+ wrapper = createComponent({ provide: { initialSort: sort.toLowerCase() } });
+ await waitForPromises();
+
+ expect(findIssuableList().props('initialSortBy')).toBe(sort);
+ });
+ });
+
+ describe('when initial sort value is invalid', () => {
+ it.each(['', 'asdf', null, undefined])(
+ 'initial sort is set to value CREATED_DESC',
+ async (sort) => {
+ wrapper = createComponent({ provide: { initialSort: sort } });
+ await waitForPromises();
+
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
+ },
+ );
+ });
+
+ describe('when sort is manual and issue repositioning is disabled', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ provide: { initialSort: RELATIVE_POSITION, isIssueRepositioningDisabled: true },
+ });
+ await waitForPromises();
+ });
+
+ it('changes the sort to the default of created descending', () => {
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
+ });
+
+ it('shows an alert to tell the user that manual reordering is disabled', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: ServiceDeskListApp.i18n.issueRepositioningMessage,
+ variant: VARIANT_INFO,
+ });
+ });
+ });
+ });
+
+ describe('state', () => {
+ it('is set from the url params', async () => {
+ const initialState = STATUS_ALL;
+ setWindowLocation(`?state=${initialState}`);
+ wrapper = createComponent();
+ await waitForPromises();
+
+ expect(findIssuableList().props('currentTab')).toBe(initialState);
+ });
+ });
+
+ describe('filter tokens', () => {
+ it('are set from the url params', () => {
+ setWindowLocation(locationSearch);
+ wrapper = createComponent();
+
+ expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
+ });
+ });
+ });
+
+ describe('Tokens', () => {
+ const mockCurrentUser = {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ avatar_url: 'avatar/url',
+ };
+
+ describe('when user is signed out', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ provide: { isSignedIn: false } });
+ return waitForPromises();
+ });
+
+ it('does not render My-Reaction or Confidential tokens', () => {
+ expect(findIssuableList().props('searchTokens')).not.toMatchObject([
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers: [mockCurrentUser] },
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers: [mockCurrentUser] },
+ { type: TOKEN_TYPE_MY_REACTION },
+ { type: TOKEN_TYPE_CONFIDENTIAL },
+ ]);
+ });
+ });
+
+ describe('when all tokens are available', () => {
+ beforeEach(() => {
+ window.gon = {
+ current_user_id: mockCurrentUser.id,
+ current_user_fullname: mockCurrentUser.name,
+ current_username: mockCurrentUser.username,
+ current_user_avatar_url: mockCurrentUser.avatar_url,
+ };
+
+ wrapper = createComponent();
+ return waitForPromises();
+ });
+
+ it('renders all tokens alphabetically', () => {
+ const preloadedUsers = [
+ { ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
+ ];
+
+ expect(findIssuableList().props('searchTokens')).toMatchObject([
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
+ { type: TOKEN_TYPE_CONFIDENTIAL },
+ { type: TOKEN_TYPE_LABEL },
+ { type: TOKEN_TYPE_MILESTONE },
+ { type: TOKEN_TYPE_MY_REACTION },
+ { type: TOKEN_TYPE_RELEASE },
+ { type: TOKEN_TYPE_SEARCH_WITHIN },
+ ]);
+ });
+ });
+ });
+
+ describe('Events', () => {
+ describe('when "click-tab" event is emitted by IssuableList', () => {
+ beforeEach(async () => {
+ wrapper = createComponent();
+ router.push = jest.fn();
+ await waitForPromises();
+
+ findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
+ });
+
+ it('updates ui to the new tab', () => {
+ expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
+ });
+
+ it('updates url to the new tab', () => {
+ expect(router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ state: STATUS_CLOSED }),
+ });
+ });
+ });
+
+ describe('when "reorder" event is emitted by IssuableList', () => {
+ const issueOne = {
+ ...defaultQueryResponse.data.project.issues.nodes[0],
+ id: 'gid://gitlab/Issue/1',
+ iid: '101',
+ reference: 'group/project#1',
+ webPath: '/group/project/-/issues/1',
+ };
+ const issueTwo = {
+ ...defaultQueryResponse.data.project.issues.nodes[0],
+ id: 'gid://gitlab/Issue/2',
+ iid: '102',
+ reference: 'group/project#2',
+ webPath: '/group/project/-/issues/2',
+ };
+ const issueThree = {
+ ...defaultQueryResponse.data.project.issues.nodes[0],
+ id: 'gid://gitlab/Issue/3',
+ iid: '103',
+ reference: 'group/project#3',
+ webPath: '/group/project/-/issues/3',
+ };
+ const issueFour = {
+ ...defaultQueryResponse.data.project.issues.nodes[0],
+ id: 'gid://gitlab/Issue/4',
+ iid: '104',
+ reference: 'group/project#4',
+ webPath: '/group/project/-/issues/4',
+ };
+ const response = () => ({
+ data: {
+ project: {
+ id: '1',
+ issues: {
+ ...defaultQueryResponse.data.project.issues,
+ nodes: [issueOne, issueTwo, issueThree, issueFour],
+ },
+ },
+ },
+ });
+
+ describe('when successful', () => {
+ describe.each`
+ description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId
+ ${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id}
+ ${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id}
+ ${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id}
+ ${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null}
+ `(
+ 'when moving issue $description',
+ ({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ serviceDeskIssuesQueryResponseHandler: jest.fn().mockResolvedValue(response()),
+ });
+ return waitForPromises();
+ });
+
+ it('makes API call to reorder the issue', async () => {
+ findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
+ await waitForPromises();
+
+ expect(axiosMock.history.put[0]).toMatchObject({
+ url: joinPaths(issueToMove.webPath, 'reorder'),
+ data: JSON.stringify({
+ move_before_id: getIdFromGraphQLId(moveBeforeId),
+ move_after_id: getIdFromGraphQLId(moveAfterId),
+ }),
+ });
+ });
+ },
+ );
+ });
+
+ describe('when unsuccessful', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ serviceDeskIssuesQueryResponseHandler: jest.fn().mockResolvedValue(response()),
+ });
+ return waitForPromises();
+ });
+
+ it('displays an error message', async () => {
+ axiosMock
+ .onPut(joinPaths(issueOne.webPath, 'reorder'))
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 });
+ await waitForPromises();
+
+ expect(findIssuableList().props('error')).toBe(ServiceDeskListApp.i18n.reorderError);
+ expect(Sentry.captureException).toHaveBeenCalledWith(
+ new Error('Request failed with status code 500'),
+ );
+ });
+ });
+ });
+
+ describe('when "sort" event is emitted by IssuableList', () => {
+ it.each(Object.keys(urlSortParams))(
+ 'updates to the new sort when payload is `%s`',
+ async (sortKey) => {
+ // Ensure initial sort key is different so we can trigger an update when emitting a sort key
+ wrapper =
+ sortKey === CREATED_DESC
+ ? createComponent({ provide: { initialSort: UPDATED_DESC } })
+ : createComponent();
+ router.push = jest.fn();
+ await waitForPromises();
+
+ findIssuableList().vm.$emit('sort', sortKey);
+
+ expect(router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ sort: urlSortParams[sortKey] }),
+ });
+ },
+ );
+
+ describe('when issue repositioning is disabled', () => {
+ const initialSort = CREATED_DESC;
+
+ beforeEach(async () => {
+ wrapper = createComponent({
+ provide: { initialSort, isIssueRepositioningDisabled: true },
+ });
+ router.push = jest.fn();
+ await waitForPromises();
+
+ findIssuableList().vm.$emit('sort', RELATIVE_POSITION_ASC);
+ });
+
+ it('does not update the sort to manual', () => {
+ expect(router.push).not.toHaveBeenCalled();
+ });
+
+ it('shows an alert to tell the user that manual reordering is disabled', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: ServiceDeskListApp.i18n.issueRepositioningMessage,
+ variant: VARIANT_INFO,
+ });
+ });
+ });
+
+ describe('when user is signed in', () => {
+ it('calls mutation to save sort preference', async () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ wrapper = createComponent({ sortPreferenceMutationResponse: mutationMock });
+ await waitForPromises();
+
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
+
+ expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } });
+ });
+
+ it('captures error when mutation response has errors', async () => {
+ const mutationMock = jest
+ .fn()
+ .mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
+ wrapper = createComponent({ sortPreferenceMutationResponse: mutationMock });
+ await waitForPromises();
+
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
+ });
+ });
+
+ describe('when user is signed out', () => {
+ it('does not call mutation to save sort preference', async () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ wrapper = createComponent({
+ provide: { isSignedIn: false },
+ sortPreferenceMutationResponse: mutationMock,
+ });
+ await waitForPromises();
+
+ findIssuableList().vm.$emit('sort', CREATED_DESC);
+
+ expect(mutationMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe.each`
+ event | params
+ ${'next-page'} | ${{ page_after: 'endcursor', page_before: undefined, first_page_size: 20, last_page_size: undefined }}
+ ${'previous-page'} | ${{ page_after: undefined, page_before: 'startcursor', first_page_size: undefined, last_page_size: 20 }}
+ `('when "$event" event is emitted by IssuableList', ({ event, params }) => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ data: {
+ pageInfo: {
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
+ },
+ });
+ await waitForPromises();
+ router.push = jest.fn();
+
+ findIssuableList().vm.$emit(event);
+ });
+
+ it('scrolls to the top', () => {
+ expect(scrollUp).toHaveBeenCalled();
+ });
+
+ it('updates url', () => {
+ expect(router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining(params),
+ });
+ });
+ });
+
+ describe('when "filter" event is emitted by IssuableList', () => {
+ it('updates IssuableList with url params', async () => {
+ wrapper = createComponent();
+ router.push = jest.fn();
+ await waitForPromises();
+
+ findIssuableList().vm.$emit('filter', filteredTokens);
+ await nextTick();
+
+ expect(router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining(urlParams),
+ });
+ });
+ });
+
+ describe('when "page-size-change" event is emitted by IssuableList', () => {
+ it('updates url params with new page size', async () => {
+ wrapper = createComponent();
+ router.push = jest.fn();
+ await waitForPromises();
+
+ findIssuableList().vm.$emit('page-size-change', 50);
+ await nextTick();
+
+ expect(router.push).toHaveBeenCalledTimes(1);
+ expect(router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ first_page_size: 50 }),
+ });
+ });
+ });
+ });
+
+ describe('Errors', () => {
+ describe.each`
+ error | responseHandler | message
+ ${'fetching issues'} | ${'serviceDeskIssuesQueryResponseHandler'} | ${'An error occurred while loading issues'}
+ ${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponseHandler'} | ${'An error occurred while getting issue counts'}
+ `('when there is an error $error', ({ responseHandler, message }) => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ [responseHandler]: jest.fn().mockRejectedValue(new Error('ERROR')),
+ });
+ return waitForPromises();
+ });
+
+ it('shows an error message', () => {
+ expect(findIssuableList().props('error')).toBe(message);
+ });
+
+ it('is captured with Sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ });
+ });
+
+ it('clears error message when "dismiss-alert" event is emitted from IssuableList', async () => {
+ wrapper = createComponent({
+ serviceDeskIssuesQueryResponseHandler: jest.fn().mockRejectedValue(new Error()),
+ });
+ await waitForPromises();
+ findIssuableList().vm.$emit('dismiss-alert');
+ await nextTick();
+
+ expect(findIssuableList().props('error')).toBe('');
+ });
+ });
+
+ describe('When providing token for labels', () => {
+ it('passes function to fetchLatestLabels property if frontend caching is enabled', async () => {
+ wrapper = createComponent({
+ provide: {
+ glFeatures: {
+ frontendCaching: true,
+ },
+ },
+ });
+ await waitForPromises();
+
+ expect(typeof findLabelsToken().fetchLatestLabels).toBe('function');
+ });
+
+ it('passes null to fetchLatestLabels property if frontend caching is disabled', async () => {
+ wrapper = createComponent({
+ provide: {
+ glFeatures: {
+ frontendCaching: false,
+ },
+ },
+ });
+ await waitForPromises();
+
+ expect(findLabelsToken().fetchLatestLabels).toBe(null);
+ });
+ });
+});
diff --git a/spec/frontend/service_desk/mock_data.js b/spec/frontend/issues/service_desk/mock_data.js
index dc875cb5c1e..1e2f209d732 100644
--- a/spec/frontend/service_desk/mock_data.js
+++ b/spec/frontend/issues/service_desk/mock_data.js
@@ -74,6 +74,7 @@ export const getServiceDeskIssuesQueryResponse = {
username: 'support-bot',
webUrl: 'url/hsimpson',
},
+ externalAuthor: 'client@client.com',
labels: {
nodes: [
{
@@ -134,6 +135,22 @@ export const getServiceDeskIssuesCountsQueryResponse = {
},
};
+export const setSortPreferenceMutationResponse = {
+ data: {
+ userPreferencesUpdate: {
+ errors: [],
+ },
+ },
+};
+
+export const setSortPreferenceMutationResponseWithErrors = {
+ data: {
+ userPreferencesUpdate: {
+ errors: ['oh no!'],
+ },
+ },
+};
+
export const filteredTokens = [
{ type: FILTERED_SEARCH_TERM, value: { data: 'find issues', operator: 'undefined' } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'bart', operator: OPERATOR_IS } },
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index de183f94277..8999952c54c 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -1,23 +1,14 @@
-import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import {
- issuableStatusText,
- STATUS_CLOSED,
- STATUS_OPEN,
- STATUS_REOPENED,
- TYPE_EPIC,
- TYPE_INCIDENT,
- TYPE_ISSUE,
-} from '~/issues/constants';
+import { TYPE_EPIC, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
import DescriptionComponent from '~/issues/show/components/description.vue';
import EditedComponent from '~/issues/show/components/edited.vue';
import FormComponent from '~/issues/show/components/form.vue';
+import StickyHeader from '~/issues/show/components/sticky_header.vue';
import TitleComponent from '~/issues/show/components/title.vue';
import IncidentTabs from '~/issues/show/components/incidents/incident_tabs.vue';
import PinnedLinks from '~/issues/show/components/pinned_links.vue';
@@ -44,22 +35,15 @@ describe('Issuable output', () => {
let axiosMock;
let wrapper;
- const findStickyHeader = () => wrapper.findByTestId('issue-sticky-header');
- const findLockedBadge = () => wrapper.findByTestId('locked');
- const findConfidentialBadge = () => wrapper.findByTestId('confidential');
- const findHiddenBadge = () => wrapper.findByTestId('hidden');
-
+ const findStickyHeader = () => wrapper.findComponent(StickyHeader);
const findTitle = () => wrapper.findComponent(TitleComponent);
const findDescription = () => wrapper.findComponent(DescriptionComponent);
const findEdited = () => wrapper.findComponent(EditedComponent);
const findForm = () => wrapper.findComponent(FormComponent);
const findPinnedLinks = () => wrapper.findComponent(PinnedLinks);
- const createComponent = ({ props = {}, options = {}, data = {} } = {}) => {
- wrapper = shallowMountExtended(IssuableApp, {
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
+ const createComponent = ({ props = {}, options = {} } = {}) => {
+ wrapper = shallowMount(IssuableApp, {
propsData: { ...appProps, ...props },
provide: {
fullPath: 'gitlab-org/incidents',
@@ -69,11 +53,6 @@ describe('Issuable output', () => {
HighlightBar: true,
IncidentTabs: true,
},
- data() {
- return {
- ...data,
- };
- },
...options,
});
@@ -81,13 +60,6 @@ describe('Issuable output', () => {
return waitForPromises();
};
- const createComponentAndScroll = async (props) => {
- await createComponent({ props });
- global.pageYOffset = 100;
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
- await nextTick();
- };
-
const emitHubEvent = (event) => {
eventHub.$emit(event);
return waitForPromises();
@@ -332,104 +304,36 @@ describe('Issuable output', () => {
describe('when title is in view', () => {
it('is not shown', async () => {
await createComponent();
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
- expect(findStickyHeader().exists()).toBe(false);
+ wrapper.findComponent(StickyHeader).vm.$emit('show');
+
+ expect(findStickyHeader().props('show')).toBe(false);
});
});
- describe('when title is not in view', () => {
- it.each([TYPE_INCIDENT, TYPE_ISSUE, TYPE_EPIC])(
- 'shows with title when issuableType="%s"',
- async (issuableType) => {
- await createComponentAndScroll({ issuableType });
-
- expect(findStickyHeader().text()).toContain('this is a title');
- },
- );
-
- it.each`
- issuableType | issuableStatus | statusIcon
- ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issues'}
- ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-closed'}
- ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
- ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
- ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'}
- ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'}
- `(
- 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
- async ({ issuableType, issuableStatus, statusIcon }) => {
- await createComponentAndScroll({ issuableType, issuableStatus });
-
- expect(findStickyHeader().findComponent(GlIcon).props('name')).toBe(statusIcon);
- },
- );
-
- it.each`
- title | issuableStatus
- ${'shows with Open when status is opened'} | ${STATUS_OPEN}
- ${'shows with Closed when status is closed'} | ${STATUS_CLOSED}
- ${'shows with Open when status is reopened'} | ${STATUS_REOPENED}
- `('$title', async ({ issuableStatus }) => {
- await createComponentAndScroll({ issuableStatus });
-
- expect(findStickyHeader().text()).toContain(issuableStatusText[issuableStatus]);
- });
+ describe.each([TYPE_INCIDENT, TYPE_ISSUE, TYPE_EPIC])(
+ 'when title is not in view',
+ (issuableType) => {
+ beforeEach(async () => {
+ await createComponent({ props: { issuableType } });
- it.each`
- title | isConfidential
- ${'does not show confidential badge when issue is not confidential'} | ${false}
- ${'shows confidential badge when issue is confidential'} | ${true}
- `('$title', async ({ isConfidential }) => {
- await createComponentAndScroll({ isConfidential });
- const confidentialEl = findConfidentialBadge();
-
- expect(confidentialEl.exists()).toBe(isConfidential);
-
- if (isConfidential) {
- expect(confidentialEl.props()).toMatchObject({
- workspaceType: 'project',
- issuableType: 'issue',
- });
- }
- });
+ global.pageYOffset = 100;
+ wrapper.findComponent(StickyHeader).vm.$emit('show');
+ await nextTick();
+ });
- it.each`
- title | isLocked
- ${'does not show locked badge when issue is not locked'} | ${false}
- ${'shows locked badge when issue is locked'} | ${true}
- `('$title', async ({ isLocked }) => {
- await createComponentAndScroll({ isLocked });
- const lockedBadge = findLockedBadge();
-
- expect(lockedBadge.exists()).toBe(isLocked);
-
- if (isLocked) {
- expect(lockedBadge.attributes('title')).toBe(
- 'This issue is locked. Only project members can comment.',
- );
- expect(getBinding(lockedBadge.element, 'gl-tooltip')).not.toBeUndefined();
- }
- });
+ it(`shows when issuableType=${issuableType}`, () => {
+ expect(findStickyHeader().props('show')).toBe(true);
+ });
- it.each`
- title | isHidden
- ${'does not show hidden badge when issue is not hidden'} | ${false}
- ${'shows hidden badge when issue is hidden'} | ${true}
- `('$title', async ({ isHidden }) => {
- await createComponentAndScroll({ isHidden });
- const hiddenBadge = findHiddenBadge();
-
- expect(hiddenBadge.exists()).toBe(isHidden);
-
- if (isHidden) {
- expect(hiddenBadge.attributes('title')).toBe(
- 'This issue is hidden because its author has been banned',
- );
- expect(getBinding(hiddenBadge.element, 'gl-tooltip')).not.toBeUndefined();
- }
- });
- });
+ it('hides again when title is back in view', async () => {
+ wrapper.findComponent(StickyHeader).vm.$emit('hide');
+ await nextTick();
+
+ expect(findStickyHeader().props('show')).toBe(false);
+ });
+ },
+ );
});
describe('Composable description component', () => {
diff --git a/spec/frontend/issues/show/components/sticky_header_spec.js b/spec/frontend/issues/show/components/sticky_header_spec.js
new file mode 100644
index 00000000000..0c54ae45e70
--- /dev/null
+++ b/spec/frontend/issues/show/components/sticky_header_spec.js
@@ -0,0 +1,135 @@
+import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ issuableStatusText,
+ STATUS_CLOSED,
+ STATUS_OPEN,
+ STATUS_REOPENED,
+ TYPE_EPIC,
+ TYPE_INCIDENT,
+ TYPE_ISSUE,
+} from '~/issues/constants';
+import StickyHeader from '~/issues/show/components/sticky_header.vue';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+describe('StickyHeader component', () => {
+ let wrapper;
+
+ const findConfidentialBadge = () => wrapper.findComponent(ConfidentialityBadge);
+ const findHiddenBadge = () => wrapper.findByTestId('hidden');
+ const findLockedBadge = () => wrapper.findByTestId('locked');
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(StickyHeader, {
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ propsData: {
+ issuableStatus: STATUS_OPEN,
+ issuableType: TYPE_ISSUE,
+ show: true,
+ title: 'A sticky issue',
+ titleHtml: '',
+ ...props,
+ },
+ });
+ };
+
+ it.each`
+ issuableType | issuableStatus | statusIcon
+ ${TYPE_INCIDENT} | ${STATUS_OPEN} | ${'issues'}
+ ${TYPE_INCIDENT} | ${STATUS_CLOSED} | ${'issue-closed'}
+ ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
+ ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
+ ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'}
+ ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'}
+ `(
+ 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
+ ({ issuableType, issuableStatus, statusIcon }) => {
+ createComponent({ issuableType, issuableStatus });
+
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe(statusIcon);
+ },
+ );
+
+ it.each`
+ title | issuableStatus
+ ${'shows with Open when status is opened'} | ${STATUS_OPEN}
+ ${'shows with Closed when status is closed'} | ${STATUS_CLOSED}
+ ${'shows with Open when status is reopened'} | ${STATUS_REOPENED}
+ `('$title', ({ issuableStatus }) => {
+ createComponent({ issuableStatus });
+
+ expect(wrapper.text()).toContain(issuableStatusText[issuableStatus]);
+ });
+
+ it.each`
+ title | isConfidential
+ ${'does not show confidential badge when issue is not confidential'} | ${false}
+ ${'shows confidential badge when issue is confidential'} | ${true}
+ `('$title', ({ isConfidential }) => {
+ createComponent({ isConfidential });
+ const confidentialBadge = findConfidentialBadge();
+
+ expect(confidentialBadge.exists()).toBe(isConfidential);
+
+ if (isConfidential) {
+ expect(confidentialBadge.props()).toMatchObject({
+ workspaceType: 'project',
+ issuableType: 'issue',
+ });
+ }
+ });
+
+ it.each`
+ title | isLocked
+ ${'does not show locked badge when issue is not locked'} | ${false}
+ ${'shows locked badge when issue is locked'} | ${true}
+ `('$title', ({ isLocked }) => {
+ createComponent({ isLocked });
+ const lockedBadge = findLockedBadge();
+
+ expect(lockedBadge.exists()).toBe(isLocked);
+
+ if (isLocked) {
+ expect(lockedBadge.attributes('title')).toBe(
+ 'This issue is locked. Only project members can comment.',
+ );
+ expect(getBinding(lockedBadge.element, 'gl-tooltip')).not.toBeUndefined();
+ }
+ });
+
+ it.each`
+ title | isHidden
+ ${'does not show hidden badge when issue is not hidden'} | ${false}
+ ${'shows hidden badge when issue is hidden'} | ${true}
+ `('$title', ({ isHidden }) => {
+ createComponent({ isHidden });
+ const hiddenBadge = findHiddenBadge();
+
+ expect(hiddenBadge.exists()).toBe(isHidden);
+
+ if (isHidden) {
+ expect(hiddenBadge.attributes('title')).toBe(
+ 'This issue is hidden because its author has been banned',
+ );
+ expect(getBinding(hiddenBadge.element, 'gl-tooltip')).not.toBeUndefined();
+ }
+ });
+
+ it('shows with title', () => {
+ createComponent();
+ const title = wrapper.find('a');
+
+ expect(title.text()).toContain('A sticky issue');
+ expect(title.attributes('href')).toBe('#top');
+ });
+
+ it('shows title containing markup', () => {
+ const titleHtml = '<b>A sticky issue</b>';
+ createComponent({ titleHtml });
+
+ expect(wrapper.find('a').html()).toContain(titleHtml);
+ });
+});
diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
index 93cb7b5ae16..b2e57bf49d0 100644
--- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -1,5 +1,6 @@
-import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { TYPE_EPIC, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
import eventHub from '~/issues/show/event_hub';
@@ -9,26 +10,24 @@ describe('TaskListItemActions component', () => {
let wrapper;
const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findConvertToTaskItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(0);
- const findDeleteItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(1);
+ const findConvertToTaskItem = () => wrapper.findByTestId('convert');
+ const findDeleteItem = () => wrapper.findByTestId('delete');
- const mountComponent = () => {
+ const mountComponent = ({ issuableType = TYPE_ISSUE } = {}) => {
const li = document.createElement('li');
li.dataset.sourcepos = '3:1-3:10';
li.appendChild(document.createElement('div'));
document.body.appendChild(li);
- wrapper = shallowMount(TaskListItemActions, {
- provide: { canUpdate: true },
+ wrapper = shallowMountExtended(TaskListItemActions, {
+ provide: { canUpdate: true, issuableType },
attachTo: document.querySelector('div'),
});
};
- beforeEach(() => {
+ it('renders dropdown', () => {
mountComponent();
- });
- it('renders dropdown', () => {
expect(findGlDisclosureDropdown().props()).toMatchObject({
category: 'tertiary',
icon: 'ellipsis_v',
@@ -38,15 +37,36 @@ describe('TaskListItemActions component', () => {
});
});
- it('emits event when `Convert to task` dropdown item is clicked', () => {
- findConvertToTaskItem().vm.$emit('action');
+ describe('"Convert to task" dropdown item', () => {
+ describe.each`
+ issuableType | exists
+ ${TYPE_EPIC} | ${false}
+ ${TYPE_INCIDENT} | ${true}
+ ${TYPE_ISSUE} | ${true}
+ `(`when $issuableType`, ({ issuableType, exists }) => {
+ it(`${exists ? 'renders' : 'does not render'}`, () => {
+ mountComponent({ issuableType });
- expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
+ expect(findConvertToTaskItem().exists()).toBe(exists);
+ });
+ });
});
- it('emits event when `Delete` dropdown item is clicked', () => {
- findDeleteItem().vm.$emit('action');
+ describe('events', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('emits event when `Convert to task` dropdown item is clicked', () => {
+ findConvertToTaskItem().vm.$emit('action');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
+ });
- expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
+ it('emits event when `Delete` dropdown item is clicked', () => {
+ findDeleteItem().vm.$emit('action');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
+ });
});
});
diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js
deleted file mode 100644
index 561035242eb..00000000000
--- a/spec/frontend/issues/show/issue_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
-import { initIssueApp } from '~/issues/show';
-import * as parseData from '~/issues/show/utils/parse_data';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import createStore from '~/notes/stores';
-import { appProps } from './mock_data/mock_data';
-
-const mock = new MockAdapter(axios);
-mock.onGet().reply(HTTP_STATUS_OK);
-
-jest.mock('~/lib/utils/poll');
-
-const setupHTML = (initialData) => {
- document.body.innerHTML = `<div id="js-issuable-app"></div>`;
- document.getElementById('js-issuable-app').dataset.initial = JSON.stringify(initialData);
-};
-
-describe('Issue show index', () => {
- describe('initIssueApp', () => {
- // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/390368
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('should initialize app with no potential XSS attack', async () => {
- const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
- const parseDataSpy = jest.spyOn(parseData, 'parseIssuableData');
-
- setupHTML({
- ...appProps,
- initialDescriptionHtml: '<svg onload=window.alert(1)>',
- });
-
- const initialDataEl = document.getElementById('js-issuable-app');
- const issuableData = parseData.parseIssuableData(initialDataEl);
- initIssueApp(issuableData, createStore());
-
- await waitForPromises();
-
- expect(parseDataSpy).toHaveBeenCalled();
- expect(alertSpy).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index ed969a08ac5..37aa18ced8d 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -1,8 +1,9 @@
import { TEST_HOST } from 'helpers/test_constants';
export const initialRequest = {
- title: '<p>this is a title</p>',
+ title: '<gl-emoji title="party-parrot"></gl-emoji>this is a title',
title_text: 'this is a title',
+ title_html: '<gl-emoji title="party-parrot"></gl-emoji>this is a title',
description: '<p>this is a description!</p>',
description_text: 'this is a description',
task_completion_status: { completed_count: 2, count: 4 },
diff --git a/spec/frontend/issues/show/store_spec.js b/spec/frontend/issues/show/store_spec.js
deleted file mode 100644
index 20d3a6cdaae..00000000000
--- a/spec/frontend/issues/show/store_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import Store from '~/issues/show/stores';
-import updateDescription from '~/issues/show/utils/update_description';
-
-jest.mock('~/issues/show/utils/update_description');
-
-describe('Store', () => {
- let store;
-
- beforeEach(() => {
- store = new Store({
- descriptionHtml: '<p>This is a description</p>',
- });
- });
-
- describe('updateState', () => {
- beforeEach(() => {
- document.body.innerHTML = `
- <div class="detail-page-description content-block">
- <details open>
- <summary>One</summary>
- </details>
- <details>
- <summary>Two</summary>
- </details>
- </div>
- `;
- });
-
- afterEach(() => {
- document.getElementsByTagName('html')[0].innerHTML = '';
- });
-
- it('calls updateDetailsState', () => {
- store.updateState({ description: '' });
-
- expect(updateDescription).toHaveBeenCalledTimes(1);
- });
- });
-});
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index cf2dacb50d8..95658f66d09 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -1,58 +1,45 @@
-import { GlCollapsibleListbox } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+
import SourceBranchDropdown from '~/jira_connect/branches/components/source_branch_dropdown.vue';
import { BRANCHES_PER_PAGE } from '~/jira_connect/branches/constants';
import getProjectQuery from '~/jira_connect/branches/graphql/queries/get_project.query.graphql';
-import { mockProjects } from '../mock_data';
-
-const mockProject = {
- id: 'test',
- repository: {
- branchNames: ['main', 'f-test', 'release'],
- rootRef: 'main',
- },
-};
-const mockSelectedProject = mockProjects[0];
-
-const mockProjectQueryResponse = {
- data: {
- project: mockProject,
- },
-};
-const mockGetProjectQuery = jest.fn().mockResolvedValue(mockProjectQueryResponse);
-const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
+import {
+ mockBranchNames,
+ mockBranchNames2,
+ mockProjects,
+ mockProjectQueryResponse,
+} from '../mock_data';
+
+Vue.use(VueApollo);
describe('SourceBranchDropdown', () => {
let wrapper;
+ const mockSelectedProject = mockProjects[0];
+ const querySuccessHandler = jest.fn().mockResolvedValue(mockProjectQueryResponse());
+ const queryLoadingHandler = jest.fn().mockReturnValue(new Promise(() => {}));
+
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
- const assertListboxItems = () => {
+ const assertListboxItems = (branchNames = mockBranchNames) => {
const listboxItems = findListbox().props('items');
- expect(listboxItems).toHaveLength(mockProject.repository.branchNames.length);
- expect(listboxItems.map((item) => item.text)).toEqual(mockProject.repository.branchNames);
+ expect(listboxItems).toHaveLength(branchNames.length);
+ expect(listboxItems.map((item) => item.text)).toEqual(branchNames);
};
- function createMockApolloProvider({ getProjectQueryLoading = false } = {}) {
- Vue.use(VueApollo);
-
- const mockApollo = createMockApollo([
- [getProjectQuery, getProjectQueryLoading ? mockQueryLoading : mockGetProjectQuery],
- ]);
+ const createComponent = ({ props, handler = querySuccessHandler } = {}) => {
+ const mockApollo = createMockApollo([[getProjectQuery, handler]]);
- return mockApollo;
- }
-
- function createComponent({ mockApollo, props, mountFn = shallowMount } = {}) {
- wrapper = mountFn(SourceBranchDropdown, {
- apolloProvider: mockApollo || createMockApolloProvider(),
+ wrapper = shallowMount(SourceBranchDropdown, {
+ apolloProvider: mockApollo,
propsData: props,
});
- }
+ };
describe('when `selectedProject` prop is not specified', () => {
beforeEach(() => {
@@ -78,6 +65,7 @@ describe('SourceBranchDropdown', () => {
loading: false,
searchable: true,
searching: false,
+ selected: null,
toggleText: 'Select a branch',
});
});
@@ -92,23 +80,26 @@ describe('SourceBranchDropdown', () => {
describe('when branches are loading', () => {
it('sets loading prop to true', () => {
createComponent({
- mockApollo: createMockApolloProvider({ getProjectQueryLoading: true }),
props: { selectedProject: mockSelectedProject },
+ handler: queryLoadingHandler,
});
- expect(findListbox().props('loading')).toEqual(true);
+ expect(findListbox().props('loading')).toBe(true);
});
});
describe('when branches have loaded', () => {
describe('when searching branches', () => {
it('triggers a refetch', async () => {
- createComponent({ mountFn: mount, props: { selectedProject: mockSelectedProject } });
+ createComponent({ props: { selectedProject: mockSelectedProject } });
await waitForPromises();
const mockSearchTerm = 'mai';
+ expect(querySuccessHandler).toHaveBeenCalledTimes(1);
+
await findListbox().vm.$emit('search', mockSearchTerm);
- expect(mockGetProjectQuery).toHaveBeenCalledWith({
+ expect(querySuccessHandler).toHaveBeenCalledTimes(2);
+ expect(querySuccessHandler).toHaveBeenLastCalledWith({
branchNamesLimit: BRANCHES_PER_PAGE,
branchNamesOffset: 0,
branchNamesSearchPattern: `*${mockSearchTerm}*`,
@@ -129,10 +120,15 @@ describe('SourceBranchDropdown', () => {
loading: false,
searchable: true,
searching: false,
+ selected: null,
toggleText: 'Select a branch',
});
});
+ it('disables infinite scroll', () => {
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+ });
+
it('omits monospace styling from listbox', () => {
expect(findListbox().classes()).not.toContain('gl-font-monospace');
});
@@ -142,19 +138,19 @@ describe('SourceBranchDropdown', () => {
});
it("emits `change` event with the repository's `rootRef` by default", () => {
- expect(wrapper.emitted('change')[0]).toEqual([mockProject.repository.rootRef]);
+ expect(wrapper.emitted('change')[0]).toEqual([mockBranchNames[0]]);
});
describe('when selecting a listbox item', () => {
it('emits `change` event with the selected branch name', () => {
- const mockBranchName = mockProject.repository.branchNames[1];
+ const mockBranchName = mockBranchNames[1];
findListbox().vm.$emit('select', mockBranchName);
expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
});
});
describe('when `selectedBranchName` prop is specified', () => {
- const mockBranchName = mockProject.repository.branchNames[2];
+ const mockBranchName = mockBranchNames[2];
beforeEach(() => {
wrapper.setProps({
@@ -162,6 +158,10 @@ describe('SourceBranchDropdown', () => {
});
});
+ it('sets listbox selected to `selectedBranchName`', () => {
+ expect(findListbox().props('selected')).toBe(mockBranchName);
+ });
+
it('sets listbox text to `selectedBranchName` value', () => {
expect(findListbox().props('toggleText')).toBe(mockBranchName);
});
@@ -170,6 +170,66 @@ describe('SourceBranchDropdown', () => {
expect(findListbox().classes()).toContain('gl-font-monospace');
});
});
+
+ describe('when full page of branches returns', () => {
+ const fullPageBranchNames = Array(BRANCHES_PER_PAGE)
+ .fill(1)
+ .map((_, i) => mockBranchNames[i % mockBranchNames.length]);
+
+ beforeEach(async () => {
+ createComponent({
+ props: { selectedProject: mockSelectedProject },
+ handler: () => Promise.resolve(mockProjectQueryResponse(fullPageBranchNames)),
+ });
+ await waitForPromises();
+ });
+
+ it('enables infinite scroll', () => {
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+ });
+ });
+ });
+
+ describe('when loading more branches from infinite scroll', () => {
+ const queryLoadMoreHandler = jest.fn();
+
+ beforeEach(async () => {
+ queryLoadMoreHandler.mockResolvedValueOnce(mockProjectQueryResponse());
+ queryLoadMoreHandler.mockResolvedValueOnce(mockProjectQueryResponse(mockBranchNames2));
+ createComponent({
+ props: { selectedProject: mockSelectedProject },
+ handler: queryLoadMoreHandler,
+ });
+
+ await waitForPromises();
+
+ await findListbox().vm.$emit('bottom-reached');
+ });
+
+ it('sets loading more prop to true', () => {
+ expect(findListbox().props('infiniteScrollLoading')).toBe(true);
+ });
+
+ it('triggers load more query', () => {
+ expect(queryLoadMoreHandler).toHaveBeenLastCalledWith({
+ branchNamesLimit: BRANCHES_PER_PAGE,
+ branchNamesOffset: 3,
+ branchNamesSearchPattern: '*',
+ projectPath: 'test-path',
+ });
+ });
+
+ it('renders available source branches as listbox items', async () => {
+ await waitForPromises();
+
+ assertListboxItems([...mockBranchNames, ...mockBranchNames2]);
+ });
+
+ it('sets loading more prop to false once done', async () => {
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScrollLoading')).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/jira_connect/branches/mock_data.js b/spec/frontend/jira_connect/branches/mock_data.js
index 742ab5392c8..1720e0118c8 100644
--- a/spec/frontend/jira_connect/branches/mock_data.js
+++ b/spec/frontend/jira_connect/branches/mock_data.js
@@ -1,3 +1,6 @@
+export const mockBranchNames = ['main', 'f-test', 'release'];
+export const mockBranchNames2 = ['dev', 'dev-1', 'dev-2'];
+
export const mockProjects = [
{
id: 'test',
@@ -28,3 +31,15 @@ export const mockProjects = [
},
},
];
+
+export const mockProjectQueryResponse = (branchNames = mockBranchNames) => ({
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/27',
+ repository: {
+ branchNames,
+ rootRef: 'main',
+ },
+ },
+ },
+});
diff --git a/spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap b/spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap
index 21c903f064d..af9f827117f 100644
--- a/spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap
+++ b/spec/frontend/jira_connect/subscriptions/components/__snapshots__/group_item_name_spec.js.snap
@@ -2,16 +2,15 @@
exports[`GroupItemName template matches the snapshot 1`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-icon-stub
class="gl-mr-3"
name="folder-o"
size="16"
/>
-
<div
- class="gl-display-none gl-flex-shrink-0 gl-sm-display-flex gl-mr-3"
+ class="gl-display-none gl-flex-shrink-0 gl-mr-3 gl-sm-display-flex"
>
<gl-avatar-stub
alt="avatar"
@@ -22,19 +21,15 @@ exports[`GroupItemName template matches the snapshot 1`] = `
src="avatar.png"
/>
</div>
-
<div>
<span
- class="gl-mr-3 gl-text-gray-900! gl-font-weight-bold"
+ class="gl-font-weight-bold gl-mr-3 gl-text-gray-900!"
>
-
Gitlab Org
-
</span>
-
<div>
<p
- class="gl-mt-2! gl-mb-0 gl-text-gray-600"
+ class="gl-mb-0 gl-mt-2! gl-text-gray-600"
>
Open source software to collaborate on code
</p>
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index abd849b387e..263deb3b616 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -4,23 +4,17 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<table
aria-busy=""
aria-colcount="3"
- class="table b-table gl-table b-table-fixed"
+ class="b-table b-table-fixed gl-table table"
role="table"
>
- <!---->
- <!---->
<thead
- class=""
role="rowgroup"
>
- <!---->
<tr
- class=""
role="row"
>
<th
aria-colindex="1"
- class=""
role="columnheader"
scope="col"
>
@@ -31,7 +25,6 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<th
aria-colindex="2"
aria-label="Arrow"
- class=""
role="columnheader"
scope="col"
>
@@ -39,7 +32,6 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
</th>
<th
aria-colindex="3"
- class=""
role="columnheader"
scope="col"
>
@@ -52,21 +44,17 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<tbody
role="rowgroup"
>
- <!---->
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
Jane Doe
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
<svg
@@ -82,33 +70,26 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
</td>
<td
aria-colindex="3"
- class=""
role="cell"
>
<div
aria-label="The GitLab user to which the Jira user Jane Doe will be mapped"
- class="dropdown b-dropdown gl-dropdown w-100 btn-group"
+ class="b-dropdown btn-group dropdown gl-dropdown w-100"
>
- <!---->
<button
aria-expanded="false"
aria-haspopup="menu"
- class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
+ class="btn btn-default btn-md dropdown-toggle gl-button gl-dropdown-toggle"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-dropdown-button-text"
>
janedoe
</span>
-
<svg
aria-hidden="true"
- class="gl-button-icon dropdown-chevron gl-icon s16"
+ class="dropdown-chevron gl-button-icon gl-icon s16"
data-testid="chevron-down-icon"
role="img"
>
@@ -125,21 +106,15 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<div
class="gl-dropdown-inner"
>
- <!---->
-
- <!---->
-
<div
class="gl-dropdown-contents"
>
- <!---->
-
<div
class="gl-search-box-by-type"
>
<svg
aria-hidden="true"
- class="gl-search-box-by-type-search-icon gl-icon s16"
+ class="gl-icon gl-search-box-by-type-search-icon s16"
data-testid="search-icon"
role="img"
>
@@ -147,17 +122,13 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
href="file-mock#search"
/>
</svg>
-
<input
aria-label="Search"
- class="gl-form-input form-control gl-search-box-by-type-input"
+ class="form-control gl-form-input gl-search-box-by-type-input"
placeholder="Search"
type="search"
/>
-
- <!---->
</div>
-
<li
class="gl-dropdown-text text-secondary"
role="presentation"
@@ -165,33 +136,26 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<p
class="b-dropdown-text"
>
-
- No matches found
-
+ No matches found
</p>
</li>
</div>
-
- <!---->
</div>
</ul>
</div>
</td>
</tr>
<tr
- class=""
role="row"
>
<td
aria-colindex="1"
- class=""
role="cell"
>
Fred Chopin
</td>
<td
aria-colindex="2"
- class=""
role="cell"
>
<svg
@@ -207,33 +171,26 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
</td>
<td
aria-colindex="3"
- class=""
role="cell"
>
<div
aria-label="The GitLab user to which the Jira user Fred Chopin will be mapped"
- class="dropdown b-dropdown gl-dropdown w-100 btn-group"
+ class="b-dropdown btn-group dropdown gl-dropdown w-100"
>
- <!---->
<button
aria-expanded="false"
aria-haspopup="menu"
- class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
+ class="btn btn-default btn-md dropdown-toggle gl-button gl-dropdown-toggle"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-dropdown-button-text"
>
mrgitlab
</span>
-
<svg
aria-hidden="true"
- class="gl-button-icon dropdown-chevron gl-icon s16"
+ class="dropdown-chevron gl-button-icon gl-icon s16"
data-testid="chevron-down-icon"
role="img"
>
@@ -250,21 +207,15 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<div
class="gl-dropdown-inner"
>
- <!---->
-
- <!---->
-
<div
class="gl-dropdown-contents"
>
- <!---->
-
<div
class="gl-search-box-by-type"
>
<svg
aria-hidden="true"
- class="gl-search-box-by-type-search-icon gl-icon s16"
+ class="gl-icon gl-search-box-by-type-search-icon s16"
data-testid="search-icon"
role="img"
>
@@ -272,17 +223,13 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
href="file-mock#search"
/>
</svg>
-
<input
aria-label="Search"
- class="gl-form-input form-control gl-search-box-by-type-input"
+ class="form-control gl-form-input gl-search-box-by-type-input"
placeholder="Search"
type="search"
/>
-
- <!---->
</div>
-
<li
class="gl-dropdown-text text-secondary"
role="presentation"
@@ -290,22 +237,15 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<p
class="b-dropdown-text"
>
-
- No matches found
-
+ No matches found
</p>
</li>
</div>
-
- <!---->
</div>
</ul>
</div>
</td>
</tr>
- <!---->
- <!---->
</tbody>
- <!---->
</table>
`;
diff --git a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
deleted file mode 100644
index 5ecddc7efd6..00000000000
--- a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { GlFilteredSearch } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import {
- OPERATORS_IS,
- TOKEN_TITLE_STATUS,
- TOKEN_TYPE_STATUS,
-} from '~/vue_shared/components/filtered_search_bar/constants';
-import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
-import { mockFailedSearchToken } from '../../mock_data';
-
-describe('Jobs filtered search', () => {
- let wrapper;
-
- const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
- const getSearchToken = (type) =>
- findFilteredSearch()
- .props('availableTokens')
- .find((token) => token.type === type);
-
- const findStatusToken = () => getSearchToken('status');
-
- const createComponent = (props) => {
- wrapper = shallowMount(JobsFilteredSearch, {
- propsData: {
- ...props,
- },
- });
- };
-
- it('displays filtered search', () => {
- createComponent();
-
- expect(findFilteredSearch().exists()).toBe(true);
- });
-
- it('displays status token', () => {
- createComponent();
-
- expect(findStatusToken()).toMatchObject({
- type: TOKEN_TYPE_STATUS,
- icon: 'status',
- title: TOKEN_TITLE_STATUS,
- unique: true,
- operators: OPERATORS_IS,
- });
- });
-
- it('emits filter token to parent component', () => {
- createComponent();
-
- findFilteredSearch().vm.$emit('submit', mockFailedSearchToken);
-
- expect(wrapper.emitted('filterJobsBySearch')).toEqual([[mockFailedSearchToken]]);
- });
-
- it('filtered search value is empty array when no query string is passed', () => {
- createComponent();
-
- expect(findFilteredSearch().props('value')).toEqual([]);
- });
-
- it('filtered search returns correct data shape when passed query string', () => {
- const value = 'SUCCESS';
-
- createComponent({ queryString: { statuses: value } });
-
- expect(findFilteredSearch().props('value')).toEqual([
- { type: TOKEN_TYPE_STATUS, value: { data: value, operator: '=' } },
- ]);
- });
-});
diff --git a/spec/frontend/jobs/components/filtered_search/utils_spec.js b/spec/frontend/jobs/components/filtered_search/utils_spec.js
deleted file mode 100644
index 8440ab42b86..00000000000
--- a/spec/frontend/jobs/components/filtered_search/utils_spec.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { validateQueryString } from '~/jobs/components/filtered_search/utils';
-
-describe('Filtered search utils', () => {
- describe('validateQueryString', () => {
- it.each`
- queryStringObject | expected
- ${{ statuses: 'SUCCESS' }} | ${{ statuses: 'SUCCESS' }}
- ${{ statuses: 'failed' }} | ${{ statuses: 'FAILED' }}
- ${{ wrong: 'SUCCESS' }} | ${null}
- ${{ statuses: 'wrong' }} | ${null}
- ${{ wrong: 'wrong' }} | ${null}
- `(
- 'when provided $queryStringObject, the expected result is $expected',
- ({ queryStringObject, expected }) => {
- expect(validateQueryString(queryStringObject)).toEqual(expected);
- },
- );
- });
-});
diff --git a/spec/frontend/lib/utils/array_utility_spec.js b/spec/frontend/lib/utils/array_utility_spec.js
index 64ddd400114..94461c72106 100644
--- a/spec/frontend/lib/utils/array_utility_spec.js
+++ b/spec/frontend/lib/utils/array_utility_spec.js
@@ -42,4 +42,40 @@ describe('array_utility', () => {
expect(arrayUtils.getDuplicateItemsFromArray(array)).toEqual(result);
});
});
+
+ describe('toggleArrayItem', () => {
+ it('adds an item to the array if it does not exist', () => {
+ expect(arrayUtils.toggleArrayItem([], 'item')).toStrictEqual(['item']);
+ });
+
+ it('removes an item from the array if it already exists', () => {
+ expect(arrayUtils.toggleArrayItem(['item'], 'item')).toStrictEqual([]);
+ });
+
+ describe('pass by value', () => {
+ it('does not toggle the array item when passed a new object', () => {
+ expect(arrayUtils.toggleArrayItem([{ a: 1 }], { a: 1 })).toStrictEqual([
+ { a: 1 },
+ { a: 1 },
+ ]);
+ });
+
+ it('does not toggle the array item when passed a new array', () => {
+ expect(arrayUtils.toggleArrayItem([[1]], [1])).toStrictEqual([[1], [1]]);
+ });
+ });
+
+ describe('pass by reference', () => {
+ const array = [1];
+ const object = { a: 1 };
+
+ it('toggles the array item when passed a object reference', () => {
+ expect(arrayUtils.toggleArrayItem([object], object)).toStrictEqual([]);
+ });
+
+ it('toggles the array item when passed an array reference', () => {
+ expect(arrayUtils.toggleArrayItem([array], array)).toStrictEqual([]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/breadcrumbs_spec.js b/spec/frontend/lib/utils/breadcrumbs_spec.js
new file mode 100644
index 00000000000..3c29e3723d3
--- /dev/null
+++ b/spec/frontend/lib/utils/breadcrumbs_spec.js
@@ -0,0 +1,84 @@
+import { createWrapper } from '@vue/test-utils';
+import Vue from 'vue';
+import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+describe('Breadcrumbs utils', () => {
+ const breadcrumbsHTML = `
+ <nav>
+ <ul class="js-breadcrumbs-list">
+ <li>
+ <a href="/group-name" data-testid="existing-crumb">Group name</a>
+ </li>
+ <li>
+ <a href="/group-name/project-name/-/subpage" data-testid="last-crumb">Subpage</a>
+ </li>
+ </ul>
+ </nav>
+ `;
+
+ const emptyBreadcrumbsHTML = `
+ <nav>
+ <ul class="js-breadcrumbs-list" data-testid="breadcumbs-list">
+ </ul>
+ </nav>
+ `;
+
+ const mockRouter = jest.fn();
+ let MockComponent;
+ let mockApolloProvider;
+
+ beforeEach(() => {
+ MockComponent = Vue.component('MockComponent', {
+ render: (createElement) =>
+ createElement('span', {
+ attrs: {
+ 'data-testid': 'mock-component',
+ },
+ }),
+ });
+ mockApolloProvider = createMockApollo();
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ MockComponent = null;
+ });
+
+ describe('injectVueAppBreadcrumbs', () => {
+ describe('without any breadcrumbs', () => {
+ beforeEach(() => {
+ setHTMLFixture(emptyBreadcrumbsHTML);
+ });
+
+ it('returns early and stops trying to inject', () => {
+ expect(injectVueAppBreadcrumbs(mockRouter, MockComponent)).toBe(false);
+ });
+ });
+
+ describe('with breadcrumbs', () => {
+ beforeEach(() => {
+ setHTMLFixture(breadcrumbsHTML);
+ });
+
+ describe.each`
+ testLabel | apolloProvider
+ ${'set'} | ${mockApolloProvider}
+ ${'not set'} | ${null}
+ `('given the apollo provider is $testLabel', ({ apolloProvider }) => {
+ beforeEach(() => {
+ createWrapper(injectVueAppBreadcrumbs(mockRouter, MockComponent, apolloProvider));
+ });
+
+ it('returns a new breadcrumbs component replacing the inject HTML', () => {
+ // Using `querySelectorAll` because we're not testing a full Vue app.
+ // We are testing a partial Vue app added into the pages HTML.
+ expect(document.querySelectorAll('[data-testid="existing-crumb"]')).toHaveLength(1);
+ expect(document.querySelectorAll('[data-testid="last-crumb"]')).toHaveLength(0);
+ expect(document.querySelectorAll('[data-testid="mock-component"]')).toHaveLength(1);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 444d4a96f9c..8697249ebf5 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1174,4 +1174,43 @@ describe('common_utils', () => {
});
});
});
+
+ describe('cloneWithoutReferences', () => {
+ it('clones the provided object', () => {
+ const obj = {
+ foo: 'bar',
+ cool: 1337,
+ nested: {
+ peanut: 'butter',
+ },
+ arrays: [0, 1, 2],
+ };
+
+ const cloned = commonUtils.cloneWithoutReferences(obj);
+
+ expect(cloned).toMatchObject({
+ foo: 'bar',
+ cool: 1337,
+ nested: {
+ peanut: 'butter',
+ },
+ arrays: [0, 1, 2],
+ });
+ });
+
+ it('does not persist object references after cloning', () => {
+ const ref = {
+ foo: 'bar',
+ };
+
+ const obj = {
+ ref,
+ };
+
+ const cloned = commonUtils.cloneWithoutReferences(obj);
+
+ expect(cloned.ref).toMatchObject({ foo: 'bar' });
+ expect(cloned.ref === ref).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/datetime_range_spec.js b/spec/frontend/lib/utils/datetime_range_spec.js
deleted file mode 100644
index 996a8e2e47b..00000000000
--- a/spec/frontend/lib/utils/datetime_range_spec.js
+++ /dev/null
@@ -1,382 +0,0 @@
-import _ from 'lodash';
-import {
- getRangeType,
- convertToFixedRange,
- isEqualTimeRanges,
- findTimeRange,
- timeRangeToParams,
- timeRangeFromParams,
-} from '~/lib/utils/datetime_range';
-
-const MOCK_NOW = Date.UTC(2020, 0, 23, 20);
-
-const MOCK_NOW_ISO_STRING = new Date(MOCK_NOW).toISOString();
-
-const mockFixedRange = {
- label: 'January 2020',
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-31T23:59:00.000Z',
-};
-
-const mockAnchoredRange = {
- label: 'First two minutes of 2020',
- anchor: '2020-01-01T00:00:00.000Z',
- direction: 'after',
- duration: {
- seconds: 60 * 2,
- },
-};
-
-const mockRollingRange = {
- label: 'Next 2 minutes',
- direction: 'after',
- duration: {
- seconds: 60 * 2,
- },
-};
-
-const mockOpenRange = {
- label: '2020 so far',
- anchor: '2020-01-01T00:00:00.000Z',
- direction: 'after',
-};
-
-describe('Date time range utils', () => {
- describe('getRangeType', () => {
- it('infers correctly the range type from the input object', () => {
- const rangeTypes = {
- fixed: [{ start: MOCK_NOW_ISO_STRING, end: MOCK_NOW_ISO_STRING }],
- anchored: [{ anchor: MOCK_NOW_ISO_STRING, duration: { seconds: 0 } }],
- rolling: [{ duration: { seconds: 0 } }],
- open: [{ anchor: MOCK_NOW_ISO_STRING }],
- invalid: [
- {},
- { start: MOCK_NOW_ISO_STRING },
- { end: MOCK_NOW_ISO_STRING },
- { start: 'NOT_A_DATE', end: 'NOT_A_DATE' },
- { duration: { seconds: 'NOT_A_NUMBER' } },
- { duration: { seconds: Infinity } },
- { duration: { minutes: 20 } },
- { anchor: MOCK_NOW_ISO_STRING, duration: { seconds: 'NOT_A_NUMBER' } },
- { anchor: MOCK_NOW_ISO_STRING, duration: { seconds: Infinity } },
- { junk: 'exists' },
- ],
- };
-
- Object.entries(rangeTypes).forEach(([type, examples]) => {
- examples.forEach((example) => expect(getRangeType(example)).toEqual(type));
- });
- });
- });
-
- describe('convertToFixedRange', () => {
- beforeEach(() => {
- jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW);
- });
-
- afterEach(() => {
- Date.now.mockRestore();
- });
-
- describe('When a fixed range is input', () => {
- it('converts a fixed range to an equal fixed range', () => {
- expect(convertToFixedRange(mockFixedRange)).toEqual({
- start: mockFixedRange.start,
- end: mockFixedRange.end,
- });
- });
-
- it('throws an error when fixed range does not contain an end time', () => {
- const aFixedRangeMissingEnd = _.omit(mockFixedRange, 'end');
-
- expect(() => convertToFixedRange(aFixedRangeMissingEnd)).toThrow();
- });
-
- it('throws an error when fixed range does not contain a start time', () => {
- const aFixedRangeMissingStart = _.omit(mockFixedRange, 'start');
-
- expect(() => convertToFixedRange(aFixedRangeMissingStart)).toThrow();
- });
-
- it('throws an error when the dates cannot be parsed', () => {
- const wrongStart = { ...mockFixedRange, start: 'I_CANNOT_BE_PARSED' };
- const wrongEnd = { ...mockFixedRange, end: 'I_CANNOT_BE_PARSED' };
-
- expect(() => convertToFixedRange(wrongStart)).toThrow();
- expect(() => convertToFixedRange(wrongEnd)).toThrow();
- });
- });
-
- describe('When an anchored range is input', () => {
- it('converts to a fixed range', () => {
- expect(convertToFixedRange(mockAnchoredRange)).toEqual({
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T00:02:00.000Z',
- });
- });
-
- it('converts to a fixed range with a `before` direction', () => {
- expect(convertToFixedRange({ ...mockAnchoredRange, direction: 'before' })).toEqual({
- start: '2019-12-31T23:58:00.000Z',
- end: '2020-01-01T00:00:00.000Z',
- });
- });
-
- it('converts to a fixed range without an explicit direction, defaulting to `before`', () => {
- const defaultDirectionRange = _.omit(mockAnchoredRange, 'direction');
-
- expect(convertToFixedRange(defaultDirectionRange)).toEqual({
- start: '2019-12-31T23:58:00.000Z',
- end: '2020-01-01T00:00:00.000Z',
- });
- });
-
- it('throws an error when the anchor cannot be parsed', () => {
- const wrongAnchor = { ...mockAnchoredRange, anchor: 'I_CANNOT_BE_PARSED' };
-
- expect(() => convertToFixedRange(wrongAnchor)).toThrow();
- });
- });
-
- describe('when a rolling range is input', () => {
- it('converts to a fixed range', () => {
- expect(convertToFixedRange(mockRollingRange)).toEqual({
- start: '2020-01-23T20:00:00.000Z',
- end: '2020-01-23T20:02:00.000Z',
- });
- });
-
- it('converts to a fixed range with an implicit `before` direction', () => {
- const noDirection = _.omit(mockRollingRange, 'direction');
-
- expect(convertToFixedRange(noDirection)).toEqual({
- start: '2020-01-23T19:58:00.000Z',
- end: '2020-01-23T20:00:00.000Z',
- });
- });
-
- it('throws an error when the duration is not in the right format', () => {
- const wrongDuration = { ...mockRollingRange, duration: { minutes: 20 } };
-
- expect(() => convertToFixedRange(wrongDuration)).toThrow();
- });
-
- it('throws an error when the anchor is not valid', () => {
- const wrongAnchor = { ...mockRollingRange, anchor: 'CAN_T_PARSE_THIS' };
-
- expect(() => convertToFixedRange(wrongAnchor)).toThrow();
- });
- });
-
- describe('when an open range is input', () => {
- it('converts to a fixed range with an `after` direction', () => {
- expect(convertToFixedRange(mockOpenRange)).toEqual({
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-23T20:00:00.000Z',
- });
- });
-
- it('converts to a fixed range with the explicit `before` direction', () => {
- const beforeOpenRange = { ...mockOpenRange, direction: 'before' };
-
- expect(convertToFixedRange(beforeOpenRange)).toEqual({
- start: '1970-01-01T00:00:00.000Z',
- end: '2020-01-01T00:00:00.000Z',
- });
- });
-
- it('converts to a fixed range with the implicit `before` direction', () => {
- const noDirectionOpenRange = _.omit(mockOpenRange, 'direction');
-
- expect(convertToFixedRange(noDirectionOpenRange)).toEqual({
- start: '1970-01-01T00:00:00.000Z',
- end: '2020-01-01T00:00:00.000Z',
- });
- });
-
- it('throws an error when the anchor cannot be parsed', () => {
- const wrongAnchor = { ...mockOpenRange, anchor: 'CAN_T_PARSE_THIS' };
-
- expect(() => convertToFixedRange(wrongAnchor)).toThrow();
- });
- });
- });
-
- describe('isEqualTimeRanges', () => {
- it('equal only compares relevant properies', () => {
- expect(
- isEqualTimeRanges(
- {
- ...mockFixedRange,
- label: 'A label',
- default: true,
- },
- {
- ...mockFixedRange,
- label: 'Another label',
- default: false,
- anotherKey: 'anotherValue',
- },
- ),
- ).toBe(true);
-
- expect(
- isEqualTimeRanges(
- {
- ...mockAnchoredRange,
- label: 'A label',
- default: true,
- },
- {
- ...mockAnchoredRange,
- anotherKey: 'anotherValue',
- },
- ),
- ).toBe(true);
- });
- });
-
- describe('findTimeRange', () => {
- const timeRanges = [
- {
- label: 'Before 2020',
- anchor: '2020-01-01T00:00:00.000Z',
- },
- {
- label: 'Last 30 minutes',
- duration: { seconds: 60 * 30 },
- },
- {
- label: 'In 2019',
- start: '2019-01-01T00:00:00.000Z',
- end: '2019-12-31T12:59:59.999Z',
- },
- {
- label: 'Next 2 minutes',
- direction: 'after',
- duration: {
- seconds: 60 * 2,
- },
- },
- ];
-
- it('finds a time range', () => {
- const tr0 = {
- anchor: '2020-01-01T00:00:00.000Z',
- };
- expect(findTimeRange(tr0, timeRanges)).toBe(timeRanges[0]);
-
- const tr1 = {
- duration: { seconds: 60 * 30 },
- };
- expect(findTimeRange(tr1, timeRanges)).toBe(timeRanges[1]);
-
- const tr1Direction = {
- direction: 'before',
- duration: {
- seconds: 60 * 30,
- },
- };
- expect(findTimeRange(tr1Direction, timeRanges)).toBe(timeRanges[1]);
-
- const tr2 = {
- someOtherLabel: 'Added arbitrarily',
- start: '2019-01-01T00:00:00.000Z',
- end: '2019-12-31T12:59:59.999Z',
- };
- expect(findTimeRange(tr2, timeRanges)).toBe(timeRanges[2]);
-
- const tr3 = {
- direction: 'after',
- duration: {
- seconds: 60 * 2,
- },
- };
- expect(findTimeRange(tr3, timeRanges)).toBe(timeRanges[3]);
- });
-
- it('doesnot finds a missing time range', () => {
- const nonExistant = {
- direction: 'before',
- duration: {
- seconds: 200,
- },
- };
- expect(findTimeRange(nonExistant, timeRanges)).toBeUndefined();
- });
- });
-
- describe('conversion to/from params', () => {
- const mockFixedParams = {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-31T23:59:00.000Z',
- };
-
- const mockAnchoredParams = {
- anchor: '2020-01-01T00:00:00.000Z',
- direction: 'after',
- duration_seconds: '120',
- };
-
- const mockRollingParams = {
- direction: 'after',
- duration_seconds: '120',
- };
-
- describe('timeRangeToParams', () => {
- it('converts fixed ranges to params', () => {
- expect(timeRangeToParams(mockFixedRange)).toEqual(mockFixedParams);
- });
-
- it('converts anchored ranges to params', () => {
- expect(timeRangeToParams(mockAnchoredRange)).toEqual(mockAnchoredParams);
- });
-
- it('converts rolling ranges to params', () => {
- expect(timeRangeToParams(mockRollingRange)).toEqual(mockRollingParams);
- });
- });
-
- describe('timeRangeFromParams', () => {
- it('converts fixed ranges from params', () => {
- const params = { ...mockFixedParams, other_param: 'other_value' };
- const expectedRange = _.omit(mockFixedRange, 'label');
-
- expect(timeRangeFromParams(params)).toEqual(expectedRange);
- });
-
- it('converts anchored ranges to params', () => {
- const expectedRange = _.omit(mockRollingRange, 'label');
-
- expect(timeRangeFromParams(mockRollingParams)).toEqual(expectedRange);
- });
-
- it('converts rolling ranges from params', () => {
- const params = { ...mockRollingParams, other_param: 'other_value' };
- const expectedRange = _.omit(mockRollingRange, 'label');
-
- expect(timeRangeFromParams(params)).toEqual(expectedRange);
- });
-
- it('converts rolling ranges from params with a default direction', () => {
- const params = {
- ...mockRollingParams,
- direction: 'before',
- other_param: 'other_value',
- };
- const expectedRange = _.omit(mockRollingRange, 'label', 'direction');
-
- expect(timeRangeFromParams(params)).toEqual(expectedRange);
- });
-
- it('converts to null when for no relevant params', () => {
- const range = {
- useless_param_1: 'value1',
- useless_param_2: 'value2',
- };
-
- expect(timeRangeFromParams(range)).toBe(null);
- });
- });
- });
-});
diff --git a/spec/frontend/lib/utils/secret_detection_spec.js b/spec/frontend/lib/utils/secret_detection_spec.js
index 3213ecf3fe1..761062f0340 100644
--- a/spec/frontend/lib/utils/secret_detection_spec.js
+++ b/spec/frontend/lib/utils/secret_detection_spec.js
@@ -28,6 +28,7 @@ describe('containsSensitiveToken', () => {
'token: feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'token: feed_token=glft-ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'token: feed_token=glft-a8cc74ccb0de004d09a968705ba49099229b288b3de43f26c473a9d8d7fb7693-1234',
+ 'token: gloas-a8cc74ccb0de004d09a968705ba49099229b288b3de43f26c473a9d8d7fb7693',
'https://example.com/feed?feed_token=123456789_abcdefghij',
'glpat-1234567890 and feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
];
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index b7d6bbd3991..6821ed56857 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -221,23 +221,6 @@ describe('text_utility', () => {
});
});
- describe('getFirstCharacterCapitalized', () => {
- it('returns the first character capitalized, if first character is alphabetic', () => {
- expect(textUtils.getFirstCharacterCapitalized('loremIpsumDolar')).toEqual('L');
- expect(textUtils.getFirstCharacterCapitalized('Sit amit !')).toEqual('S');
- });
-
- it('returns the first character, if first character is non-alphabetic', () => {
- expect(textUtils.getFirstCharacterCapitalized(' lorem')).toEqual(' ');
- expect(textUtils.getFirstCharacterCapitalized('%#!')).toEqual('%');
- });
-
- it('returns an empty string, if string is falsey', () => {
- expect(textUtils.getFirstCharacterCapitalized('')).toEqual('');
- expect(textUtils.getFirstCharacterCapitalized(null)).toEqual('');
- });
- });
-
describe('slugifyWithUnderscore', () => {
it('should replaces whitespaces with underscore and convert to lower case', () => {
expect(textUtils.slugifyWithUnderscore('My Input String')).toEqual('my_input_string');
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 450eeefd898..ecd2d7f888d 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1,11 +1,8 @@
-import * as Sentry from '@sentry/browser';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as urlUtils from '~/lib/utils/url_utility';
import { safeUrls, unsafeUrls } from './mock_data';
-jest.mock('@sentry/browser');
-
const shas = {
valid: [
'ad9be38573f9ee4c4daec22673478c2dd1d81cd8',
@@ -434,11 +431,10 @@ describe('URL utility', () => {
it('does not navigate to unsafe urls', () => {
// eslint-disable-next-line no-script-url
const url = 'javascript:alert(document.domain)';
- urlUtils.visitUrl(url);
- expect(Sentry.captureException).toHaveBeenCalledWith(
- new RangeError(`Only http and https protocols are allowed: ${url}`),
- );
+ expect(() => {
+ urlUtils.visitUrl(url);
+ }).toThrow(new RangeError(`Only http and https protocols are allowed: ${url}`));
});
it('navigates to a page', () => {
diff --git a/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
index a0d9bae8a0b..3ad02d3851d 100644
--- a/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
+++ b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
@@ -2,21 +2,14 @@
exports[`MemberActivity with a member that does not have all of the fields renders \`User created\` field 1`] = `
<div>
- <!---->
-
<div>
<strong>
Access granted:
</strong>
-
<span>
-
- Aug 06, 2020
-
+ Aug 06, 2020
</span>
</div>
-
- <!---->
</div>
`;
@@ -26,35 +19,24 @@ exports[`MemberActivity with a member that has all fields renders \`User created
<strong>
User created:
</strong>
-
<span>
-
- Mar 10, 2022
-
+ Mar 10, 2022
</span>
</div>
-
<div>
<strong>
Access granted:
</strong>
-
<span>
-
- Jul 17, 2020
-
+ Jul 17, 2020
</span>
</div>
-
<div>
<strong>
Last activity:
</strong>
-
<span>
-
- Mar 15, 2022
-
+ Mar 15, 2022
</span>
</div>
</div>
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 3b8c9dd3bf3..6c4ea7063ad 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -281,6 +281,14 @@ describe('MergeRequestTabs', () => {
testContext.class.expandViewContainer();
expect($('.content-wrapper .container-limited')).toHaveLength(0);
});
+
+ it('adds the diff-specific width-limiter', () => {
+ testContext.class.expandViewContainer();
+
+ expect(testContext.class.contentWrapper.classList.contains('diffs-container-limited')).toBe(
+ true,
+ );
+ });
});
describe('resetViewContainer', () => {
@@ -302,6 +310,14 @@ describe('MergeRequestTabs', () => {
expect($('.content-wrapper .container-limited')).toHaveLength(1);
});
+
+ it('removes the diff-specific width-limiter', () => {
+ testContext.class.resetViewContainer();
+
+ expect(testContext.class.contentWrapper.classList.contains('diffs-container-limited')).toBe(
+ false,
+ );
+ });
});
describe('tabShown', () => {
diff --git a/spec/frontend/merge_requests/components/compare_app_spec.js b/spec/frontend/merge_requests/components/compare_app_spec.js
index ba129363ffd..887f79f9fad 100644
--- a/spec/frontend/merge_requests/components/compare_app_spec.js
+++ b/spec/frontend/merge_requests/components/compare_app_spec.js
@@ -1,10 +1,14 @@
-import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
import CompareApp from '~/merge_requests/components/compare_app.vue';
let wrapper;
+let mock;
function factory(provideData = {}) {
- wrapper = shallowMount(CompareApp, {
+ wrapper = shallowMountExtended(CompareApp, {
provide: {
inputs: {
project: {
@@ -16,6 +20,7 @@ function factory(provideData = {}) {
name: 'branch',
},
},
+ branchCommitPath: '/commit',
toggleClass: {
project: 'project',
branch: 'branch',
@@ -29,7 +34,18 @@ function factory(provideData = {}) {
});
}
+const findCommitBox = () => wrapper.findByTestId('commit-box');
+
describe('Merge requests compare app component', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/commit').reply(200, 'commit content');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
it('shows commit box when selected branch is empty', () => {
factory({
currentBranch: {
@@ -38,9 +54,41 @@ describe('Merge requests compare app component', () => {
},
});
- const commitBox = wrapper.find('[data-testid="commit-box"]');
+ const commitBox = findCommitBox();
expect(commitBox.exists()).toBe(true);
expect(commitBox.text()).toBe('Select a branch to compare');
});
+
+ it('emits select-branch on selected event', () => {
+ factory({
+ currentBranch: {
+ text: '',
+ value: '',
+ },
+ });
+
+ wrapper.findByTestId('compare-dropdown').vm.$emit('selected', { value: 'main' });
+
+ expect(wrapper.emitted('select-branch')).toEqual([['main']]);
+ });
+
+ describe('currentBranch watcher', () => {
+ it('changes selected value', async () => {
+ factory({
+ currentBranch: {
+ text: '',
+ value: '',
+ },
+ });
+
+ expect(findCommitBox().text()).toBe('Select a branch to compare');
+
+ wrapper.setProps({ currentBranch: { text: 'main', value: 'main ' } });
+
+ await waitForPromises();
+
+ expect(findCommitBox().text()).toBe('commit content');
+ });
+ });
});
diff --git a/spec/frontend/merge_requests/components/header_metadata_spec.js b/spec/frontend/merge_requests/components/header_metadata_spec.js
new file mode 100644
index 00000000000..2823b4b9d97
--- /dev/null
+++ b/spec/frontend/merge_requests/components/header_metadata_spec.js
@@ -0,0 +1,93 @@
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import HeaderMetadata from '~/merge_requests/components/header_metadata.vue';
+import mrStore from '~/mr_notes/stores';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores'));
+
+describe('HeaderMetadata component', () => {
+ let wrapper;
+
+ const findConfidentialIcon = () => wrapper.findComponent(ConfidentialityBadge);
+ const findLockedIcon = () => wrapper.findByTestId('locked');
+ const findHiddenIcon = () => wrapper.findByTestId('hidden');
+
+ const renderTestMessage = (renders) => (renders ? 'renders' : 'does not render');
+
+ const createComponent = ({ store, provide }) => {
+ wrapper = shallowMountExtended(HeaderMetadata, {
+ mocks: {
+ $store: store,
+ },
+ provide,
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ describe.each`
+ lockStatus | confidentialStatus | hiddenStatus
+ ${true} | ${true} | ${false}
+ ${true} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ ${false} | ${false} | ${false}
+ ${true} | ${true} | ${true}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ ${false} | ${false} | ${true}
+ `(
+ `when locked=$lockStatus, confidential=$confidentialStatus, and hidden=$hiddenStatus`,
+ ({ lockStatus, confidentialStatus, hiddenStatus }) => {
+ const store = mrStore;
+
+ beforeEach(() => {
+ store.getters.getNoteableData = {};
+ store.getters.getNoteableData.confidential = confidentialStatus;
+ store.getters.getNoteableData.discussion_locked = lockStatus;
+ store.getters.getNoteableData.targetType = 'merge_request';
+
+ createComponent({ store, provide: { hidden: hiddenStatus } });
+ });
+
+ it(`${renderTestMessage(lockStatus)} the locked icon`, () => {
+ const lockedIcon = findLockedIcon();
+
+ expect(lockedIcon.exists()).toBe(lockStatus);
+
+ if (lockStatus) {
+ expect(lockedIcon.attributes('title')).toBe(
+ `This merge request is locked. Only project members can comment.`,
+ );
+ expect(getBinding(lockedIcon.element, 'gl-tooltip')).not.toBeUndefined();
+ }
+ });
+
+ it(`${renderTestMessage(confidentialStatus)} the confidential icon`, () => {
+ const confidentialIcon = findConfidentialIcon();
+ expect(confidentialIcon.exists()).toBe(confidentialStatus);
+
+ if (confidentialStatus && !hiddenStatus) {
+ expect(confidentialIcon.props()).toMatchObject({
+ workspaceType: 'project',
+ issuableType: 'issue',
+ });
+ }
+ });
+
+ it(`${renderTestMessage(confidentialStatus)} the hidden icon`, () => {
+ const hiddenIcon = findHiddenIcon();
+
+ expect(hiddenIcon.exists()).toBe(hiddenStatus);
+
+ if (hiddenStatus) {
+ expect(hiddenIcon.attributes('title')).toBe(
+ `This merge request is hidden because its author has been banned`,
+ );
+ expect(getBinding(hiddenIcon.element, 'gl-tooltip')).not.toBeUndefined();
+ }
+ });
+ },
+ );
+});
diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
index 2cd65307b0b..432ee5e9ecd 100644
--- a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
+++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
@@ -57,7 +57,8 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
if (type === 'divider') {
return { type };
- } else if (type === 'header') {
+ }
+ if (type === 'header') {
return { type, text: child.text() };
}
diff --git a/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap b/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
index a4611149432..277ff2aa441 100644
--- a/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
+++ b/spec/frontend/notes/components/__snapshots__/notes_app_spec.js.snap
@@ -1,17 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`note_app when sort direction is asc shows skeleton notes after the loaded discussions 1`] = `
-"<ul id=\\"notes-list\\" class=\\"notes main-notes-list timeline\\">
- <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\" shouldscrolltonote=\\"true\\"></noteable-discussion-stub>
- <skeleton-loading-container-stub class=\\"note-skeleton\\"></skeleton-loading-container-stub>
- <!---->
-</ul>"
+<ul
+ class="main-notes-list notes timeline"
+ id="reference-0"
+>
+ <noteable-discussion-stub
+ discussion="[object Object]"
+ helppagepath=""
+ isoverviewtab="true"
+ renderdifffile="true"
+ shouldscrolltonote="true"
+ />
+ <skeleton-loading-container-stub
+ class="note-skeleton"
+ />
+</ul>
`;
exports[`note_app when sort direction is desc shows skeleton notes before the loaded discussions 1`] = `
-"<ul id=\\"notes-list\\" class=\\"notes main-notes-list timeline\\">
- <skeleton-loading-container-stub class=\\"note-skeleton\\"></skeleton-loading-container-stub>
- <noteable-discussion-stub discussion=\\"[object Object]\\" renderdifffile=\\"true\\" helppagepath=\\"\\" isoverviewtab=\\"true\\" shouldscrolltonote=\\"true\\"></noteable-discussion-stub>
- <!---->
-</ul>"
+<ul
+ class="main-notes-list notes timeline"
+ id="reference-0"
+>
+ <skeleton-loading-container-stub
+ class="note-skeleton"
+ />
+ <noteable-discussion-stub
+ discussion="[object Object]"
+ helppagepath=""
+ isoverviewtab="true"
+ renderdifffile="true"
+ shouldscrolltonote="true"
+ />
+</ul>
`;
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 0728646246d..9b1678c0a8a 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -153,21 +153,18 @@ describe('issue_comment_form component', () => {
mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- jest.spyOn(wrapper.vm, 'stopPolling');
findCloseReopenButton().trigger('click');
expect(wrapper.vm.isSubmitting).toBe(true);
expect(wrapper.vm.note).toBe('');
expect(wrapper.vm.saveNote).toHaveBeenCalled();
- expect(wrapper.vm.stopPolling).toHaveBeenCalled();
});
it('tracks event', () => {
mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- jest.spyOn(wrapper.vm, 'stopPolling');
findCloseReopenButton().trigger('click');
@@ -302,7 +299,6 @@ describe('issue_comment_form component', () => {
const saveNotePromise = Promise.resolve();
jest.spyOn(wrapper.vm, 'saveNote').mockReturnValue(saveNotePromise);
- jest.spyOn(wrapper.vm, 'stopPolling');
const actionButton = findCloseReopenButton();
@@ -351,7 +347,6 @@ describe('issue_comment_form component', () => {
it('should make textarea disabled while requesting', async () => {
mountComponent({ mountFunction: mount });
- jest.spyOn(wrapper.vm, 'stopPolling');
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
findMarkdownEditor().vm.$emit('input', 'hello world');
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index caf47febedd..d49ab0d71db 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -288,7 +288,6 @@ describe('note_app', () => {
wrapper.vm.$store.hotUpdate({
actions: {
toggleAward: toggleAwardAction,
- stopPolling() {},
},
});
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 0205f606297..104c297b44e 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -8,11 +8,7 @@ import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
-import {
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
- HTTP_STATUS_OK,
- HTTP_STATUS_SERVICE_UNAVAILABLE,
-} from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
import * as actions from '~/notes/stores/actions';
@@ -24,7 +20,6 @@ import updateMergeRequestLockMutation from '~/sidebar/queries/update_merge_reque
import promoteTimelineEvent from '~/notes/graphql/promote_timeline_event.mutation.graphql';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
import notesEventHub from '~/notes/event_hub';
-import waitForPromises from 'helpers/wait_for_promises';
import { resetStore } from '../helpers';
import {
discussionMock,
@@ -262,13 +257,7 @@ describe('Actions Notes Store', () => {
});
describe('initPolling', () => {
- afterEach(() => {
- gon.features = {};
- });
-
it('creates the Action Cable subscription', () => {
- gon.features = { actionCableNotes: true };
-
store.dispatch('setNotesData', notesDataMock);
store.dispatch('initPolling');
@@ -290,8 +279,6 @@ describe('Actions Notes Store', () => {
const response = { notes: [], last_fetched_at: '123456' };
const successMock = () =>
axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_OK, response);
- const failureMock = () =>
- axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
beforeEach(() => {
return store.dispatch('setNotesData', notesDataMock);
@@ -304,153 +291,6 @@ describe('Actions Notes Store', () => {
expect(store.state.lastFetchedAt).toBe('123456');
});
-
- it('shows an alert when fetching fails', async () => {
- failureMock();
-
- await store.dispatch('fetchUpdatedNotes');
-
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('poll', () => {
- const pollInterval = 6000;
- const pollResponse = { notes: [], last_fetched_at: '123456' };
- const pollHeaders = { 'poll-interval': `${pollInterval}` };
- const successMock = () =>
- axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_OK, pollResponse, pollHeaders);
- const failureMock = () =>
- axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- const advanceAndRAF = (time) => {
- if (time) {
- jest.advanceTimersByTime(time);
- }
-
- return waitForPromises();
- };
- const advanceXMoreIntervals = (number) => {
- const timeoutLength = pollInterval * number;
-
- return advanceAndRAF(timeoutLength);
- };
- const startPolling = async () => {
- await store.dispatch('poll');
- await advanceAndRAF(2);
- };
- const cleanUp = () => {
- jest.clearAllTimers();
-
- return store.dispatch('stopPolling');
- };
-
- beforeEach(() => {
- return store.dispatch('setNotesData', notesDataMock);
- });
-
- afterEach(() => {
- return cleanUp();
- });
-
- it('calls service with last fetched state', async () => {
- successMock();
-
- await startPolling();
-
- expect(store.state.lastFetchedAt).toBe('123456');
-
- await advanceXMoreIntervals(1);
-
- expect(axiosMock.history.get).toHaveLength(2);
- expect(axiosMock.history.get[1].headers).toMatchObject({
- 'X-Last-Fetched-At': '123456',
- });
- });
-
- describe('polling side effects', () => {
- it('retries twice', async () => {
- failureMock();
-
- await startPolling();
-
- // This is the first request, not a retry
- expect(axiosMock.history.get).toHaveLength(1);
-
- await advanceXMoreIntervals(1);
-
- // Retry #1
- expect(axiosMock.history.get).toHaveLength(2);
-
- await advanceXMoreIntervals(1);
-
- // Retry #2
- expect(axiosMock.history.get).toHaveLength(3);
-
- await advanceXMoreIntervals(10);
-
- // There are no more retries
- expect(axiosMock.history.get).toHaveLength(3);
- });
-
- it('shows the error display on the second failure', async () => {
- failureMock();
-
- await startPolling();
-
- expect(axiosMock.history.get).toHaveLength(1);
- expect(createAlert).not.toHaveBeenCalled();
-
- await advanceXMoreIntervals(1);
-
- expect(axiosMock.history.get).toHaveLength(2);
- expect(createAlert).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
-
- it('resets the failure counter on success', async () => {
- // We can't get access to the actual counter in the polling closure.
- // So we can infer that it's reset by ensuring that the error is only
- // shown when we cause two failures in a row - no successes between
-
- axiosMock
- .onGet(notesDataMock.notesPath)
- .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR) // cause one error
- .onGet(notesDataMock.notesPath)
- .replyOnce(HTTP_STATUS_OK, pollResponse, pollHeaders) // then a success
- .onGet(notesDataMock.notesPath)
- .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); // and then more errors
-
- await startPolling(); // Failure #1
- await advanceXMoreIntervals(1); // Success #1
- await advanceXMoreIntervals(1); // Failure #2
-
- // That was the first failure AFTER a success, so we should NOT see the error displayed
- expect(createAlert).not.toHaveBeenCalled();
-
- // Now we'll allow another failure
- await advanceXMoreIntervals(1); // Failure #3
-
- // Since this is the second failure in a row, the error should happen
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
-
- it('hides the error display if it exists on success', async () => {
- failureMock();
-
- await startPolling();
- await advanceXMoreIntervals(2);
-
- // After two errors, the error should be displayed
- expect(createAlert).toHaveBeenCalledTimes(1);
-
- axiosMock.reset();
- successMock();
-
- await advanceXMoreIntervals(1);
-
- expect(mockAlertDismiss).toHaveBeenCalledTimes(1);
- });
- });
});
describe('setNotesFetchedState', () => {
@@ -996,11 +836,7 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(dispatch.mock.calls).toEqual([
- ['stopPolling'],
- ['resolveDiscussion', { discussionId }],
- ['restartPolling'],
- ]);
+ expect(dispatch.mock.calls).toEqual([['resolveDiscussion', { discussionId }]]);
expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -1015,7 +851,6 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
expect(createAlert).toHaveBeenCalledWith({
message: TEST_ERROR_MESSAGE,
parent: flashContainer,
@@ -1033,7 +868,6 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, true],
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while applying the suggestion. Please try again.',
parent: flashContainer,
@@ -1081,10 +915,8 @@ describe('Actions Notes Store', () => {
]);
expect(dispatch.mock.calls).toEqual([
- ['stopPolling'],
['resolveDiscussion', { discussionId: discussionIds[0] }],
['resolveDiscussion', { discussionId: discussionIds[1] }],
- ['restartPolling'],
]);
expect(createAlert).not.toHaveBeenCalled();
@@ -1104,7 +936,6 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
expect(createAlert).toHaveBeenCalledWith({
message: TEST_ERROR_MESSAGE,
parent: flashContainer,
@@ -1125,7 +956,6 @@ describe('Actions Notes Store', () => {
[mutationTypes.SET_RESOLVING_DISCUSSION, false],
]);
- expect(dispatch.mock.calls).toEqual([['stopPolling'], ['restartPolling']]);
expect(createAlert).toHaveBeenCalledWith({
message:
'Something went wrong while applying the batch of suggestions. Please try again.',
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
index 10fdc8c33c4..056175eac07 100644
--- a/spec/frontend/observability/client_spec.js
+++ b/spec/frontend/observability/client_spec.js
@@ -1,15 +1,19 @@
import MockAdapter from 'axios-mock-adapter';
+import * as Sentry from '@sentry/browser';
import { buildClient } from '~/observability/client';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/lib/utils/axios_utils');
+jest.mock('@sentry/browser');
describe('buildClient', () => {
let client;
let axiosMock;
const tracingUrl = 'https://example.com/tracing';
- const EXPECTED_ERROR_MESSAGE = 'traces are missing/invalid in the response';
+ const provisioningUrl = 'https://example.com/provisioning';
+
+ const FETCHING_TRACES_ERROR = 'traces are missing/invalid in the response';
beforeEach(() => {
axiosMock = new MockAdapter(axios);
@@ -17,7 +21,7 @@ describe('buildClient', () => {
client = buildClient({
tracingUrl,
- provisioningUrl: 'https://example.com/provisioning',
+ provisioningUrl,
});
});
@@ -25,10 +29,85 @@ describe('buildClient', () => {
axiosMock.restore();
});
+ describe('isTracingEnabled', () => {
+ it('returns true if requests succeedes', async () => {
+ axiosMock.onGet(provisioningUrl).reply(200, {
+ status: 'ready',
+ });
+
+ const enabled = await client.isTracingEnabled();
+
+ expect(enabled).toBe(true);
+ });
+
+ it('returns false if response is 404', async () => {
+ axiosMock.onGet(provisioningUrl).reply(404);
+
+ const enabled = await client.isTracingEnabled();
+
+ expect(enabled).toBe(false);
+ });
+
+ // we currently ignore the 'status' payload and just check if the request was successful
+ // We might improve this as part of https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2315
+ it('returns true for any status', async () => {
+ axiosMock.onGet(provisioningUrl).reply(200, {
+ status: 'not ready',
+ });
+
+ const enabled = await client.isTracingEnabled();
+
+ expect(enabled).toBe(true);
+ });
+
+ it('throws in case of any non-404 error', async () => {
+ axiosMock.onGet(provisioningUrl).reply(500);
+
+ const e = 'Request failed with status code 500';
+ await expect(client.isTracingEnabled()).rejects.toThrow(e);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ });
+
+ it('throws in case of unexpected response', async () => {
+ axiosMock.onGet(provisioningUrl).reply(200, {});
+
+ const e = 'Failed to check provisioning';
+ await expect(client.isTracingEnabled()).rejects.toThrow(e);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ });
+ });
+
+ describe('enableTraces', () => {
+ it('makes a PUT request to the provisioning URL', async () => {
+ let putConfig;
+ axiosMock.onPut(provisioningUrl).reply((config) => {
+ putConfig = config;
+ return [200];
+ });
+
+ await client.enableTraces();
+
+ expect(putConfig.withCredentials).toBe(true);
+ });
+
+ it('reports an error if the req fails', async () => {
+ axiosMock.onPut(provisioningUrl).reply(401);
+
+ const e = 'Request failed with status code 401';
+
+ await expect(client.enableTraces()).rejects.toThrow(e);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ });
+ });
+
describe('fetchTrace', () => {
it('fetches the trace from the tracing URL', async () => {
const mockTraces = [
- { trace_id: 'trace-1', spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }] },
+ {
+ trace_id: 'trace-1',
+ duration_nano: 3000,
+ spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }],
+ },
];
axiosMock.onGet(tracingUrl).reply(200, {
@@ -42,34 +121,37 @@ describe('buildClient', () => {
withCredentials: true,
params: { trace_id: 'trace-1' },
});
- expect(result).toEqual({
- ...mockTraces[0],
- duration: 1,
- });
+ expect(result).toEqual(mockTraces[0]);
});
it('rejects if trace id is missing', () => {
return expect(client.fetchTrace()).rejects.toThrow('traceId is required.');
});
- it('rejects if traces are empty', () => {
+ it('rejects if traces are empty', async () => {
axiosMock.onGet(tracingUrl).reply(200, { traces: [] });
- return expect(client.fetchTrace('trace-1')).rejects.toThrow(EXPECTED_ERROR_MESSAGE);
+ await expect(client.fetchTrace('trace-1')).rejects.toThrow(FETCHING_TRACES_ERROR);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
});
- it('rejects if traces are invalid', () => {
+ it('rejects if traces are invalid', async () => {
axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
- return expect(client.fetchTraces()).rejects.toThrow(EXPECTED_ERROR_MESSAGE);
+ await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
});
});
describe('fetchTraces', () => {
it('fetches traces from the tracing URL', async () => {
const mockTraces = [
- { trace_id: 'trace-1', spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }] },
- { trace_id: 'trace-2', spans: [{ duration_nano: 2000 }] },
+ {
+ trace_id: 'trace-1',
+ duration_nano: 3000,
+ spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }],
+ },
+ { trace_id: 'trace-2', duration_nano: 3000, spans: [{ duration_nano: 2000 }] },
];
axiosMock.onGet(tracingUrl).reply(200, {
@@ -83,28 +165,21 @@ describe('buildClient', () => {
withCredentials: true,
params: new URLSearchParams(),
});
- expect(result).toEqual([
- {
- ...mockTraces[0],
- duration: 1,
- },
- {
- ...mockTraces[1],
- duration: 2,
- },
- ]);
+ expect(result).toEqual(mockTraces);
});
- it('rejects if traces are missing', () => {
+ it('rejects if traces are missing', async () => {
axiosMock.onGet(tracingUrl).reply(200, {});
- return expect(client.fetchTraces()).rejects.toThrow(EXPECTED_ERROR_MESSAGE);
+ await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
});
- it('rejects if traces are invalid', () => {
+ it('rejects if traces are invalid', async () => {
axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
- return expect(client.fetchTraces()).rejects.toThrow(EXPECTED_ERROR_MESSAGE);
+ await expect(client.fetchTraces()).rejects.toThrow(FETCHING_TRACES_ERROR);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(FETCHING_TRACES_ERROR));
});
describe('query filter', () => {
diff --git a/spec/frontend/organizations/groups_and_projects/components/app_spec.js b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
index 64182b74e4f..e2301de8607 100644
--- a/spec/frontend/organizations/groups_and_projects/components/app_spec.js
+++ b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
@@ -1,10 +1,9 @@
import { GlCollapsibleListbox, GlSorting, GlSortingItem } from '@gitlab/ui';
import App from '~/organizations/groups_and_projects/components/app.vue';
-import GroupsPage from '~/organizations/groups_and_projects/components/groups_page.vue';
-import ProjectsPage from '~/organizations/groups_and_projects/components/projects_page.vue';
+import GroupsView from '~/organizations/shared/components/groups_view.vue';
+import ProjectsView from '~/organizations/shared/components/projects_view.vue';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
import {
- DISPLAY_QUERY_GROUPS,
- DISPLAY_QUERY_PROJECTS,
SORT_ITEM_CREATED,
SORT_DIRECTION_DESC,
} from '~/organizations/groups_and_projects/constants';
@@ -36,10 +35,10 @@ describe('GroupsAndProjectsApp', () => {
describe.each`
display | expectedComponent | expectedDisplayListboxSelectedProp
- ${null} | ${GroupsPage} | ${DISPLAY_QUERY_GROUPS}
- ${'unsupported_value'} | ${GroupsPage} | ${DISPLAY_QUERY_GROUPS}
- ${DISPLAY_QUERY_GROUPS} | ${GroupsPage} | ${DISPLAY_QUERY_GROUPS}
- ${DISPLAY_QUERY_PROJECTS} | ${ProjectsPage} | ${DISPLAY_QUERY_PROJECTS}
+ ${null} | ${GroupsView} | ${RESOURCE_TYPE_GROUPS}
+ ${'unsupported_value'} | ${GroupsView} | ${RESOURCE_TYPE_GROUPS}
+ ${RESOURCE_TYPE_GROUPS} | ${GroupsView} | ${RESOURCE_TYPE_GROUPS}
+ ${RESOURCE_TYPE_PROJECTS} | ${ProjectsView} | ${RESOURCE_TYPE_PROJECTS}
`(
'when `display` query string is $display',
({ display, expectedComponent, expectedDisplayListboxSelectedProp }) => {
@@ -122,11 +121,11 @@ describe('GroupsAndProjectsApp', () => {
beforeEach(() => {
createComponent();
- findListbox().vm.$emit('select', DISPLAY_QUERY_PROJECTS);
+ findListbox().vm.$emit('select', RESOURCE_TYPE_PROJECTS);
});
it('updates `display` query string', () => {
- expect(routerMock.push).toHaveBeenCalledWith({ query: { display: DISPLAY_QUERY_PROJECTS } });
+ expect(routerMock.push).toHaveBeenCalledWith({ query: { display: RESOURCE_TYPE_PROJECTS } });
});
});
diff --git a/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js b/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js
deleted file mode 100644
index 537f8114fcf..00000000000
--- a/spec/frontend/organizations/groups_and_projects/components/groups_page_spec.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import VueApollo from 'vue-apollo';
-import Vue from 'vue';
-import { GlLoadingIcon } from '@gitlab/ui';
-import GroupsPage from '~/organizations/groups_and_projects/components/groups_page.vue';
-import { formatGroups } from '~/organizations/groups_and_projects/utils';
-import resolvers from '~/organizations/groups_and_projects/graphql/resolvers';
-import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
-import { createAlert } from '~/alert';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { organizationGroups } from '../mock_data';
-
-jest.mock('~/alert');
-
-Vue.use(VueApollo);
-jest.useFakeTimers();
-
-describe('GroupsPage', () => {
- let wrapper;
- let mockApollo;
-
- const createComponent = ({ mockResolvers = resolvers } = {}) => {
- mockApollo = createMockApollo([], mockResolvers);
-
- wrapper = shallowMountExtended(GroupsPage, { apolloProvider: mockApollo });
- };
-
- afterEach(() => {
- mockApollo = null;
- });
-
- describe('when API call is loading', () => {
- beforeEach(() => {
- const mockResolvers = {
- Query: {
- organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
- },
- };
-
- createComponent({ mockResolvers });
- });
-
- it('renders loading icon', () => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
-
- describe('when API call is successful', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders `GroupsList` component and passes correct props', async () => {
- jest.runAllTimers();
- await waitForPromises();
-
- expect(wrapper.findComponent(GroupsList).props()).toEqual({
- groups: formatGroups(organizationGroups.nodes),
- showGroupIcon: true,
- });
- });
- });
-
- describe('when API call is not successful', () => {
- const error = new Error();
-
- beforeEach(() => {
- const mockResolvers = {
- Query: {
- organization: jest.fn().mockRejectedValueOnce(error),
- },
- };
-
- createComponent({ mockResolvers });
- });
-
- it('displays error alert', async () => {
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({
- message: GroupsPage.i18n.errorMessage,
- error,
- captureError: true,
- });
- });
- });
-});
diff --git a/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js b/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js
deleted file mode 100644
index 7cadcab5021..00000000000
--- a/spec/frontend/organizations/groups_and_projects/components/projects_page_spec.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import VueApollo from 'vue-apollo';
-import Vue from 'vue';
-import { GlLoadingIcon } from '@gitlab/ui';
-import ProjectsPage from '~/organizations/groups_and_projects/components/projects_page.vue';
-import { formatProjects } from '~/organizations/groups_and_projects/utils';
-import resolvers from '~/organizations/groups_and_projects/graphql/resolvers';
-import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
-import { createAlert } from '~/alert';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { organizationProjects } from '../mock_data';
-
-jest.mock('~/alert');
-
-Vue.use(VueApollo);
-jest.useFakeTimers();
-
-describe('ProjectsPage', () => {
- let wrapper;
- let mockApollo;
-
- const createComponent = ({ mockResolvers = resolvers } = {}) => {
- mockApollo = createMockApollo([], mockResolvers);
-
- wrapper = shallowMountExtended(ProjectsPage, { apolloProvider: mockApollo });
- };
-
- afterEach(() => {
- mockApollo = null;
- });
-
- describe('when API call is loading', () => {
- beforeEach(() => {
- const mockResolvers = {
- Query: {
- organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
- },
- };
-
- createComponent({ mockResolvers });
- });
-
- it('renders loading icon', () => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
-
- describe('when API call is successful', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders `ProjectsList` component and passes correct props', async () => {
- jest.runAllTimers();
- await waitForPromises();
-
- expect(wrapper.findComponent(ProjectsList).props()).toEqual({
- projects: formatProjects(organizationProjects.nodes),
- showProjectIcon: true,
- });
- });
- });
-
- describe('when API call is not successful', () => {
- const error = new Error();
-
- beforeEach(() => {
- const mockResolvers = {
- Query: {
- organization: jest.fn().mockRejectedValueOnce(error),
- },
- };
-
- createComponent({ mockResolvers });
- });
-
- it('displays error alert', async () => {
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({
- message: ProjectsPage.i18n.errorMessage,
- error,
- captureError: true,
- });
- });
- });
-});
diff --git a/spec/frontend/organizations/groups_and_projects/mock_data.js b/spec/frontend/organizations/groups_and_projects/mock_data.js
deleted file mode 100644
index eb829a24f50..00000000000
--- a/spec/frontend/organizations/groups_and_projects/mock_data.js
+++ /dev/null
@@ -1,252 +0,0 @@
-export const organization = {
- id: 'gid://gitlab/Organization/1',
- __typename: 'Organization',
-};
-
-export const organizationProjects = {
- nodes: [
- {
- id: 'gid://gitlab/Project/8',
- nameWithNamespace: 'Twitter / Typeahead.Js',
- webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
- topics: ['JavaScript', 'Vue.js'],
- forksCount: 4,
- avatarUrl: null,
- starCount: 0,
- visibility: 'public',
- openIssuesCount: 48,
- descriptionHtml:
- '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
- issuesAccessLevel: 'enabled',
- forkingAccessLevel: 'enabled',
- isForked: true,
- accessLevel: {
- integerValue: 30,
- },
- },
- {
- id: 'gid://gitlab/Project/7',
- nameWithNamespace: 'Flightjs / Flight',
- webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
- topics: [],
- forksCount: 0,
- avatarUrl: null,
- starCount: 0,
- visibility: 'private',
- openIssuesCount: 37,
- descriptionHtml:
- '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>',
- issuesAccessLevel: 'enabled',
- forkingAccessLevel: 'enabled',
- isForked: false,
- accessLevel: {
- integerValue: 20,
- },
- },
- {
- id: 'gid://gitlab/Project/6',
- nameWithNamespace: 'Jashkenas / Underscore',
- webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
- topics: [],
- forksCount: 0,
- avatarUrl: null,
- starCount: 0,
- visibility: 'private',
- openIssuesCount: 34,
- descriptionHtml:
- '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>',
- issuesAccessLevel: 'enabled',
- forkingAccessLevel: 'enabled',
- isForked: false,
- accessLevel: {
- integerValue: 40,
- },
- },
- {
- id: 'gid://gitlab/Project/5',
- nameWithNamespace: 'Commit451 / Lab Coat',
- webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
- topics: [],
- forksCount: 0,
- avatarUrl: null,
- starCount: 0,
- visibility: 'internal',
- openIssuesCount: 49,
- descriptionHtml:
- '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>',
- issuesAccessLevel: 'enabled',
- forkingAccessLevel: 'enabled',
- isForked: false,
- accessLevel: {
- integerValue: 10,
- },
- },
- {
- id: 'gid://gitlab/Project/1',
- nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
- webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',
- topics: [],
- forksCount: 0,
- avatarUrl: null,
- starCount: 0,
- visibility: 'internal',
- openIssuesCount: 34,
- descriptionHtml:
- '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>',
- issuesAccessLevel: 'enabled',
- forkingAccessLevel: 'enabled',
- isForked: false,
- accessLevel: {
- integerValue: 30,
- },
- },
- ],
-};
-
-export const organizationGroups = {
- nodes: [
- {
- id: 'gid://gitlab/Group/29',
- fullName: 'Commit451',
- parent: null,
- webUrl: 'http://127.0.0.1:3000/groups/Commit451',
- descriptionHtml:
- '<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa.</p>',
- avatarUrl: null,
- descendantGroupsCount: 0,
- projectsCount: 3,
- groupMembersCount: 2,
- visibility: 'public',
- accessLevel: {
- integerValue: 30,
- },
- },
- {
- id: 'gid://gitlab/Group/33',
- fullName: 'Flightjs',
- parent: null,
- webUrl: 'http://127.0.0.1:3000/groups/flightjs',
- descriptionHtml:
- '<p data-sourcepos="1:1-1:60" dir="auto">Ipsa reiciendis deleniti officiis illum nostrum quo aliquam.</p>',
- avatarUrl: null,
- descendantGroupsCount: 4,
- projectsCount: 3,
- groupMembersCount: 1,
- visibility: 'private',
- accessLevel: {
- integerValue: 20,
- },
- },
- {
- id: 'gid://gitlab/Group/24',
- fullName: 'Gitlab Org',
- parent: null,
- webUrl: 'http://127.0.0.1:3000/groups/gitlab-org',
- descriptionHtml:
- '<p data-sourcepos="1:1-1:64" dir="auto">Dolorem dolorem omnis impedit cupiditate pariatur officia velit.</p>',
- avatarUrl: null,
- descendantGroupsCount: 1,
- projectsCount: 1,
- groupMembersCount: 2,
- visibility: 'internal',
- accessLevel: {
- integerValue: 10,
- },
- },
- {
- id: 'gid://gitlab/Group/27',
- fullName: 'Gnuwget',
- parent: null,
- webUrl: 'http://127.0.0.1:3000/groups/gnuwgetf',
- descriptionHtml:
- '<p data-sourcepos="1:1-1:47" dir="auto">Culpa soluta aut eius dolores est vel sapiente.</p>',
- avatarUrl: null,
- descendantGroupsCount: 4,
- projectsCount: 2,
- groupMembersCount: 3,
- visibility: 'public',
- accessLevel: {
- integerValue: 40,
- },
- },
- {
- id: 'gid://gitlab/Group/31',
- fullName: 'Jashkenas',
- parent: null,
- webUrl: 'http://127.0.0.1:3000/groups/jashkenas',
- descriptionHtml: '<p data-sourcepos="1:1-1:25" dir="auto">Ut ut id aliquid nostrum.</p>',
- avatarUrl: null,
- descendantGroupsCount: 3,
- projectsCount: 3,
- groupMembersCount: 10,
- visibility: 'private',
- accessLevel: {
- integerValue: 10,
- },
- },
- {
- id: 'gid://gitlab/Group/22',
- fullName: 'Toolbox',
- parent: null,
- webUrl: 'http://127.0.0.1:3000/groups/toolbox',
- descriptionHtml:
- '<p data-sourcepos="1:1-1:46" dir="auto">Quo voluptatem magnam facere voluptates alias.</p>',
- avatarUrl: null,
- descendantGroupsCount: 2,
- projectsCount: 3,
- groupMembersCount: 40,
- visibility: 'internal',
- accessLevel: {
- integerValue: 30,
- },
- },
- {
- id: 'gid://gitlab/Group/35',
- fullName: 'Twitter',
- parent: null,
- webUrl: 'http://127.0.0.1:3000/groups/twitter',
- descriptionHtml:
- '<p data-sourcepos="1:1-1:40" dir="auto">Quae nulla consequatur assumenda id quo.</p>',
- avatarUrl: null,
- descendantGroupsCount: 20,
- projectsCount: 30,
- groupMembersCount: 100,
- visibility: 'public',
- accessLevel: {
- integerValue: 40,
- },
- },
- {
- id: 'gid://gitlab/Group/73',
- fullName: 'test',
- parent: null,
- webUrl: 'http://127.0.0.1:3000/groups/test',
- descriptionHtml: '',
- avatarUrl: null,
- descendantGroupsCount: 1,
- projectsCount: 1,
- groupMembersCount: 1,
- visibility: 'private',
- accessLevel: {
- integerValue: 30,
- },
- },
- {
- id: 'gid://gitlab/Group/74',
- fullName: 'Twitter / test subgroup',
- parent: {
- id: 'gid://gitlab/Group/35',
- },
- webUrl: 'http://127.0.0.1:3000/groups/twitter/test-subgroup',
- descriptionHtml: '',
- avatarUrl: null,
- descendantGroupsCount: 4,
- projectsCount: 4,
- groupMembersCount: 4,
- visibility: 'internal',
- accessLevel: {
- integerValue: 20,
- },
- },
- ],
-};
diff --git a/spec/frontend/organizations/shared/components/groups_view_spec.js b/spec/frontend/organizations/shared/components/groups_view_spec.js
new file mode 100644
index 00000000000..8d6ea60ffd2
--- /dev/null
+++ b/spec/frontend/organizations/shared/components/groups_view_spec.js
@@ -0,0 +1,146 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
+import GroupsView from '~/organizations/shared/components/groups_view.vue';
+import { formatGroups } from '~/organizations/shared/utils';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { organizationGroups } from '~/organizations/mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+
+describe('GroupsView', () => {
+ let wrapper;
+ let mockApollo;
+
+ const defaultProvide = {
+ groupsEmptyStateSvgPath: 'illustrations/empty-state/empty-groups-md.svg',
+ newGroupPath: '/groups/new',
+ };
+
+ const createComponent = ({ mockResolvers = resolvers, propsData = {} } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(GroupsView, {
+ apolloProvider: mockApollo,
+ provide: defaultProvide,
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ describe('when API call is loading', () => {
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('renders loading icon', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when API call is successful', () => {
+ describe('when there are no groups', () => {
+ it('renders empty state without buttons by default', async () => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockResolvedValueOnce({
+ groups: { nodes: [] },
+ }),
+ },
+ };
+ createComponent({ mockResolvers });
+
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: "You don't have any groups yet.",
+ description:
+ 'A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
+ svgHeight: 144,
+ svgPath: defaultProvide.groupsEmptyStateSvgPath,
+ primaryButtonLink: null,
+ primaryButtonText: null,
+ });
+ });
+
+ describe('when `shouldShowEmptyStateButtons` is `true` and `groupsEmptyStateSvgPath` is set', () => {
+ it('renders empty state with buttons', async () => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockResolvedValueOnce({
+ groups: { nodes: [] },
+ }),
+ },
+ };
+ createComponent({ mockResolvers, propsData: { shouldShowEmptyStateButtons: true } });
+
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ primaryButtonLink: defaultProvide.newGroupPath,
+ primaryButtonText: 'New group',
+ });
+ });
+ });
+ });
+
+ describe('when there are groups', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `GroupsList` component and passes correct props', async () => {
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GroupsList).props()).toEqual({
+ groups: formatGroups(organizationGroups.nodes),
+ showGroupIcon: true,
+ });
+ });
+ });
+ });
+
+ describe('when API call is not successful', () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('displays error alert', async () => {
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: GroupsView.i18n.errorMessage,
+ error,
+ captureError: true,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js
new file mode 100644
index 00000000000..490b0c89348
--- /dev/null
+++ b/spec/frontend/organizations/shared/components/projects_view_spec.js
@@ -0,0 +1,146 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import ProjectsView from '~/organizations/shared/components/projects_view.vue';
+import { formatProjects } from '~/organizations/shared/utils';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { organizationProjects } from '~/organizations/mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+
+describe('ProjectsView', () => {
+ let wrapper;
+ let mockApollo;
+
+ const defaultProvide = {
+ projectsEmptyStateSvgPath: 'illustrations/empty-state/empty-projects-md.svg',
+ newProjectPath: '/projects/new',
+ };
+
+ const createComponent = ({ mockResolvers = resolvers, propsData = {} } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(ProjectsView, {
+ apolloProvider: mockApollo,
+ provide: defaultProvide,
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ describe('when API call is loading', () => {
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('renders loading icon', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when API call is successful', () => {
+ describe('when there are no projects', () => {
+ it('renders empty state without buttons by default', async () => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockResolvedValueOnce({
+ projects: { nodes: [] },
+ }),
+ },
+ };
+ createComponent({ mockResolvers });
+
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ title: "You don't have any projects yet.",
+ description:
+ 'Projects are where you can store your code, access issues, wiki, and other features of Gitlab.',
+ svgHeight: 144,
+ svgPath: defaultProvide.projectsEmptyStateSvgPath,
+ primaryButtonLink: null,
+ primaryButtonText: null,
+ });
+ });
+
+ describe('when `shouldShowEmptyStateButtons` is `true` and `projectsEmptyStateSvgPath` is set', () => {
+ it('renders empty state with buttons', async () => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockResolvedValueOnce({
+ projects: { nodes: [] },
+ }),
+ },
+ };
+ createComponent({ mockResolvers, propsData: { shouldShowEmptyStateButtons: true } });
+
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
+ primaryButtonLink: defaultProvide.newProjectPath,
+ primaryButtonText: 'New project',
+ });
+ });
+ });
+ });
+
+ describe('when there are projects', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ProjectsList` component and passes correct props', async () => {
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(ProjectsList).props()).toEqual({
+ projects: formatProjects(organizationProjects.nodes),
+ showProjectIcon: true,
+ });
+ });
+ });
+ });
+
+ describe('when API call is not successful', () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('displays error alert', async () => {
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: ProjectsView.i18n.errorMessage,
+ error,
+ captureError: true,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/groups_and_projects/utils_spec.js b/spec/frontend/organizations/shared/utils_spec.js
index 2cb1ee02061..778a18ab2bc 100644
--- a/spec/frontend/organizations/groups_and_projects/utils_spec.js
+++ b/spec/frontend/organizations/shared/utils_spec.js
@@ -1,7 +1,7 @@
-import { formatProjects, formatGroups } from '~/organizations/groups_and_projects/utils';
-import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/projects_list/constants';
+import { formatProjects, formatGroups } from '~/organizations/shared/utils';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { organizationProjects, organizationGroups } from './mock_data';
+import { organizationProjects, organizationGroups } from '~/organizations/mock_data';
describe('formatProjects', () => {
it('correctly formats the projects', () => {
@@ -17,7 +17,7 @@ describe('formatProjects', () => {
accessLevel: firstMockProject.accessLevel.integerValue,
},
},
- actions: [ACTION_EDIT, ACTION_DELETE],
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
});
expect(formattedProjects.length).toBe(organizationProjects.nodes.length);
});
@@ -29,7 +29,11 @@ describe('formatGroups', () => {
const formattedGroups = formatGroups(organizationGroups.nodes);
const [firstFormattedGroup] = formattedGroups;
- expect(firstFormattedGroup.id).toBe(getIdFromGraphQLId(firstMockGroup.id));
+ expect(firstFormattedGroup).toMatchObject({
+ id: getIdFromGraphQLId(firstMockGroup.id),
+ editPath: `${firstFormattedGroup.webUrl}/-/edit`,
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
+ });
expect(formattedGroups.length).toBe(organizationGroups.nodes.length);
});
});
diff --git a/spec/frontend/organizations/show/components/app_spec.js b/spec/frontend/organizations/show/components/app_spec.js
new file mode 100644
index 00000000000..46496e40bdd
--- /dev/null
+++ b/spec/frontend/organizations/show/components/app_spec.js
@@ -0,0 +1,49 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import App from '~/organizations/show/components/app.vue';
+import OrganizationAvatar from '~/organizations/show/components/organization_avatar.vue';
+import GroupsAndProjects from '~/organizations/show/components/groups_and_projects.vue';
+import AssociationCount from '~/organizations/show/components/association_counts.vue';
+
+describe('OrganizationShowApp', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ organization: {
+ id: 1,
+ name: 'GitLab',
+ },
+ associationCounts: {
+ groups: 10,
+ projects: 5,
+ users: 6,
+ },
+ groupsAndProjectsOrganizationPath: '/-/organizations/default/groups_and_projects',
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(App, { propsData: defaultPropsData });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders organization avatar and passes organization prop', () => {
+ expect(wrapper.findComponent(OrganizationAvatar).props('organization')).toEqual(
+ defaultPropsData.organization,
+ );
+ });
+
+ it('renders groups and projects component and passes `groupsAndProjectsOrganizationPath` prop', () => {
+ expect(
+ wrapper.findComponent(GroupsAndProjects).props('groupsAndProjectsOrganizationPath'),
+ ).toEqual(defaultPropsData.groupsAndProjectsOrganizationPath);
+ });
+
+ it('renders associations count component and passes expected props', () => {
+ expect(wrapper.findComponent(AssociationCount).props()).toEqual({
+ associationCounts: defaultPropsData.associationCounts,
+ groupsAndProjectsOrganizationPath: defaultPropsData.groupsAndProjectsOrganizationPath,
+ });
+ });
+});
diff --git a/spec/frontend/organizations/show/components/association_count_card_spec.js b/spec/frontend/organizations/show/components/association_count_card_spec.js
new file mode 100644
index 00000000000..752a02110b6
--- /dev/null
+++ b/spec/frontend/organizations/show/components/association_count_card_spec.js
@@ -0,0 +1,48 @@
+import { GlCard, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AssociationCountCard from '~/organizations/show/components/association_count_card.vue';
+
+describe('AssociationCountCard', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ title: 'Groups',
+ iconName: 'group',
+ count: 1050,
+ linkHref: '/-/organizations/default/groups_and_projects?display=groups',
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMountExtended(AssociationCountCard, {
+ propsData: { ...defaultPropsData, ...propsData },
+ });
+ };
+
+ const findCard = () => wrapper.findComponent(GlCard);
+ const findLink = () => findCard().findComponent(GlLink);
+
+ it('renders card with title, link and count', () => {
+ createComponent();
+
+ const card = findCard();
+ const link = findLink();
+
+ expect(card.text()).toContain(defaultPropsData.title);
+ expect(card.text()).toContain('1k');
+ expect(link.text()).toBe('View all');
+ expect(link.attributes('href')).toBe(defaultPropsData.linkHref);
+ });
+
+ describe('when `linkText` prop is set', () => {
+ const linkText = 'Manage';
+ beforeEach(() => {
+ createComponent({
+ propsData: { linkText },
+ });
+ });
+
+ it('sets link text', () => {
+ expect(findLink().text()).toBe(linkText);
+ });
+ });
+});
diff --git a/spec/frontend/organizations/show/components/association_counts_spec.js b/spec/frontend/organizations/show/components/association_counts_spec.js
new file mode 100644
index 00000000000..80e57ede502
--- /dev/null
+++ b/spec/frontend/organizations/show/components/association_counts_spec.js
@@ -0,0 +1,61 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AssociationCounts from '~/organizations/show/components/association_counts.vue';
+import AssociationCountCard from '~/organizations/show/components/association_count_card.vue';
+
+describe('AssociationCounts', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ associationCounts: {
+ groups: 10,
+ projects: 5,
+ users: 6,
+ },
+ groupsAndProjectsOrganizationPath: '/-/organizations/default/groups_and_projects',
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMountExtended(AssociationCounts, {
+ propsData: { ...defaultPropsData, ...propsData },
+ });
+ };
+
+ const findAssociationCountCardAt = (index) =>
+ wrapper.findAllComponents(AssociationCountCard).at(index);
+
+ it('renders groups association count card', () => {
+ createComponent();
+
+ expect(findAssociationCountCardAt(0).props()).toEqual({
+ title: 'Groups',
+ iconName: 'group',
+ count: defaultPropsData.associationCounts.groups,
+ linkText: 'View all',
+ linkHref: '/-/organizations/default/groups_and_projects?display=groups',
+ });
+ });
+
+ it('renders projects association count card', () => {
+ createComponent();
+
+ expect(findAssociationCountCardAt(1).props()).toEqual({
+ title: 'Projects',
+ iconName: 'project',
+ count: defaultPropsData.associationCounts.projects,
+ linkText: 'View all',
+ linkHref: '/-/organizations/default/groups_and_projects?display=projects',
+ });
+ });
+
+ it('renders users association count card', () => {
+ createComponent();
+
+ expect(findAssociationCountCardAt(2).props()).toEqual({
+ title: 'Users',
+ iconName: 'users',
+ count: defaultPropsData.associationCounts.users,
+ linkText: 'Manage',
+ linkHref: '/',
+ });
+ });
+});
diff --git a/spec/frontend/organizations/show/components/groups_and_projects_spec.js b/spec/frontend/organizations/show/components/groups_and_projects_spec.js
new file mode 100644
index 00000000000..83970d4e76d
--- /dev/null
+++ b/spec/frontend/organizations/show/components/groups_and_projects_spec.js
@@ -0,0 +1,106 @@
+import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GroupsAndProjects from '~/organizations/show/components/groups_and_projects.vue';
+import { createRouter } from '~/organizations/show';
+import GroupsView from '~/organizations/shared/components/groups_view.vue';
+import ProjectsView from '~/organizations/shared/components/projects_view.vue';
+
+describe('OrganizationShowGroupsAndProjects', () => {
+ const router = createRouter();
+ const routerMock = {
+ push: jest.fn(),
+ };
+ const defaultPropsData = {
+ groupsAndProjectsOrganizationPath: '/-/organizations/default/groups_and_projects',
+ };
+
+ let wrapper;
+
+ const createComponent = ({ routeQuery = {} } = {}) => {
+ wrapper = shallowMountExtended(GroupsAndProjects, {
+ router,
+ mocks: { $route: { path: '/', query: routeQuery }, $router: routerMock },
+ propsData: defaultPropsData,
+ });
+ };
+
+ const findCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ it('renders listbox with expected props', () => {
+ createComponent();
+
+ expect(findCollapsibleListbox().props()).toMatchObject({
+ items: [
+ {
+ value: 'frequently_visited_projects',
+ text: 'Frequently visited projects',
+ },
+ {
+ value: 'frequently_visited_groups',
+ text: 'Frequently visited groups',
+ },
+ ],
+ selected: 'frequently_visited_projects',
+ });
+ });
+
+ describe.each`
+ displayQueryParam | expectedViewAllLinkQuery | expectedViewComponent | expectedDisplayListboxSelectedProp
+ ${'frequently_visited_projects'} | ${'?display=projects'} | ${ProjectsView} | ${'frequently_visited_projects'}
+ ${'frequently_visited_groups'} | ${'?display=groups'} | ${GroupsView} | ${'frequently_visited_groups'}
+ ${'unsupported'} | ${'?display=projects'} | ${ProjectsView} | ${'frequently_visited_projects'}
+ `(
+ 'when display query param is $displayQueryParam',
+ ({
+ displayQueryParam,
+ expectedViewAllLinkQuery,
+ expectedViewComponent,
+ expectedDisplayListboxSelectedProp,
+ }) => {
+ beforeEach(() => {
+ createComponent({ routeQuery: { display: displayQueryParam } });
+ });
+
+ it('sets listbox `selected` prop correctly', () => {
+ expect(findCollapsibleListbox().props('selected')).toBe(expectedDisplayListboxSelectedProp);
+ });
+
+ it('renders "View all" link with correct href', () => {
+ expect(wrapper.findComponent(GlLink).attributes('href')).toBe(
+ `${defaultPropsData.groupsAndProjectsOrganizationPath}${expectedViewAllLinkQuery}`,
+ );
+ });
+
+ it('renders expected view', () => {
+ expect(
+ wrapper.findComponent(expectedViewComponent).props('shouldShowEmptyStateButtons'),
+ ).toBe(true);
+ });
+ },
+ );
+
+ it('renders label and associates listbox with it', () => {
+ createComponent();
+
+ const expectedId = 'display-listbox-label';
+
+ expect(wrapper.findByTestId('label').attributes('id')).toBe(expectedId);
+ expect(findCollapsibleListbox().props('toggleAriaLabelledBy')).toBe(expectedId);
+ });
+
+ describe('when listbox item is selected', () => {
+ const selectValue = 'frequently_visited_groups';
+
+ beforeEach(() => {
+ createComponent();
+
+ findCollapsibleListbox().vm.$emit('select', selectValue);
+ });
+
+ it('updates `display` query param', () => {
+ expect(routerMock.push).toHaveBeenCalledWith({
+ query: { display: selectValue },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/show/components/organization_avatar_spec.js b/spec/frontend/organizations/show/components/organization_avatar_spec.js
new file mode 100644
index 00000000000..c98fa14e49b
--- /dev/null
+++ b/spec/frontend/organizations/show/components/organization_avatar_spec.js
@@ -0,0 +1,64 @@
+import { GlAvatar, GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import OrganizationAvatar from '~/organizations/show/components/organization_avatar.vue';
+import {
+ VISIBILITY_TYPE_ICON,
+ ORGANIZATION_VISIBILITY_TYPE,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+} from '~/visibility_level/constants';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+describe('OrganizationAvatar', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ organization: {
+ id: 1,
+ name: 'GitLab',
+ },
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(OrganizationAvatar, {
+ propsData: defaultPropsData,
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders avatar', () => {
+ expect(wrapper.findComponent(GlAvatar).props()).toMatchObject({
+ entityId: defaultPropsData.organization.id,
+ entityName: defaultPropsData.organization.name,
+ });
+ });
+
+ it('renders organization name', () => {
+ expect(
+ wrapper.findByRole('heading', { name: defaultPropsData.organization.name }).exists(),
+ ).toBe(true);
+ });
+
+ it('renders visibility icon', () => {
+ const icon = wrapper.findComponent(GlIcon);
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+
+ expect(icon.props('name')).toBe(VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PUBLIC_STRING]);
+ expect(tooltip.value).toBe(ORGANIZATION_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING]);
+ });
+
+ it('renders button to copy organization ID', () => {
+ expect(wrapper.findComponent(ClipboardButton).props()).toMatchObject({
+ category: 'tertiary',
+ title: 'Copy organization ID',
+ text: '1',
+ size: 'small',
+ });
+ });
+});
diff --git a/spec/frontend/organizations/show/utils_spec.js b/spec/frontend/organizations/show/utils_spec.js
new file mode 100644
index 00000000000..583f105c8c0
--- /dev/null
+++ b/spec/frontend/organizations/show/utils_spec.js
@@ -0,0 +1,20 @@
+import { buildDisplayListboxItem } from '~/organizations/show/utils';
+import { RESOURCE_TYPE_PROJECTS } from '~/organizations/constants';
+import { FILTER_FREQUENTLY_VISITED } from '~/organizations/show/constants';
+
+describe('buildDisplayListboxItem', () => {
+ it('returns list item in correct format', () => {
+ const text = 'Frequently visited projects';
+
+ expect(
+ buildDisplayListboxItem({
+ filter: FILTER_FREQUENTLY_VISITED,
+ resourceType: RESOURCE_TYPE_PROJECTS,
+ text,
+ }),
+ ).toEqual({
+ text,
+ value: 'frequently_visited_projects',
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
index 5f191ef5561..771fb9e4e08 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap
@@ -10,7 +10,6 @@ exports[`TagsLoader component has the correct markup 1`] = `
x="0"
y="12.5"
/>
-
<rect
height="20"
rx="4"
@@ -18,13 +17,11 @@ exports[`TagsLoader component has the correct markup 1`] = `
x="25"
y="10"
/>
-
<circle
cx="290"
cy="20"
r="10"
/>
-
<rect
height="20"
rx="4"
@@ -32,7 +29,6 @@ exports[`TagsLoader component has the correct markup 1`] = `
x="315"
y="10"
/>
-
<rect
height="20"
rx="4"
@@ -40,7 +36,6 @@ exports[`TagsLoader component has the correct markup 1`] = `
x="500"
y="10"
/>
-
<rect
height="20"
rx="4"
@@ -48,7 +43,6 @@ exports[`TagsLoader component has the correct markup 1`] = `
x="630"
y="10"
/>
-
<rect
height="40"
rx="4"
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
index 56579847468..3e136c750cd 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Registry Group Empty state to match the default snapshot 1`] = `
<div>
<p>
- With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here.
+ With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here.
<gl-link-stub
href="baz"
target="_blank"
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
index 4b52e84d1a6..5a6d84734a0 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Registry Project Empty state to match the default snapshot 1`] = `
<div>
<p>
- With the Container Registry, every project can have its own space to store its Docker images.
+ With the Container Registry, every project can have its own space to store its Docker images.
<gl-link-stub
href="baz"
target="_blank"
@@ -11,29 +11,26 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
More Information
</gl-link-stub>
</p>
-
<h5>
CLI Commands
</h5>
-
<p>
- If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have
+ If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have
<gl-link-stub
href="barBaz"
target="_blank"
>
Two-Factor Authentication
</gl-link-stub>
- enabled, use a
+ enabled, use a
<gl-link-stub
href="fooBaz"
target="_blank"
>
Personal Access Token
</gl-link-stub>
- instead of a password.
+ instead of a password.
</p>
-
<gl-form-input-group-stub
class="gl-mb-4"
inputclass=""
@@ -47,15 +44,11 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
value="bazbaz"
/>
</gl-form-input-group-stub>
-
<p
class="gl-mb-4"
>
-
- You can add an image to this registry with the following commands:
-
+ You can add an image to this registry with the following commands:
</p>
-
<gl-form-input-group-stub
class="gl-mb-4"
inputclass=""
@@ -69,7 +62,6 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
value="foofoo"
/>
</gl-form-input-group-stub>
-
<gl-form-input-group-stub
inputclass=""
predefinedoptions="[object Object]"
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index f590cff0312..dd70fca9dd2 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -1,12 +1,12 @@
import {
GlAlert,
- GlDropdown,
- GlDropdownItem,
GlFormInputGroup,
GlFormGroup,
GlModal,
GlSprintf,
GlSkeletonLoader,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
} from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -18,12 +18,13 @@ import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
-
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
-
+import createRouter from '~/packages_and_registries/dependency_proxy/router';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data';
@@ -37,6 +38,7 @@ Vue.use(VueApollo);
describe('DependencyProxyApp', () => {
let wrapper;
+ let router;
let apolloProvider;
let resolver;
let mock;
@@ -53,15 +55,14 @@ describe('DependencyProxyApp', () => {
const requestHandlers = [[getDependencyProxyDetailsQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
+ router = createRouter('/');
wrapper = shallowMountExtended(DependencyProxyApp, {
apolloProvider,
provide,
+ router,
stubs: {
GlAlert,
- GlDropdown,
- GlDropdownItem,
- GlFormInputGroup,
GlFormGroup,
GlModal,
GlSprintf,
@@ -79,7 +80,7 @@ describe('DependencyProxyApp', () => {
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
const findManifestList = () => wrapper.findComponent(ManifestsList);
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findClearCacheDropdownList = () => wrapper.findComponent(GlDropdown);
+ const findClearCacheDropdownList = () => wrapper.findComponent(GlDisclosureDropdown);
const findClearCacheModal = () => wrapper.findComponent(GlModal);
const findClearCacheAlert = () => wrapper.findComponent(GlAlert);
const findSettingsLink = () => wrapper.findByTestId('settings-link');
@@ -94,6 +95,7 @@ describe('DependencyProxyApp', () => {
mock = new MockAdapter(axios);
mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED, {});
+ setWindowLocation(TEST_HOST);
});
afterEach(() => {
@@ -123,6 +125,13 @@ describe('DependencyProxyApp', () => {
return waitForPromises();
});
+ it('resolver is called with right arguments', () => {
+ expect(resolver).toHaveBeenCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ fullPath: provideDefaults.groupPath,
+ });
+ });
+
it('renders a form group with a label', () => {
expect(findFormGroup().attributes('label')).toBe(
DependencyProxyApp.i18n.proxyImagePrefix,
@@ -225,6 +234,7 @@ describe('DependencyProxyApp', () => {
fullPath: provideDefaults.groupPath,
last: GRAPHQL_PAGE_SIZE,
});
+ expect(window.location.search).toBe(`?before=${pagination().startCursor}`);
});
});
@@ -252,6 +262,7 @@ describe('DependencyProxyApp', () => {
first: GRAPHQL_PAGE_SIZE,
fullPath: provideDefaults.groupPath,
});
+ expect(window.location.search).toBe(`?after=${pagination().endCursor}`);
});
});
@@ -270,7 +281,7 @@ describe('DependencyProxyApp', () => {
expect(findClearCacheDropdownList().exists()).toBe(true);
const clearCacheDropdownItem = findClearCacheDropdownList().findComponent(
- GlDropdownItem,
+ GlDisclosureDropdownItem,
);
expect(clearCacheDropdownItem.text()).toBe('Clear cache');
@@ -315,6 +326,48 @@ describe('DependencyProxyApp', () => {
});
});
});
+
+ describe('pagination params', () => {
+ it('after is set from the url params', async () => {
+ setWindowLocation('?after=1234');
+ createComponent();
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ after: '1234',
+ fullPath: provideDefaults.groupPath,
+ });
+ });
+
+ it('before is set from the url params', async () => {
+ setWindowLocation('?before=1234');
+ createComponent();
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenCalledWith({
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: '1234',
+ fullPath: provideDefaults.groupPath,
+ });
+ });
+
+ describe('when url params are changed', () => {
+ it('after is set from the url params', async () => {
+ createComponent();
+ await waitForPromises();
+ router.push('?after=1234');
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ after: '1234',
+ fullPath: provideDefaults.groupPath,
+ });
+ });
+ });
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/utils_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/utils_spec.js
new file mode 100644
index 00000000000..72072c08537
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/utils_spec.js
@@ -0,0 +1,25 @@
+import { getPageParams } from '~/packages_and_registries/dependency_proxy/utils';
+
+describe('getPageParams', () => {
+ it('should return the previous page params if before cursor is available', () => {
+ const pageInfo = { before: 'abc123' };
+ expect(getPageParams(pageInfo)).toEqual({
+ first: null,
+ before: pageInfo.before,
+ last: 20,
+ });
+ });
+
+ it('should return the next page params if after cursor is available', () => {
+ const pageInfo = { after: 'abc123' };
+ expect(getPageParams(pageInfo)).toEqual({
+ after: pageInfo.after,
+ first: 20,
+ });
+ });
+
+ it('should return an empty object if both before and after cursors are not available', () => {
+ const pageInfo = {};
+ expect(getPageParams(pageInfo)).toEqual({});
+ });
+});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
index f95564e3fad..8e757c136ec 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/file_sha_spec.js.snap
@@ -2,18 +2,13 @@
exports[`FileSha renders 1`] = `
<div
- class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all gl-py-2 gl-border-b-solid gl-border-gray-100 gl-border-b-1"
+ class="gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-gray-100 gl-display-flex gl-font-monospace gl-font-sm gl-py-2 gl-word-break-all"
>
- <!---->
-
<span>
<div
class="gl-px-4"
>
-
- bar:
- foo
-
+ bar: foo
<gl-button-stub
aria-label="Copy SHA"
aria-live="polite"
@@ -22,7 +17,7 @@ exports[`FileSha renders 1`] = `
data-clipboard-handle-tooltip="false"
data-clipboard-text="foo"
icon="copy-to-clipboard"
- id="clipboard-button-1"
+ id="reference-0"
size="small"
title="Copy SHA"
variant="default"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/terraform_installation_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/terraform_installation_spec.js.snap
index 03236737572..30fe6544057 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/terraform_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/__snapshots__/terraform_installation_spec.js.snap
@@ -7,7 +7,6 @@ exports[`TerraformInstallation renders all the messages 1`] = `
>
Provision instructions
</h3>
-
<code-instruction-stub
copytext="Copy Terraform Command"
instruction="module \\"my_module_name\\" {
@@ -19,13 +18,11 @@ exports[`TerraformInstallation renders all the messages 1`] = `
trackingaction=""
trackinglabel=""
/>
-
<h3
class="gl-font-lg"
>
Registry setup
</h3>
-
<code-instruction-stub
copytext="Copy Terraform Setup Command"
instruction="credentials \\"bar.dev\\" {
@@ -36,7 +33,6 @@ exports[`TerraformInstallation renders all the messages 1`] = `
trackingaction=""
trackinglabel=""
/>
-
<gl-sprintf-stub
message="For more information on the Terraform registry, %{linkStart}see our documentation%{linkEnd}."
/>
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index d0841c6110f..7f26ed778a5 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -6,12 +6,10 @@ exports[`packages_list_app renders 1`] = `
count="1"
helpurl="foo"
/>
-
<infrastructure-search-stub />
-
<div>
<section
- class="gl-display-flex empty-state gl-text-center gl-flex-direction-column"
+ class="empty-state gl-display-flex gl-flex-direction-column gl-text-center"
>
<div
class="gl-max-w-full"
@@ -21,15 +19,14 @@ exports[`packages_list_app renders 1`] = `
>
<img
alt=""
- class="gl-max-w-full gl-dark-invert-keep-hue"
+ class="gl-dark-invert-keep-hue gl-max-w-full"
role="img"
src="helpSvg"
/>
</div>
</div>
-
<div
- class="gl-max-w-full gl-m-auto"
+ class="gl-m-auto gl-max-w-full"
data-testid="gl-empty-state-content"
>
<div
@@ -38,15 +35,12 @@ exports[`packages_list_app renders 1`] = `
<h1
class="gl-font-size-h-display gl-line-height-36 h4"
>
-
- There are no packages yet
-
+ There are no packages yet
</h1>
-
<p
class="gl-mt-3"
>
- Learn how to
+ Learn how to
<b-link-stub
class="gl-link"
href="helpUrl"
@@ -54,16 +48,11 @@ exports[`packages_list_app renders 1`] = `
>
publish and share your packages
</b-link-stub>
- with GitLab.
+ with GitLab.
</p>
-
<div
class="gl-display-flex gl-flex-wrap gl-justify-content-center"
- >
- <!---->
-
- <!---->
- </div>
+ />
</div>
</div>
</section>
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
index 250b33cbb14..edba81da1f5 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
@@ -2,28 +2,26 @@
exports[`packages_list_row renders 1`] = `
<div
- class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
+ class="gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-t-transparent gl-display-flex gl-flex-direction-column"
data-testid="package-row"
>
<div
- class="gl-display-flex gl-align-items-center gl-py-3"
+ class="gl-align-items-center gl-display-flex gl-py-3"
>
- <!---->
-
<div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
+ class="gl-align-items-stretch gl-display-flex gl-flex-grow-1 gl-justify-content-space-between gl-xs-flex-direction-column"
>
<div
- class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
+ class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-min-w-0 gl-xs-mb-3"
>
<div
- class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
+ class="gl-align-items-center gl-display-flex gl-font-weight-bold gl-min-h-6 gl-min-w-0 gl-text-body"
>
<div
- class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
+ class="gl-align-items-center gl-display-flex gl-min-w-0 gl-mr-3"
>
<gl-link-stub
- class="gl-text-body gl-min-w-0"
+ class="gl-min-w-0 gl-text-body"
data-testid="details-link"
href="foo"
>
@@ -32,17 +30,10 @@ exports[`packages_list_row renders 1`] = `
text="Test package"
/>
</gl-link-stub>
-
- <!---->
-
- <!---->
</div>
-
- <!---->
</div>
-
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
+ class="gl-align-items-center gl-display-flex gl-flex-grow-1 gl-min-h-6 gl-min-w-0 gl-text-gray-500"
>
<div
class="gl-display-flex"
@@ -50,31 +41,25 @@ exports[`packages_list_row renders 1`] = `
<span>
1.0.0
</span>
-
- <!---->
-
<div />
-
<package-path-stub
path="foo/bar/baz"
/>
</div>
</div>
</div>
-
<div
- class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
+ class="gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-justify-content-space-between gl-sm-align-items-flex-end gl-text-gray-500"
>
<div
- class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
+ class="gl-align-items-center gl-display-flex gl-min-h-6 gl-sm-font-weight-bold gl-sm-text-body"
>
<publish-method-stub
packageentity="[object Object]"
/>
</div>
-
<div
- class="gl-display-flex gl-align-items-center gl-min-h-6"
+ class="gl-align-items-center gl-display-flex gl-min-h-6"
>
<span>
<gl-sprintf-stub
@@ -84,9 +69,8 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
</div>
-
<div
- class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
+ class="gl-display-flex gl-justify-content-end gl-pr-1 gl-w-9"
>
<gl-button-stub
aria-label="Remove package"
@@ -100,7 +84,5 @@ exports[`packages_list_row renders 1`] = `
/>
</div>
</div>
-
- <!---->
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
index b3d0d88be4d..cfdaebd889d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/conan_installation_spec.js.snap
@@ -6,7 +6,6 @@ exports[`ConanInstallation renders all the messages 1`] = `
options="[object Object]"
packagetype="conan"
/>
-
<code-instruction-stub
copytext="Copy Conan Command"
instruction="conan install @gitlab-org/package-15 --remote=gitlab"
@@ -14,13 +13,11 @@ exports[`ConanInstallation renders all the messages 1`] = `
trackingaction="copy_conan_command"
trackinglabel="code_instruction"
/>
-
<h3
class="gl-font-lg"
>
Registry setup
</h3>
-
<code-instruction-stub
copytext="Copy Conan Setup Command"
instruction="conan remote add gitlab http://gdk.test:3000/api/v4/projects/1/packages/conan"
@@ -28,7 +25,7 @@ exports[`ConanInstallation renders all the messages 1`] = `
trackingaction="copy_conan_setup_command"
trackinglabel="code_instruction"
/>
- For more information on the Conan registry,
+ For more information on the Conan registry,
<gl-link-stub
href="/help/user/packages/conan_repository/index"
target="_blank"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
index f83df7b11f4..37401786d21 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/dependency_row_spec.js.snap
@@ -5,25 +5,21 @@ exports[`DependencyRow renders full dependency 1`] = `
class="gl-responsive-table-row"
>
<div
- class="table-section section-50"
+ class="section-50 table-section"
>
<strong
class="gl-text-body"
>
Ninject.Extensions.Factory
</strong>
-
<span
data-testid="target-framework"
>
-
(.NETCoreApp3.1)
-
</span>
</div>
-
<div
- class="table-section section-50 gl-display-flex gl-md-justify-content-end"
+ class="gl-display-flex gl-md-justify-content-end section-50 table-section"
data-testid="version-pattern"
>
<span
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
index f95564e3fad..8e757c136ec 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/file_sha_spec.js.snap
@@ -2,18 +2,13 @@
exports[`FileSha renders 1`] = `
<div
- class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all gl-py-2 gl-border-b-solid gl-border-gray-100 gl-border-b-1"
+ class="gl-align-items-center gl-border-b-1 gl-border-b-solid gl-border-gray-100 gl-display-flex gl-font-monospace gl-font-sm gl-py-2 gl-word-break-all"
>
- <!---->
-
<span>
<div
class="gl-px-4"
>
-
- bar:
- foo
-
+ bar: foo
<gl-button-stub
aria-label="Copy SHA"
aria-live="polite"
@@ -22,7 +17,7 @@ exports[`FileSha renders 1`] = `
data-clipboard-handle-tooltip="false"
data-clipboard-text="foo"
icon="copy-to-clipboard"
- id="clipboard-button-1"
+ id="reference-0"
size="small"
title="Copy SHA"
variant="default"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
index 9b429c39faa..23cdf4864de 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap
@@ -6,7 +6,6 @@ exports[`MavenInstallation groovy renders all the messages 1`] = `
options="[object Object],[object Object],[object Object]"
packagetype="maven"
/>
-
<code-instruction-stub
class="gl-mb-5"
copytext="Copy Gradle Groovy DSL install command"
@@ -15,7 +14,6 @@ exports[`MavenInstallation groovy renders all the messages 1`] = `
trackingaction="copy_gradle_install_command"
trackinglabel="code_instruction"
/>
-
<code-instruction-stub
copytext="Copy add Gradle Groovy DSL repository command"
instruction="maven {
@@ -35,7 +33,6 @@ exports[`MavenInstallation kotlin renders all the messages 1`] = `
options="[object Object],[object Object],[object Object]"
packagetype="maven"
/>
-
<code-instruction-stub
class="gl-mb-5"
copytext="Copy Gradle Kotlin DSL install command"
@@ -44,7 +41,6 @@ exports[`MavenInstallation kotlin renders all the messages 1`] = `
trackingaction="copy_kotlin_install_command"
trackinglabel="code_instruction"
/>
-
<code-instruction-stub
copytext="Copy add Gradle Kotlin DSL repository command"
instruction="maven(\\"http://gdk.test:3000/api/v4/projects/1/packages/maven\\")"
@@ -62,81 +58,72 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
options="[object Object],[object Object],[object Object]"
packagetype="maven"
/>
-
<p>
- Copy and paste this inside your
+ Copy and paste this inside your
<code>
pom.xml
</code>
-
<code>
dependencies
</code>
- block.
+ block.
</p>
-
<code-instruction-stub
copytext="Copy Maven XML"
- instruction="<dependency>
- <groupId>appGroup</groupId>
- <artifactId>appName</artifactId>
- <version>appVersion</version>
-</dependency>"
+ instruction=<dependency>
+ <groupid>
+ appGroup
+ </groupid>
+ <artifactid>
+ appName
+ </artifactid>
+ <version>
+ appVersion
+ </version>
+ </dependency>
label=""
multiline="true"
trackingaction="copy_maven_xml"
trackinglabel="code_instruction"
/>
-
<code-instruction-stub
- class="gl-w-20 gl-mt-5"
+ class="gl-mt-5 gl-w-20"
copytext="Copy Maven command"
instruction="mvn install"
label="Maven Command"
trackingaction="copy_maven_command"
trackinglabel="code_instruction"
/>
-
<h3
class="gl-font-lg"
>
Registry setup
</h3>
-
<p>
- If you haven't already done so, you will need to add the below to your
+ If you haven't already done so, you will need to add the below to your
<code>
pom.xml
</code>
- file.
+ file.
</p>
-
<code-instruction-stub
copytext="Copy Maven registry XML"
- instruction="<repositories>
- <repository>
- <id>gitlab-maven</id>
- <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
- </repository>
-</repositories>
-
-<distributionManagement>
- <repository>
- <id>gitlab-maven</id>
- <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
- </repository>
-
- <snapshotRepository>
- <id>gitlab-maven</id>
- <url>http://gdk.test:3000/api/v4/projects/1/packages/maven</url>
- </snapshotRepository>
-</distributionManagement>"
+ instruction=<repositories>
+ <repository>
+ <id>
+ gitlab-maven
+ </id>
+ <url>
+ http://gdk.test:3000/api/v4/projects/1/packages/maven
+ </url>
+ </repository>
+ </repositories>
label=""
multiline="true"
trackingaction="copy_maven_setup_xml"
trackinglabel="code_instruction"
/>
- For more information on the Maven registry,
+ For more information on the Maven registry,
<gl-link-stub
href="/help/user/packages/maven_repository/index"
target="_blank"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
index 4520ae9c328..7e36bfb5dc0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/npm_installation_spec.js.snap
@@ -6,7 +6,6 @@ exports[`NpmInstallation renders all the messages 1`] = `
options="[object Object],[object Object]"
packagetype="npm"
/>
-
<code-instruction-stub
copytext="Copy npm command"
instruction="npm i @gitlab-org/package-15"
@@ -14,13 +13,11 @@ exports[`NpmInstallation renders all the messages 1`] = `
trackingaction="copy_npm_install_command"
trackinglabel="code_instruction"
/>
-
<h3
class="gl-font-lg"
>
Registry setup
</h3>
-
<gl-form-radio-group-stub
checked="instance"
disabledfield="disabled"
@@ -29,7 +26,6 @@ exports[`NpmInstallation renders all the messages 1`] = `
textfield="text"
valuefield="value"
/>
-
<code-instruction-stub
copytext="Copy npm setup command"
instruction="echo @gitlab-org:registry=npmInstanceUrl/ >> .npmrc"
@@ -37,13 +33,13 @@ exports[`NpmInstallation renders all the messages 1`] = `
trackingaction="copy_npm_setup_command"
trackinglabel="code_instruction"
/>
- You may also need to setup authentication using an auth token.
+ You may also need to setup authentication using an auth token.
<gl-link-stub
href="/help/user/packages/npm_registry/index"
target="_blank"
>
See the documentation
</gl-link-stub>
- to find out more.
+ to find out more.
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
index 92930a6309a..554d4e08523 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/nuget_installation_spec.js.snap
@@ -6,7 +6,6 @@ exports[`NugetInstallation renders all the messages 1`] = `
options="[object Object]"
packagetype="nuget"
/>
-
<code-instruction-stub
copytext="Copy NuGet Command"
instruction="nuget install @gitlab-org/package-15 -Source \\"GitLab\\""
@@ -14,13 +13,11 @@ exports[`NugetInstallation renders all the messages 1`] = `
trackingaction="copy_nuget_install_command"
trackinglabel="code_instruction"
/>
-
<h3
class="gl-font-lg"
>
Registry setup
</h3>
-
<code-instruction-stub
copytext="Copy NuGet Setup Command"
instruction="nuget source Add -Name \\"GitLab\\" -Source \\"http://gdk.test:3000/api/v4/projects/1/packages/nuget/index.json\\" -UserName <your_username> -Password <your_token>"
@@ -28,7 +25,7 @@ exports[`NugetInstallation renders all the messages 1`] = `
trackingaction="copy_nuget_setup_command"
trackinglabel="code_instruction"
/>
- For more information on the NuGet registry,
+ For more information on the NuGet registry,
<gl-link-stub
href="/help/user/packages/nuget_repository/index"
target="_blank"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 99ee6ce01b2..05a5a718e52 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -3,45 +3,37 @@
exports[`PypiInstallation renders all the messages 1`] = `
<div>
<div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ class="gl-align-items-center gl-display-flex gl-justify-content-space-between"
>
<h3
class="gl-font-lg"
>
Installation
</h3>
-
<div>
<div
class="gl-new-dropdown"
>
<button
- aria-controls="base-dropdown-10"
+ aria-controls="reference-1"
aria-haspopup="listbox"
- aria-labelledby="dropdown-toggle-btn-8"
+ aria-labelledby="reference-0"
class="btn btn-default btn-md gl-button gl-new-dropdown-toggle"
data-testid="base-dropdown-toggle"
- id="dropdown-toggle-btn-8"
+ id="reference-0"
type="button"
>
- <!---->
-
- <!---->
-
<span
class="gl-button-text"
>
<span
class="gl-new-dropdown-button-text"
>
-
- Show PyPi commands
-
+ Show PyPi commands
</span>
-
<svg
aria-hidden="true"
- class="gl-button-icon gl-new-dropdown-chevron gl-icon s16"
+ class="gl-button-icon gl-icon gl-new-dropdown-chevron s16"
data-testid="chevron-down-icon"
role="img"
>
@@ -51,24 +43,18 @@ exports[`PypiInstallation renders all the messages 1`] = `
</svg>
</span>
</button>
-
<div
class="gl-new-dropdown-panel gl-w-31!"
data-testid="base-dropdown-menu"
- id="base-dropdown-10"
+ id="reference-1"
>
<div
class="gl-new-dropdown-inner"
>
-
- <!---->
-
- <!---->
-
<ul
- aria-labelledby="dropdown-toggle-btn-8"
- class="gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay gl-new-dropdown-contents"
- id="listbox-9"
+ aria-labelledby="reference-0"
+ class="gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay"
+ id="reference-2"
role="listbox"
tabindex="-1"
>
@@ -81,11 +67,9 @@ exports[`PypiInstallation renders all the messages 1`] = `
class="top-scrim top-scrim-light"
/>
</li>
-
<li
aria-hidden="true"
/>
-
<li
aria-selected="true"
class="gl-new-dropdown-item"
@@ -94,11 +78,11 @@ exports[`PypiInstallation renders all the messages 1`] = `
tabindex="-1"
>
<span
- class="gl-new-dropdown-item-content gl-bg-gray-50!"
+ class="gl-bg-gray-50! gl-new-dropdown-item-content"
>
<svg
aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon gl-mt-3 gl-align-self-start"
+ class="gl-align-self-start gl-icon gl-mt-3 gl-new-dropdown-item-check-icon s16"
data-testid="dropdown-item-checkbox"
role="img"
>
@@ -106,25 +90,16 @@ exports[`PypiInstallation renders all the messages 1`] = `
href="file-mock#mobile-issue-close"
/>
</svg>
-
<span
class="gl-new-dropdown-item-text-wrapper"
>
-
- Show PyPi commands
-
+ Show PyPi commands
</span>
</span>
</li>
-
- <!---->
-
- <!---->
-
<li
aria-hidden="true"
/>
-
<li
aria-hidden="true"
class="bottom-scrim-wrapper"
@@ -135,56 +110,43 @@ exports[`PypiInstallation renders all the messages 1`] = `
/>
</li>
</ul>
-
- <!---->
-
</div>
</div>
</div>
</div>
</div>
-
<fieldset
class="form-group gl-form-group"
- id="installation-pip-command-group"
+ id="reference-3"
>
<legend
- class="bv-no-focus-ring col-form-label pt-0 col-form-label"
- id="installation-pip-command-group__BV_label_"
+ class="bv-no-focus-ring col-form-label pt-0"
+ id="reference-4"
tabindex="-1"
- >
-
-
-
- <!---->
-
- <!---->
- </legend>
+ />
<div>
<div
data-testid="pip-command"
- id="installation-pip-command"
+ id="reference-5"
>
<label
- for="instruction-input_11"
+ for="reference-6"
>
Pip Command
</label>
-
<div
class="gl-mb-3"
>
<div
- class="input-group gl-mb-3"
+ class="gl-mb-3 input-group"
>
<input
class="form-control gl-font-monospace"
data-testid="instruction-input"
- id="instruction-input_11"
- readonly="readonly"
+ id="reference-6"
+ readonly=""
type="text"
/>
-
<span
class="input-group-append"
data-testid="instruction-button"
@@ -192,15 +154,13 @@ exports[`PypiInstallation renders all the messages 1`] = `
<button
aria-label="Copy Pip command"
aria-live="polite"
- class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
+ class="btn btn-default btn-default-secondary btn-icon btn-md gl-button input-group-text"
data-clipboard-handle-tooltip="false"
data-clipboard-text="pip install @gitlab-org/package-15 --index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
- id="clipboard-button-12"
+ id="reference-7"
title="Copy Pip command"
type="button"
>
- <!---->
-
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
@@ -211,21 +171,17 @@ exports[`PypiInstallation renders all the messages 1`] = `
href="file-mock#copy-to-clipboard"
/>
</svg>
-
- <!---->
</button>
</span>
</div>
</div>
</div>
- <!---->
- <!---->
<small
class="form-text text-muted"
- id="installation-pip-command-group__BV_description_"
+ id="reference-8"
tabindex="-1"
>
- You will need a
+ You will need a
<a
class="gl-link"
data-testid="access-token-link"
@@ -237,39 +193,31 @@ exports[`PypiInstallation renders all the messages 1`] = `
</small>
</div>
</fieldset>
-
<h3
class="gl-font-lg"
>
Registry setup
</h3>
-
<p>
- If you haven't already done so, you will need to add the below to your
+ If you haven't already done so, you will need to add the below to your
<code>
.pypirc
</code>
- file.
+ file.
</p>
-
<div
data-testid="pypi-setup-content"
>
- <!---->
-
<div>
<pre
class="gl-font-monospace"
data-testid="multiline-instruction"
>
- [gitlab]
-repository = http://gdk.test:3000/api/v4/projects/1/packages/pypi
-username = __token__
-password = &lt;your personal access token&gt;
+ [gitlab]repository = http://gdk.test:3000/api/v4/projects/1/packages/pypiusername = __token__password = &lt;your personal access token&gt;
</pre>
</div>
</div>
- For more information on the PyPi registry,
+ For more information on the PyPi registry,
<a
class="gl-link"
data-testid="pypi-docs-link"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index e0e6c101029..40fcd290b33 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -2,36 +2,35 @@
exports[`packages_list_row renders 1`] = `
<div
- class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
+ class="gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-t-transparent gl-display-flex gl-flex-direction-column"
data-testid="package-row"
>
<div
- class="gl-display-flex gl-align-items-center gl-py-3"
+ class="gl-align-items-center gl-display-flex gl-py-3"
>
<div
- class="gl-w-7 gl-display-flex gl-justify-content-start gl-pl-2"
+ class="gl-display-flex gl-justify-content-start gl-pl-2 gl-w-7"
>
<gl-form-checkbox-stub
class="gl-m-0"
- id="2"
+ id="reference-0"
/>
</div>
-
<div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
+ class="gl-align-items-stretch gl-display-flex gl-flex-grow-1 gl-justify-content-space-between gl-xs-flex-direction-column"
>
<div
- class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
+ class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-min-w-0 gl-xs-mb-3"
>
<div
- class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
+ class="gl-align-items-center gl-display-flex gl-font-weight-bold gl-min-h-6 gl-min-w-0 gl-text-body"
>
<div
- class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
+ class="gl-align-items-center gl-display-flex gl-min-w-0 gl-mr-3"
>
<router-link-stub
ariacurrentvalue="page"
- class="gl-text-body gl-min-w-0"
+ class="gl-min-w-0 gl-text-body"
data-testid="details-link"
event="click"
tag="a"
@@ -42,18 +41,13 @@ exports[`packages_list_row renders 1`] = `
text="@gitlab-org/package-15"
/>
</router-link-stub>
-
- <!---->
</div>
-
- <!---->
</div>
-
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
+ class="gl-align-items-center gl-display-flex gl-flex-grow-1 gl-min-h-6 gl-min-w-0 gl-text-gray-500"
>
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
data-testid="left-secondary-infos"
>
<gl-truncate-stub
@@ -62,7 +56,6 @@ exports[`packages_list_row renders 1`] = `
text="1.0.0"
withtooltip="true"
/>
-
<span
class="gl-ml-2"
data-testid="package-type"
@@ -72,25 +65,22 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
</div>
-
<div
- class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
+ class="gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-justify-content-space-between gl-sm-align-items-flex-end gl-text-gray-500"
>
<div
- class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
+ class="gl-align-items-center gl-display-flex gl-min-h-6 gl-sm-font-weight-bold gl-sm-text-body"
>
<publish-method-stub />
</div>
-
<div
- class="gl-display-flex gl-align-items-center gl-min-h-6"
+ class="gl-align-items-center gl-display-flex gl-min-h-6"
>
<span
data-testid="right-secondary"
>
- Published
+ Published
<time
- class=""
datetime="2020-05-17T14:23:32Z"
title="May 17, 2020 2:23pm UTC"
>
@@ -100,9 +90,8 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
</div>
-
<div
- class="gl-w-9 gl-display-flex gl-justify-content-end gl-pr-1"
+ class="gl-display-flex gl-justify-content-end gl-pr-1 gl-w-9"
>
<gl-disclosure-dropdown-stub
autoclose="true"
@@ -125,15 +114,11 @@ exports[`packages_list_row renders 1`] = `
<span
class="gl-text-red-500"
>
-
Delete package
-
</span>
</gl-disclosure-dropdown-item-stub>
</gl-disclosure-dropdown-stub>
</div>
</div>
-
- <!---->
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
index 4407c4a2003..f202635d717 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/publish_method_spec.js.snap
@@ -2,27 +2,24 @@
exports[`publish_method renders 1`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-icon-stub
class="gl-mr-2"
name="git-merge"
size="16"
/>
-
<span
class="gl-mr-2"
data-testid="pipeline-ref"
>
master
</span>
-
<gl-icon-stub
class="gl-mr-2"
name="commit"
size="16"
/>
-
<gl-link-stub
class="gl-mr-2"
data-testid="pipeline-sha"
@@ -30,7 +27,6 @@ exports[`publish_method renders 1`] = `
>
b83d6e39
</gl-link-stub>
-
<clipboard-button-stub
category="tertiary"
size="small"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index fad8863e3d9..acf8b718400 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -37,7 +37,6 @@ describe('packages_list', () => {
const defaultProps = {
list: [firstPackage, secondPackage],
isLoading: false,
- pageInfo: {},
groupSettings: defaultPackageGroupSettings,
};
@@ -113,7 +112,6 @@ describe('packages_list', () => {
expect(findRegistryList().props()).toMatchObject({
title: '2 packages',
items: defaultProps.list,
- pagination: defaultProps.pageInfo,
hiddenDelete: false,
isLoading: false,
});
@@ -314,22 +312,4 @@ describe('packages_list', () => {
expect(emptySlot.exists()).toBe(true);
});
});
-
- describe('pagination', () => {
- beforeEach(() => {
- mountComponent({ props: { pageInfo: { hasPreviousPage: true } } });
- });
-
- it('emits prev-page events when the prev event is fired', () => {
- findRegistryList().vm.$emit('prev-page');
-
- expect(wrapper.emitted('prev-page')).toHaveLength(1);
- });
-
- it('emits next-page events when the next event is fired', () => {
- findRegistryList().vm.$emit('next-page');
-
- expect(wrapper.emitted('next-page')).toHaveLength(1);
- });
- });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index 82fa5b76367..f4e36f51c27 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -3,19 +3,12 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sortableFields } from '~/packages_and_registries/package_registry/utils';
import component from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
-import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import { LIST_KEY_CREATED_AT } from '~/packages_and_registries/package_registry/constants';
-import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import { TOKEN_TYPE_TYPE } from '~/vue_shared/components/filtered_search_bar/constants';
-jest.mock('~/packages_and_registries/shared/utils');
-
-useMockLocationHelper();
-
describe('Package Search', () => {
let wrapper;
@@ -24,8 +17,7 @@ describe('Package Search', () => {
sorting: { sort: 'desc' },
};
- const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
- const findUrlSync = () => wrapper.findComponent(UrlSync);
+ const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const mountComponent = (isGroupPage = false) => {
@@ -36,34 +28,23 @@ describe('Package Search', () => {
};
},
stubs: {
- UrlSync,
LocalStorageSync,
},
});
};
- beforeEach(() => {
- extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
- });
-
it('has a registry search component', async () => {
mountComponent();
await nextTick();
- expect(findRegistrySearch().exists()).toBe(true);
+ expect(findPersistedSearch().exists()).toBe(true);
});
it('registry search is mounted after mount', () => {
mountComponent();
- expect(findRegistrySearch().exists()).toBe(false);
- });
-
- it('has a UrlSync component', () => {
- mountComponent();
-
- expect(findUrlSync().exists()).toBe(true);
+ expect(findPersistedSearch().exists()).toBe(false);
});
it('has a LocalStorageSync component', () => {
@@ -87,7 +68,7 @@ describe('Package Search', () => {
await nextTick();
- expect(findRegistrySearch().props()).toMatchObject({
+ expect(findPersistedSearch().props()).toMatchObject({
tokens: expect.arrayContaining([
expect.objectContaining({
token: PackageTypeToken,
@@ -99,85 +80,63 @@ describe('Package Search', () => {
});
});
- it('on sorting:changed emits update event and update internal sort', async () => {
- const payload = { sort: 'foo' };
+ it('on update event re-emits update event and updates internal sort', async () => {
+ const payload = {
+ sort: 'CREATED_FOO',
+ filters: defaultQueryParamsMock.filters,
+ sorting: { sort: 'foo', orderBy: 'created_at' },
+ };
mountComponent();
await nextTick();
- findRegistrySearch().vm.$emit('sorting:changed', payload);
+ findPersistedSearch().vm.$emit('update', payload);
await nextTick();
- expect(findRegistrySearch().props('sorting')).toEqual({ sort: 'foo', orderBy: 'created_at' });
+ expect(findLocalStorageSync().props('value')).toEqual({ sort: 'foo', orderBy: 'created_at' });
- // there is always a first call on mounted that emits up default values
- expect(wrapper.emitted('update')[1]).toEqual([
+ expect(wrapper.emitted('update')[0]).toEqual([
{
filters: {
packageName: '',
packageType: undefined,
},
- sort: 'CREATED_FOO',
+ sort: payload.sort,
+ sorting: payload.sorting,
},
]);
});
- it('on filter:changed updates the filters', async () => {
- const payload = ['foo'];
+ it('on update event, re-emits update event with formatted filters', async () => {
+ const payload = {
+ sort: 'CREATED_FOO',
+ filters: [
+ { type: 'type', value: { data: 'Generic', operator: '=' }, id: 'token-3' },
+ { id: 'token-4', type: 'filtered-search-term', value: { data: 'gl' } },
+ { id: 'token-5', type: 'filtered-search-term', value: { data: '' } },
+ ],
+ sorting: { sort: 'foo', orderBy: 'created_at' },
+ };
mountComponent();
await nextTick();
- findRegistrySearch().vm.$emit('filter:changed', payload);
+ findPersistedSearch().vm.$emit('update', payload);
await nextTick();
- expect(findRegistrySearch().props('filters')).toEqual(['foo']);
- });
-
- it('on filter:submit emits update event', async () => {
- mountComponent();
-
- await nextTick();
-
- findRegistrySearch().vm.$emit('filter:submit');
-
- expect(wrapper.emitted('update')[1]).toEqual([
+ expect(wrapper.emitted('update')[0]).toEqual([
{
filters: {
- packageName: '',
- packageType: undefined,
+ packageName: 'gl',
+ packageType: 'GENERIC',
},
- sort: 'CREATED_DESC',
+ sort: payload.sort,
+ sorting: payload.sorting,
},
]);
});
-
- it('on query:changed calls updateQuery from UrlSync', async () => {
- jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
-
- mountComponent();
-
- await nextTick();
-
- findRegistrySearch().vm.$emit('query:changed');
-
- expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
- });
-
- it('sets the component sorting and filtering based on the querystring', async () => {
- mountComponent();
-
- await nextTick();
-
- expect(getQueryParams).toHaveBeenCalled();
-
- expect(findRegistrySearch().props()).toMatchObject({
- filters: defaultQueryParamsMock.filters,
- sorting: defaultQueryParamsMock.sorting,
- });
- });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index 0d262036ee7..0ce2b86b9a4 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -17,7 +17,7 @@ import {
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
} from '~/packages_and_registries/package_registry/constants';
-
+import PersistedPagination from '~/packages_and_registries/shared/components/persisted_pagination.vue';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
import { packagesListQuery, packageData, pagination } from '../mock_data';
@@ -53,6 +53,7 @@ describe('PackagesListApp', () => {
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
const findSettingsLink = () => wrapper.findComponent(GlButton);
+ const findPagination = () => wrapper.findComponent(PersistedPagination);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@@ -99,6 +100,15 @@ describe('PackagesListApp', () => {
expect(resolver).not.toHaveBeenCalled();
});
+ it('has persisted pagination', async () => {
+ const resolver = jest.fn().mockResolvedValue(packagesListQuery());
+
+ mountComponent({ resolver });
+ await waitForFirstRequest();
+
+ expect(findPagination().props('pagination')).toEqual(pagination());
+ });
+
it('has a package title', async () => {
mountComponent();
@@ -194,7 +204,6 @@ describe('PackagesListApp', () => {
expect(findListComponent().props()).toMatchObject({
list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]),
isLoading: false,
- pageInfo: expect.objectContaining({ endCursor: pagination().endCursor }),
groupSettings: expect.objectContaining({
mavenPackageRequestsForwarding: true,
npmPackageRequestsForwarding: true,
@@ -203,9 +212,9 @@ describe('PackagesListApp', () => {
});
});
- it('when list emits next-page fetches the next set of records', async () => {
+ it('when pagination emits next event fetches the next set of records', async () => {
await waitForFirstRequest();
- findListComponent().vm.$emit('next-page');
+ findPagination().vm.$emit('next');
await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
@@ -213,9 +222,9 @@ describe('PackagesListApp', () => {
);
});
- it('when list emits prev-page fetches the prev set of records', async () => {
+ it('when pagination emits prev event fetches the prev set of records', async () => {
await waitForFirstRequest();
- findListComponent().vm.$emit('prev-page');
+ findPagination().vm.$emit('prev');
await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
diff --git a/spec/frontend/packages_and_registries/package_registry/utils_spec.js b/spec/frontend/packages_and_registries/package_registry/utils_spec.js
index 019f94aaec2..ecb5a8a77f1 100644
--- a/spec/frontend/packages_and_registries/package_registry/utils_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/utils_spec.js
@@ -1,4 +1,9 @@
-import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
+import {
+ getPackageTypeLabel,
+ getNextPageParams,
+ getPreviousPageParams,
+ getPageParams,
+} from '~/packages_and_registries/package_registry/utils';
describe('Packages shared utils', () => {
describe('getPackageTypeLabel', () => {
@@ -21,3 +26,48 @@ describe('Packages shared utils', () => {
});
});
});
+
+describe('getNextPageParams', () => {
+ it('should return the next page params with the provided cursor', () => {
+ const cursor = 'abc123';
+ expect(getNextPageParams(cursor)).toEqual({
+ after: cursor,
+ first: 20,
+ });
+ });
+});
+
+describe('getPreviousPageParams', () => {
+ it('should return the previous page params with the provided cursor', () => {
+ const cursor = 'abc123';
+ expect(getPreviousPageParams(cursor)).toEqual({
+ first: null,
+ before: cursor,
+ last: 20,
+ });
+ });
+});
+
+describe('getPageParams', () => {
+ it('should return the previous page params if before cursor is available', () => {
+ const pageInfo = { before: 'abc123' };
+ expect(getPageParams(pageInfo)).toEqual({
+ first: null,
+ before: pageInfo.before,
+ last: 20,
+ });
+ });
+
+ it('should return the next page params if after cursor is available', () => {
+ const pageInfo = { after: 'abc123' };
+ expect(getPageParams(pageInfo)).toEqual({
+ after: pageInfo.after,
+ first: 20,
+ });
+ });
+
+ it('should return an empty object if both before and after cursors are not available', () => {
+ const pageInfo = {};
+ expect(getPageParams(pageInfo)).toEqual({});
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
index 5d08574234c..d3298984f9d 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/container_expiration_policy_form_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Container Expiration Policy Settings Form Cadence matches snapshot 1`] = `
<expiration-dropdown-stub
- class="gl-mr-7 gl-mb-0!"
+ class="gl-mb-0! gl-mr-7"
data-testid="cadence-dropdown"
description=""
dropdownclass=""
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap
index 5f243799bae..084e4a2b2f3 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap
@@ -2,27 +2,24 @@
exports[`publish_method renders 1`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-icon-stub
class="gl-mr-2"
name="git-merge"
size="16"
/>
-
<span
class="gl-mr-2"
data-testid="pipeline-ref"
>
branch-name
</span>
-
<gl-icon-stub
class="gl-mr-2"
name="commit"
size="16"
/>
-
<gl-link-stub
class="gl-mr-2"
data-testid="pipeline-sha"
@@ -30,7 +27,6 @@ exports[`publish_method renders 1`] = `
>
sha-baz
</gl-link-stub>
-
<clipboard-button-stub
category="tertiary"
size="small"
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
index e9ee6ebdb5c..e67bded6a7e 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -12,34 +12,22 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class="gl-breadcrumb-item"
>
<a
- class=""
target="_self"
>
- <!---->
- <span>
-
- </span>
+ <span />
</a>
</li>
-
- <!---->
<li
class="gl-breadcrumb-item"
>
<a
aria-current="page"
- class=""
href="#"
target="_self"
>
- <!---->
- <span>
-
- </span>
+ <span />
</a>
</li>
-
- <!---->
</ol>
</nav>
`;
@@ -57,17 +45,11 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
>
<a
aria-current="page"
- class=""
target="_self"
>
- <!---->
- <span>
-
- </span>
+ <span />
</a>
</li>
-
- <!---->
</ol>
</nav>
`;
diff --git a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
index 328f83394f9..9041cb757ab 100644
--- a/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
@@ -23,7 +23,7 @@ Vue.use(Vuex);
describe('cli_commands', () => {
let wrapper;
- const findDropdownButton = () => wrapper.findComponent(GlDropdown);
+ const findDropdownButton = () => wrapper.findComponent(GlDisclosureDropdown);
const findCodeInstruction = () => wrapper.findAllComponents(CodeInstruction);
const mountComponent = () => {
diff --git a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
index 296caf091d5..615fba2e282 100644
--- a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js
@@ -86,6 +86,7 @@ describe('Persisted Search', () => {
after: '123',
before: null,
},
+ sorting: defaultQueryParamsMock.sorting,
},
]);
});
@@ -109,6 +110,7 @@ describe('Persisted Search', () => {
{
filters: [],
sort: 'TEST_DESC',
+ sorting: defaultQueryParamsMock.sorting,
pageInfo: {
before: '456',
after: null,
@@ -136,6 +138,7 @@ describe('Persisted Search', () => {
filters: ['foo'],
sort: 'TEST_DESC',
pageInfo: {},
+ sorting: payload,
},
]);
});
@@ -169,6 +172,7 @@ describe('Persisted Search', () => {
after: '123',
before: null,
},
+ sorting: defaultQueryParamsMock.sorting,
},
]);
});
diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
deleted file mode 100644
index 6cf30e84288..00000000000
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import $ from 'jquery';
-import htmlAbuseReportsList from 'test_fixtures/abuse_reports/abuse_reports_list.html';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports';
-
-describe('Abuse Reports', () => {
- const MAX_MESSAGE_LENGTH = 500;
-
- let $messages;
-
- const assertMaxLength = ($message) => {
- expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
- };
- const findMessage = (searchText) =>
- $messages.filter((index, element) => element.innerText.indexOf(searchText) > -1).first();
-
- beforeEach(() => {
- setHTMLFixture(htmlAbuseReportsList);
- new AbuseReports(); // eslint-disable-line no-new
- $messages = $('.abuse-reports .message');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('should truncate long messages', () => {
- const $longMessage = findMessage('LONG MESSAGE');
-
- expect($longMessage.data('originalMessage')).toEqual(expect.anything());
- assertMaxLength($longMessage);
- });
-
- it('should not truncate short messages', () => {
- const $shortMessage = findMessage('SHORT MESSAGE');
-
- expect($shortMessage.data('originalMessage')).not.toEqual(expect.anything());
- });
-
- it('should allow clicking a truncated message to expand and collapse the full message', () => {
- const $longMessage = findMessage('LONG MESSAGE');
- $longMessage.click();
-
- expect($longMessage.data('originalMessage').length).toEqual($longMessage.text().length);
- $longMessage.click();
- assertMaxLength($longMessage);
- });
-});
diff --git a/spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js b/spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js
index d94de48f238..2884e4ed521 100644
--- a/spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js
@@ -2,12 +2,9 @@ import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { TEST_HOST } from 'helpers/test_constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue';
-import CancelJobsModal from '~/pages/admin/jobs/components/cancel_jobs_modal.vue';
-import {
- CANCEL_JOBS_MODAL_ID,
- CANCEL_BUTTON_TOOLTIP,
-} from '~/pages/admin/jobs/components/constants';
+import CancelJobs from '~/ci/admin/jobs_table/components/cancel_jobs.vue';
+import CancelJobsModal from '~/ci/admin/jobs_table/components/cancel_jobs_modal.vue';
+import { CANCEL_JOBS_MODAL_ID, CANCEL_BUTTON_TOOLTIP } from '~/ci/admin/jobs_table/constants';
describe('CancelJobs component', () => {
let wrapper;
diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
index 71ebf64f43c..d14b78d2f4d 100644
--- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
@@ -4,17 +4,17 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue';
-import getAllJobsQuery from '~/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql';
-import getAllJobsCount from '~/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql';
-import getCancelableJobsQuery from '~/pages/admin/jobs/components/table/graphql/queries/get_cancelable_jobs_count.query.graphql';
-import AdminJobsTableApp from '~/pages/admin/jobs/components/table/admin_jobs_table_app.vue';
-import CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
+import JobsTableTabs from '~/ci/jobs_page/components/jobs_table_tabs.vue';
+import JobsSkeletonLoader from '~/ci/admin/jobs_table/components/jobs_skeleton_loader.vue';
+import getAllJobsQuery from '~/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql';
+import getAllJobsCount from '~/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql';
+import getCancelableJobsQuery from '~/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql';
+import AdminJobsTableApp from '~/ci/admin/jobs_table/admin_jobs_table_app.vue';
+import CancelJobs from '~/ci/admin/jobs_table/components/cancel_jobs.vue';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
import { createAlert } from '~/alert';
import { TEST_HOST } from 'spec/test_constants';
-import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
import * as urlUtils from '~/lib/utils/url_utility';
import {
JOBS_FETCH_ERROR_MSG,
@@ -22,7 +22,8 @@ import {
LOADING_ARIA_LABEL,
RAW_TEXT_WARNING_ADMIN,
JOBS_COUNT_ERROR_MESSAGE,
-} from '~/pages/admin/jobs/components/constants';
+} from '~/ci/admin/jobs_table/constants';
+import { TOKEN_TYPE_JOBS_RUNNER_TYPE } from '~/vue_shared/components/filtered_search_bar/constants';
import {
mockAllJobsResponsePaginated,
mockCancelableJobsCountResponse,
@@ -30,7 +31,7 @@ import {
statuses,
mockFailedSearchToken,
mockAllJobsCountResponse,
-} from '../../../../../jobs/mock_data';
+} from 'jest/ci/jobs_mock_data';
Vue.use(VueApollo);
@@ -54,6 +55,11 @@ describe('Job table app', () => {
const findCancelJobsButton = () => wrapper.findComponent(CancelJobs);
const findFilteredSearch = () => wrapper.findComponent(JobsFilteredSearch);
+ const mockSearchTokenRunnerType = {
+ type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ value: { data: 'INSTANCE_TYPE', operator: '=' },
+ };
+
const triggerInfiniteScroll = () =>
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
@@ -73,6 +79,7 @@ describe('Job table app', () => {
countHandler = countSuccessHandler,
mountFn = shallowMount,
data = {},
+ provideOptions = {},
} = {}) => {
wrapper = mountFn(AdminJobsTableApp, {
data() {
@@ -82,6 +89,8 @@ describe('Job table app', () => {
},
provide: {
jobStatuses: statuses,
+ glFeatures: { adminJobsFilterRunnerType: true },
+ ...provideOptions,
},
apolloProvider: createMockApolloProvider(handler, cancelableHandler, countHandler),
});
@@ -304,24 +313,37 @@ describe('Job table app', () => {
},
);
- it('refetches jobs query when filtering', async () => {
- createComponent();
+ describe.each`
+ searchTokens | expectedQueryParams
+ ${[]} | ${{ runnerTypes: null, statuses: null }}
+ ${[mockFailedSearchToken]} | ${{ runnerTypes: null, statuses: 'FAILED' }}
+ ${[mockFailedSearchToken, mockSearchTokenRunnerType]} | ${{ runnerTypes: 'INSTANCE_TYPE', statuses: 'FAILED' }}
+ `('when filtering jobs by searchTokens', ({ searchTokens, expectedQueryParams }) => {
+ it(`refetches jobs query including filters ${JSON.stringify(
+ expectedQueryParams,
+ )}`, async () => {
+ createComponent();
- expect(successHandler).toHaveBeenCalledTimes(1);
+ expect(successHandler).toHaveBeenCalledTimes(1);
- await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', searchTokens);
- expect(successHandler).toHaveBeenCalledTimes(2);
- });
+ expect(successHandler).toHaveBeenCalledTimes(2);
+ expect(successHandler).toHaveBeenNthCalledWith(2, { first: 50, ...expectedQueryParams });
+ });
- it('refetches jobs count query when filtering', async () => {
- createComponent();
+ it(`refetches jobs count query including filters ${JSON.stringify(
+ expectedQueryParams,
+ )}`, async () => {
+ createComponent();
- expect(countSuccessHandler).toHaveBeenCalledTimes(1);
+ expect(countSuccessHandler).toHaveBeenCalledTimes(1);
- await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', searchTokens);
- expect(countSuccessHandler).toHaveBeenCalledTimes(2);
+ expect(countSuccessHandler).toHaveBeenCalledTimes(2);
+ expect(countSuccessHandler).toHaveBeenNthCalledWith(2, expectedQueryParams);
+ });
});
it('shows raw text warning when user inputs raw text', async () => {
@@ -364,6 +386,7 @@ describe('Job table app', () => {
expect(successHandler).toHaveBeenCalledWith({
first: 50,
statuses: 'FAILED',
+ runnerTypes: null,
});
expect(urlUtils.updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?statuses=FAILED`,
@@ -378,6 +401,44 @@ describe('Job table app', () => {
expect(successHandler).toHaveBeenCalledWith({
first: 50,
statuses: null,
+ runnerTypes: null,
+ });
+ });
+
+ describe('when feature flag `adminJobsFilterRunnerType` is disabled', () => {
+ const provideOptions = { glFeatures: { adminJobsFilterRunnerType: false } };
+
+ describe.each`
+ searchTokens | expectedQueryParams
+ ${[]} | ${{ statuses: null }}
+ ${[mockFailedSearchToken]} | ${{ statuses: 'FAILED' }}
+ ${[mockFailedSearchToken, mockSearchTokenRunnerType]} | ${{ statuses: 'FAILED' }}
+ `('when filtering jobs by searchTokens', ({ searchTokens, expectedQueryParams }) => {
+ it(`refetches jobs query including filters ${JSON.stringify(
+ expectedQueryParams,
+ )}`, async () => {
+ createComponent({ provideOptions });
+
+ expect(successHandler).toHaveBeenCalledTimes(1);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', searchTokens);
+
+ expect(successHandler).toHaveBeenCalledTimes(2);
+ expect(successHandler).toHaveBeenNthCalledWith(2, { first: 50, ...expectedQueryParams });
+ });
+
+ it(`refetches jobs count query including filters ${JSON.stringify(
+ expectedQueryParams,
+ )}`, async () => {
+ createComponent({ provideOptions });
+
+ expect(countSuccessHandler).toHaveBeenCalledTimes(1);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', searchTokens);
+
+ expect(countSuccessHandler).toHaveBeenCalledTimes(2);
+ expect(countSuccessHandler).toHaveBeenNthCalledWith(2, expectedQueryParams);
+ });
});
});
});
diff --git a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
index 40d5dff9d06..50bc5bc590b 100644
--- a/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
+++ b/spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js
@@ -12,11 +12,7 @@ const BitbucketStatusTableStub = {
describe('BitbucketServerStatusTable', () => {
let wrapper;
- const findReconfigureButton = () =>
- wrapper
- .findAllComponents(GlButton)
- .filter((w) => w.props().variant === 'info')
- .at(0);
+ const findReconfigureButton = () => wrapper.findComponent(GlButton);
function createComponent(bitbucketStatusTableStub = true) {
wrapper = shallowMount(BitbucketServerStatusTable, {
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
deleted file mode 100644
index e20c2fa47a7..00000000000
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import Cookies from '~/lib/utils/cookies';
-import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
-
-const cookieKey = 'pipeline_schedules_callout_dismissed';
-const docsUrl = 'help/ci/scheduled_pipelines';
-const illustrationUrl = 'pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
-
-describe('Pipeline Schedule Callout', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = shallowMount(PipelineSchedulesCallout, {
- provide: {
- docsUrl,
- illustrationUrl,
- },
- });
- };
-
- const findInnerContentOfCallout = () => wrapper.find('[data-testid="innerContent"]');
- const findDismissCalloutBtn = () => wrapper.findComponent(GlButton);
-
- describe(`when ${cookieKey} cookie is set`, () => {
- beforeEach(async () => {
- Cookies.set(cookieKey, true);
- createComponent();
-
- await nextTick();
- });
-
- it('does not render the callout', () => {
- expect(findInnerContentOfCallout().exists()).toBe(false);
- });
- });
-
- describe('when cookie is not set', () => {
- beforeEach(() => {
- Cookies.remove(cookieKey);
- createComponent();
- });
-
- it('renders the callout container', () => {
- expect(findInnerContentOfCallout().exists()).toBe(true);
- });
-
- it('renders the callout title', () => {
- expect(wrapper.find('h4').text()).toBe('Scheduling Pipelines');
- });
-
- it('renders the callout text', () => {
- expect(wrapper.find('p').text()).toContain('runs pipelines in the future');
- });
-
- it('renders the documentation url', () => {
- expect(wrapper.find('a').attributes('href')).toBe(docsUrl);
- });
-
- describe('methods', () => {
- it('#dismissCallout sets calloutDismissed to true', async () => {
- expect(wrapper.vm.calloutDismissed).toBe(false);
-
- findDismissCalloutBtn().vm.$emit('click');
-
- await nextTick();
-
- expect(findInnerContentOfCallout().exists()).toBe(false);
- });
-
- it('sets cookie on dismiss', () => {
- const setCookiesSpy = jest.spyOn(Cookies, 'set');
-
- findDismissCalloutBtn().vm.$emit('click');
-
- expect(setCookiesSpy).toHaveBeenCalledWith('pipeline_schedules_callout_dismissed', true, {
- expires: 365,
- secure: false,
- });
- });
- });
-
- it('is hidden when close button is clicked', async () => {
- findDismissCalloutBtn().vm.$emit('click');
-
- await nextTick();
-
- expect(findInnerContentOfCallout().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap b/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap
deleted file mode 100644
index cb5f6ff5307..00000000000
--- a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap
+++ /dev/null
@@ -1,230 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`The DAG graph in the basic case renders the graph svg 1`] = `
-"<svg viewBox=\\"0,0,1000,540\\" width=\\"1000\\" height=\\"540\\">
- <g fill=\\"none\\" stroke-opacity=\\"0.8\\">
- <g id=\\"dag-link43\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad53\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"116\\" x2=\\"361.3333333333333\\">
- <stop offset=\\"0%\\" stop-color=\\"#e17223\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#83ab4a\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip63\\">
- <path d=\\"
- M100, 129
- V158
- H377.3333333333333
- V100
- H100
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M108,129L190,129L190,129L369.3333333333333,129\\" stroke=\\"url(#dag-grad53)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip63)\\"></path>
- </g>
- <g id=\\"dag-link44\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad54\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"377.3333333333333\\" x2=\\"622.6666666666666\\">
- <stop offset=\\"0%\\" stop-color=\\"#83ab4a\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#6f3500\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip64\\">
- <path d=\\"
- M361.3333333333333, 129.0000000000002
- V158.0000000000002
- H638.6666666666666
- V100
- H361.3333333333333
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002\\" stroke=\\"url(#dag-grad54)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip64)\\"></path>
- </g>
- <g id=\\"dag-link45\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad55\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"116\\" x2=\\"622.6666666666666\\">
- <stop offset=\\"0%\\" stop-color=\\"#5772ff\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#6f3500\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip65\\">
- <path d=\\"
- M100, 187.0000000000002
- V241.00000000000003
- H638.6666666666666
- V158.0000000000002
- H100
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002\\" stroke=\\"url(#dag-grad55)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip65)\\"></path>
- </g>
- <g id=\\"dag-link46\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad56\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"116\\" x2=\\"361.3333333333333\\">
- <stop offset=\\"0%\\" stop-color=\\"#b24800\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#006887\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip66\\">
- <path d=\\"
- M100, 269.9999999999998
- V324
- H377.3333333333333
- V240.99999999999977
- H100
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998\\" stroke=\\"url(#dag-grad56)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip66)\\"></path>
- </g>
- <g id=\\"dag-link47\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad57\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"116\\" x2=\\"361.3333333333333\\">
- <stop offset=\\"0%\\" stop-color=\\"#25d2d2\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#487900\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip67\\">
- <path d=\\"
- M100, 352.99999999999994
- V407.00000000000006
- H377.3333333333333
- V323.99999999999994
- H100
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994\\" stroke=\\"url(#dag-grad57)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip67)\\"></path>
- </g>
- <g id=\\"dag-link48\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad58\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"377.3333333333333\\" x2=\\"622.6666666666666\\">
- <stop offset=\\"0%\\" stop-color=\\"#006887\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#d84280\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip68\\">
- <path d=\\"
- M361.3333333333333, 270.0000000000001
- V299.0000000000001
- H638.6666666666666
- V240.99999999999977
- H361.3333333333333
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001\\" stroke=\\"url(#dag-grad58)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip68)\\"></path>
- </g>
- <g id=\\"dag-link49\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad59\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"377.3333333333333\\" x2=\\"622.6666666666666\\">
- <stop offset=\\"0%\\" stop-color=\\"#487900\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#d84280\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip69\\">
- <path d=\\"
- M361.3333333333333, 328.0000000000001
- V381.99999999999994
- H638.6666666666666
- V299.0000000000001
- H361.3333333333333
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001\\" stroke=\\"url(#dag-grad59)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip69)\\"></path>
- </g>
- <g id=\\"dag-link50\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad60\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"377.3333333333333\\" x2=\\"622.6666666666666\\">
- <stop offset=\\"0%\\" stop-color=\\"#487900\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#3547de\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip70\\">
- <path d=\\"
- M361.3333333333333, 411
- V440
- H638.6666666666666
- V381.99999999999994
- H361.3333333333333
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411\\" stroke=\\"url(#dag-grad60)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip70)\\"></path>
- </g>
- <g id=\\"dag-link51\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad61\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"638.6666666666666\\" x2=\\"884\\">
- <stop offset=\\"0%\\" stop-color=\\"#d84280\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#006887\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip71\\">
- <path d=\\"
- M622.6666666666666, 270.1890725105691
- V299.1890725105691
- H900
- V241.0000000000001
- H622.6666666666666
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691\\" stroke=\\"url(#dag-grad61)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip71)\\"></path>
- </g>
- <g id=\\"dag-link52\\" class=\\"dag-link gl-transition-property-stroke-opacity gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\">
- <linearGradient id=\\"dag-grad62\\" gradientUnits=\\"userSpaceOnUse\\" x1=\\"638.6666666666666\\" x2=\\"884\\">
- <stop offset=\\"0%\\" stop-color=\\"#3547de\\"></stop>
- <stop offset=\\"100%\\" stop-color=\\"#275600\\"></stop>
- </linearGradient>
- <clipPath id=\\"dag-clip72\\">
- <path d=\\"
- M622.6666666666666, 411
- V440
- H900
- V382
- H622.6666666666666
- Z
- \\"></path>
- </clipPath>
- <path d=\\"M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411\\" stroke=\\"url(#dag-grad62)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip72)\\"></path>
- </g>
- </g>
- <g>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node73\\" stroke=\\"#e17223\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"108\\" x2=\\"108\\" y1=\\"104\\" y2=\\"154.00000000000003\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node74\\" stroke=\\"#83ab4a\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"369\\" x2=\\"369\\" y1=\\"104\\" y2=\\"154\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node75\\" stroke=\\"#5772ff\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"108\\" x2=\\"108\\" y1=\\"187.00000000000003\\" y2=\\"237.00000000000003\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node76\\" stroke=\\"#b24800\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"108\\" x2=\\"108\\" y1=\\"270\\" y2=\\"320.00000000000006\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node77\\" stroke=\\"#25d2d2\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"108\\" x2=\\"108\\" y1=\\"353.00000000000006\\" y2=\\"403.0000000000001\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node78\\" stroke=\\"#6f3500\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"630\\" x2=\\"630\\" y1=\\"104.0000000000002\\" y2=\\"212.00000000000009\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node79\\" stroke=\\"#006887\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"369\\" x2=\\"369\\" y1=\\"244.99999999999977\\" y2=\\"294.99999999999994\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node80\\" stroke=\\"#487900\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"369\\" x2=\\"369\\" y1=\\"327.99999999999994\\" y2=\\"436\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node81\\" stroke=\\"#d84280\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"630\\" x2=\\"630\\" y1=\\"245.00000000000009\\" y2=\\"353\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node82\\" stroke=\\"#3547de\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"630\\" x2=\\"630\\" y1=\\"386\\" y2=\\"436\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node83\\" stroke=\\"#006887\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"892\\" x2=\\"892\\" y1=\\"245.18907251056908\\" y2=\\"295.1890725105691\\"></line>
- <line class=\\"dag-node gl-transition-property-stroke gl-cursor-pointer gl-transition-duration-slow gl-transition-timing-function-ease\\" id=\\"dag-node84\\" stroke=\\"#275600\\" stroke-width=\\"16\\" stroke-linecap=\\"round\\" x1=\\"892\\" x2=\\"892\\" y1=\\"386\\" y2=\\"436\\"></line>
- </g>
- <g class=\\"gl-font-sm\\">
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58.00000000000003px\\" width=\\"84\\" x=\\"8\\" y=\\"100\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58.00000000000003px; text-align: right;\\">build_a</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"369.3333333333333\\" y=\\"75\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: left;\\">test_a</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58px\\" width=\\"84\\" x=\\"8\\" y=\\"183.00000000000003\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58px; text-align: right;\\">test_b</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58.00000000000006px\\" width=\\"84\\" x=\\"8\\" y=\\"266\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58.00000000000006px; text-align: right;\\">post_test_a</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58.00000000000006px\\" width=\\"84\\" x=\\"8\\" y=\\"349.00000000000006\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58.00000000000006px; text-align: right;\\">post_test_b</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"630.6666666666666\\" y=\\"75.0000000000002\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: right;\\">post_test_c</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"369.3333333333333\\" y=\\"215.99999999999977\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: left;\\">staging_a</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"369.3333333333333\\" y=\\"298.99999999999994\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: left;\\">staging_b</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"630.6666666666666\\" y=\\"216.00000000000009\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: right;\\">canary_a</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"25px\\" width=\\"84\\" x=\\"630.6666666666666\\" y=\\"357\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 25px; text-align: right;\\">canary_c</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58px\\" width=\\"84\\" x=\\"908\\" y=\\"241.18907251056908\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58px; text-align: left;\\">production_a</div>
- </foreignObject>
- <foreignObject requiredFeatures=\\"http://www.w3.org/TR/SVG11/feature#Extensibility\\" height=\\"58px\\" width=\\"84\\" x=\\"908\\" y=\\"382\\" class=\\"gl-overflow-visible\\">
- <div class=\\"gl-display-flex gl-pointer-events-none gl-flex-direction-column gl-justify-content-center gl-overflow-wrap-break\\" style=\\"height: 58px; text-align: left;\\">production_d</div>
- </foreignObject>
- </g>
-</svg>"
-`;
diff --git a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
deleted file mode 100644
index 82206e907ff..00000000000
--- a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
+++ /dev/null
@@ -1,30 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Links Inner component with a large number of needs matches snapshot and has expected path 1`] = `
-"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
- <path d=\\"M202,118C52,118,52,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M202,118C62,118,62,148,112,148\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M222,138C72,138,72,158,122,158\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M212,128C82,128,82,168,132,168\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M232,148C92,148,92,178,142,178\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- </svg> </div>"
-`;
-
-exports[`Links Inner component with a parallel need matches snapshot and has expected path 1`] = `
-"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
- <path d=\\"M192,108C32,108,32,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- </svg> </div>"
-`;
-
-exports[`Links Inner component with one need matches snapshot and has expected path 1`] = `
-"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
- <path d=\\"M202,118C52,118,52,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- </svg> </div>"
-`;
-
-exports[`Links Inner component with same stage needs matches snapshot and has expected path 1`] = `
-"<div class=\\"gl-display-flex gl-relative\\" totalgroups=\\"10\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
- <path d=\\"M192,108C32,108,32,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- <path d=\\"M202,118C42,118,42,128,92,128\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
- </svg> </div>"
-`;
diff --git a/spec/frontend/pipelines/notification/mock_data.js b/spec/frontend/pipelines/notification/mock_data.js
deleted file mode 100644
index e36f391a854..00000000000
--- a/spec/frontend/pipelines/notification/mock_data.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const randomWarning = {
- content: 'another random warning',
- id: 'gid://gitlab/Ci::PipelineMessage/272',
-};
-
-const rootTypeWarning = {
- content: 'root `types` will be removed in 15.0.',
- id: 'gid://gitlab/Ci::PipelineMessage/273',
-};
-
-const typeWarning = {
- content: '`type` will be removed in 15.0.',
- id: 'gid://gitlab/Ci::PipelineMessage/274',
-};
-
-function createWarningMock(warnings) {
- return {
- data: {
- project: {
- id: 'gid://gitlab/Project/28"',
- pipeline: {
- id: 'gid://gitlab/Ci::Pipeline/183',
- warningMessages: warnings,
- },
- },
- },
- };
-}
-
-export const mockWarningsWithoutDeprecation = createWarningMock([randomWarning]);
-export const mockWarningsRootType = createWarningMock([rootTypeWarning]);
-export const mockWarningsType = createWarningMock([typeWarning]);
-export const mockWarningsTypesAll = createWarningMock([rootTypeWarning, typeWarning]);
diff --git a/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
index f675b6cf15c..7d5e0cccb38 100644
--- a/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
+++ b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
@@ -7,7 +7,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<label>
Preview
</label>
-
<table
class="code"
>
@@ -16,71 +15,66 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="1"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
<span
class="c1"
>
- #
+ #
<span
- class="idiff deletion"
+ class="deletion idiff"
>
Removed
</span>
- content
+ content
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="1"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
<span
class="c1"
>
- #
+ #
<span
- class="idiff addition"
+ class="addition idiff"
>
Added
</span>
- content
+ content
</span>
</span>
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="2"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
<span
@@ -88,13 +82,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
v
</span>
-
<span
class="o"
>
=
</span>
-
<span
class="mi"
>
@@ -102,17 +94,15 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="2"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
<span
@@ -120,13 +110,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
v
</span>
-
<span
class="o"
>
=
</span>
-
<span
class="mi"
>
@@ -135,20 +123,18 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="3"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
<span
@@ -156,13 +142,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
s
</span>
-
<span
class="o"
>
=
</span>
-
<span
class="s"
>
@@ -170,17 +154,15 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="3"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
<span
@@ -188,13 +170,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
s
</span>
-
<span
class="o"
>
=
</span>
-
<span
class="s"
>
@@ -203,52 +183,46 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="4"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span />
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="4"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span />
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="5"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
<span
@@ -256,19 +230,16 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
for
</span>
-
<span
class="n"
>
i
</span>
-
<span
class="ow"
>
in
</span>
-
<span
class="nb"
>
@@ -294,7 +265,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
,
</span>
-
<span
class="mi"
>
@@ -307,17 +277,15 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="5"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
<span
@@ -325,19 +293,16 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
for
</span>
-
<span
class="n"
>
i
</span>
-
<span
class="ow"
>
in
</span>
-
<span
class="nb"
>
@@ -363,7 +328,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
,
</span>
-
<span
class="mi"
>
@@ -377,25 +341,21 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="6"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
- <span>
-
- </span>
+ <span />
<span
class="k"
>
@@ -411,13 +371,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
i
</span>
-
<span
class="o"
>
+
</span>
-
<span
class="mi"
>
@@ -430,22 +388,18 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="6"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
- <span>
-
- </span>
+ <span />
<span
class="k"
>
@@ -461,13 +415,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
i
</span>
-
<span
class="o"
>
+
</span>
-
<span
class="mi"
>
@@ -481,52 +433,46 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="7"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span />
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="7"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span />
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="8"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
<span
@@ -534,7 +480,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
class
</span>
-
<span
class="nc"
>
@@ -557,17 +502,15 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="8"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
<span
@@ -575,7 +518,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
class
</span>
-
<span
class="nc"
>
@@ -599,31 +541,26 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="9"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
- <span>
-
- </span>
+ <span />
<span
class="k"
>
def
</span>
-
<span
class="nf"
>
@@ -644,7 +581,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
,
</span>
-
<span
class="n"
>
@@ -657,28 +593,23 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="9"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
- <span>
-
- </span>
+ <span />
<span
class="k"
>
def
</span>
-
<span
class="nf"
>
@@ -699,7 +630,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
,
</span>
-
<span
class="n"
>
@@ -713,25 +643,21 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="10"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
- <span>
-
- </span>
+ <span />
<span
class="bp"
>
@@ -747,13 +673,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
val
</span>
-
<span
class="o"
>
=
</span>
-
<span
class="n"
>
@@ -761,22 +685,18 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="10"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
- <span>
-
- </span>
+ <span />
<span
class="bp"
>
@@ -792,13 +712,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
val
</span>
-
<span
class="o"
>
=
</span>
-
<span
class="n"
>
@@ -807,25 +725,21 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</td>
</tr>
-
<tr
class="line_holder parallel"
>
<td
- class="old_line diff-line-num old"
+ class="diff-line-num old old_line"
>
<a
data-linenumber="11"
/>
</td>
-
<td
- class="line_content parallel left-side old"
+ class="left-side line_content old parallel"
>
<span>
- <span>
-
- </span>
+ <span />
<span
class="bp"
>
@@ -841,13 +755,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
next
</span>
-
<span
class="o"
>
=
</span>
-
<span
class="bp"
>
@@ -855,22 +767,18 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
</span>
</span>
</td>
-
<td
- class="new_line diff-line-num new"
+ class="diff-line-num new new_line"
>
<a
data-linenumber="11"
/>
</td>
-
<td
- class="line_content parallel right-side new"
+ class="line_content new parallel right-side"
>
<span>
- <span>
-
- </span>
+ <span />
<span
class="bp"
>
@@ -886,13 +794,11 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
>
next
</span>
-
<span
class="o"
>
=
</span>
-
<span
class="bp"
>
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index d40e2d7a48c..7ea3a74418d 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -72,11 +72,11 @@ describe('CommitFormModal', () => {
it('Shows modal', () => {
createComponent();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+ const rootWrapper = createWrapper(wrapper.vm.$root);
- wrapper.vm.show();
+ eventHub.$emit(mockData.modalPropsData.openModal);
- expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, mockData.modalPropsData.modalId);
+ expect(rootWrapper.emitted(BV_SHOW_MODAL)[0]).toContain(mockData.modalPropsData.modalId);
});
it('Clears the modal state once modal is hidden', () => {
@@ -150,8 +150,9 @@ describe('CommitFormModal', () => {
it('Action primary button dispatches submit action', () => {
getByText(mockData.modalPropsData.i18n.actionPrimaryText).trigger('click');
+ const formSubmitSpy = jest.spyOn(findForm().element, 'submit');
- expect(wrapper.vm.$refs.form.$el.submit).toHaveBeenCalled();
+ expect(formSubmitSpy).toHaveBeenCalled();
});
it('Changes the start_branch input value', async () => {
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
deleted file mode 100644
index e289569f8ce..00000000000
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue';
-
-const defaultProps = {
- refsProjectPath: 'some/refs/path',
- revisionText: 'Target',
- paramsName: 'from',
- paramsBranch: 'main',
-};
-
-jest.mock('~/alert');
-
-describe('RevisionDropdown component', () => {
- let wrapper;
- let axiosMock;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(RevisionDropdown, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
- createComponent();
- });
-
- afterEach(() => {
- axiosMock.restore();
- });
-
- const findGlDropdown = () => wrapper.findComponent(GlDropdown);
- const findBranchesDropdownItem = () =>
- wrapper.findAllComponents('[data-testid="branches-dropdown-item"]');
- const findTagsDropdownItem = () =>
- wrapper.findAllComponents('[data-testid="tags-dropdown-item"]');
-
- it('sets hidden input', () => {
- expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe(
- defaultProps.paramsBranch,
- );
- });
-
- it('update the branches on success', async () => {
- const Branches = ['branch-1', 'branch-2'];
- const Tags = ['tag-1', 'tag-2', 'tag-3'];
-
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
- Branches,
- Tags,
- });
-
- createComponent();
-
- expect(findBranchesDropdownItem()).toHaveLength(0);
- expect(findTagsDropdownItem()).toHaveLength(0);
-
- await waitForPromises();
-
- Branches.forEach((branch, index) => {
- expect(findBranchesDropdownItem().at(index).text()).toBe(branch);
- });
-
- Tags.forEach((tag, index) => {
- expect(findTagsDropdownItem().at(index).text()).toBe(tag);
- });
-
- expect(findBranchesDropdownItem()).toHaveLength(Branches.length);
- expect(findTagsDropdownItem()).toHaveLength(Tags.length);
- });
-
- it('sets branches and tags to be an empty array when no tags or branches are given', async () => {
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
- Branches: undefined,
- Tags: undefined,
- });
-
- await waitForPromises();
-
- expect(findBranchesDropdownItem()).toHaveLength(0);
- expect(findTagsDropdownItem()).toHaveLength(0);
- });
-
- it('shows an alert on error', async () => {
- axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
-
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalled();
- });
-
- describe('GlDropdown component', () => {
- it('renders props', () => {
- expect(wrapper.props()).toEqual(expect.objectContaining(defaultProps));
- });
-
- it('display default text', () => {
- createComponent({
- paramsBranch: null,
- });
- expect(findGlDropdown().props('text')).toBe('Select branch/tag');
- });
-
- it('display params branch text', () => {
- expect(findGlDropdown().props('text')).toBe(defaultProps.paramsBranch);
- });
-
- it('emits a "selectRevision" event when a revision is selected', async () => {
- const findGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findFirstGlDropdownItem = () => findGlDropdownItems().at(0);
- const branchName = 'some-branch';
-
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
- Branches: [branchName],
- });
-
- createComponent();
- await waitForPromises();
-
- findFirstGlDropdownItem().vm.$emit('click');
-
- expect(wrapper.emitted()).toEqual({
- selectRevision: [[{ direction: 'from', revision: branchName }]],
- });
- });
- });
-});
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index 4893ee26178..479530c1d38 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -10,12 +10,10 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
type="hidden"
value="delete"
/>
-
<input
name="authenticity_token"
type="hidden"
/>
-
<delete-modal-stub
confirmphrase="foo"
forkscount="3"
@@ -23,7 +21,6 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
mergerequestscount="2"
starscount="4"
/>
-
<gl-button-stub
buttontextclasses=""
category="primary"
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
index 61bcd44724c..efce72271e0 100644
--- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap
@@ -7,7 +7,6 @@ exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = `
<p>
Some title
</p>
-
<glareachart-stub
annotations=""
data="[object Object],[object Object]"
diff --git a/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap b/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
index 5ec0ad794fb..16d291804cc 100644
--- a/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
+++ b/spec/frontend/projects/pipelines/charts/components/__snapshots__/statistics_list_spec.js.snap
@@ -6,7 +6,6 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
<span>
Total:
</span>
-
<strong>
4 pipelines
</strong>
@@ -15,7 +14,6 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
<span>
Successful:
</span>
-
<strong>
2 pipelines
</strong>
@@ -24,20 +22,16 @@ exports[`StatisticsList displays the counts data with labels 1`] = `
<span>
Failed:
</span>
-
<gl-link-stub
href="/flightjs/Flight/-/pipelines?page=1&scope=all&status=failed"
>
-
- 2 pipelines
-
+ 2 pipelines
</gl-link-stub>
</li>
<li>
<span>
Success ratio:
</span>
-
<strong>
50.00%
</strong>
diff --git a/spec/frontend/projects/settings/access_dropdown_spec.js b/spec/frontend/projects/settings/access_dropdown_spec.js
deleted file mode 100644
index a94d7669b2b..00000000000
--- a/spec/frontend/projects/settings/access_dropdown_spec.js
+++ /dev/null
@@ -1,204 +0,0 @@
-import $ from 'jquery';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import AccessDropdown from '~/projects/settings/access_dropdown';
-import { LEVEL_TYPES } from '~/projects/settings/constants';
-
-describe('AccessDropdown', () => {
- const defaultLabel = 'dummy default label';
- let dropdown;
-
- beforeEach(() => {
- setHTMLFixture(`
- <div id="dummy-dropdown">
- <span class="dropdown-toggle-text"></span>
- </div>
- `);
- const $dropdown = $('#dummy-dropdown');
- $dropdown.data('defaultLabel', defaultLabel);
- const options = {
- $dropdown,
- accessLevelsData: {
- roles: [
- {
- id: 42,
- text: 'Dummy Role',
- },
- ],
- },
- };
- dropdown = new AccessDropdown(options);
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('toggleLabel', () => {
- let $dropdownToggleText;
- const dummyItems = [
- { type: LEVEL_TYPES.ROLE, access_level: 42 },
- { type: LEVEL_TYPES.USER },
- { type: LEVEL_TYPES.USER },
- { type: LEVEL_TYPES.GROUP },
- { type: LEVEL_TYPES.GROUP },
- { type: LEVEL_TYPES.GROUP },
- { type: LEVEL_TYPES.DEPLOY_KEY },
- { type: LEVEL_TYPES.DEPLOY_KEY },
- { type: LEVEL_TYPES.DEPLOY_KEY },
- ];
-
- beforeEach(() => {
- $dropdownToggleText = $('.dropdown-toggle-text');
- });
-
- it('displays number of items', () => {
- dropdown.setSelectedItems(dummyItems);
- $dropdownToggleText.addClass('is-default');
-
- const label = dropdown.toggleLabel();
-
- expect(label).toBe('1 role, 2 users, 3 deploy keys, 3 groups');
- expect($dropdownToggleText).not.toHaveClass('is-default');
- });
-
- describe('without selected items', () => {
- beforeEach(() => {
- dropdown.setSelectedItems([]);
- });
-
- it('falls back to default label', () => {
- const label = dropdown.toggleLabel();
-
- expect(label).toBe(defaultLabel);
- expect($dropdownToggleText).toHaveClass('is-default');
- });
- });
-
- describe('with only role', () => {
- beforeEach(() => {
- dropdown.setSelectedItems(dummyItems.filter((item) => item.type === LEVEL_TYPES.ROLE));
- $dropdownToggleText.addClass('is-default');
- });
-
- it('displays the role name', () => {
- const label = dropdown.toggleLabel();
-
- expect(label).toBe('Dummy Role');
- expect($dropdownToggleText).not.toHaveClass('is-default');
- });
- });
-
- describe('with only users', () => {
- beforeEach(() => {
- dropdown.setSelectedItems(dummyItems.filter((item) => item.type === LEVEL_TYPES.USER));
- $dropdownToggleText.addClass('is-default');
- });
-
- it('displays number of users', () => {
- const label = dropdown.toggleLabel();
-
- expect(label).toBe('2 users');
- expect($dropdownToggleText).not.toHaveClass('is-default');
- });
- });
-
- describe('with only groups', () => {
- beforeEach(() => {
- dropdown.setSelectedItems(dummyItems.filter((item) => item.type === LEVEL_TYPES.GROUP));
- $dropdownToggleText.addClass('is-default');
- });
-
- it('displays number of groups', () => {
- const label = dropdown.toggleLabel();
-
- expect(label).toBe('3 groups');
- expect($dropdownToggleText).not.toHaveClass('is-default');
- });
- });
-
- describe('with users and groups', () => {
- beforeEach(() => {
- const selectedTypes = [LEVEL_TYPES.GROUP, LEVEL_TYPES.USER];
- dropdown.setSelectedItems(dummyItems.filter((item) => selectedTypes.includes(item.type)));
- $dropdownToggleText.addClass('is-default');
- });
-
- it('displays number of groups', () => {
- const label = dropdown.toggleLabel();
-
- expect(label).toBe('2 users, 3 groups');
- expect($dropdownToggleText).not.toHaveClass('is-default');
- });
- });
-
- describe('with users and deploy keys', () => {
- beforeEach(() => {
- const selectedTypes = [LEVEL_TYPES.DEPLOY_KEY, LEVEL_TYPES.USER];
- dropdown.setSelectedItems(dummyItems.filter((item) => selectedTypes.includes(item.type)));
- $dropdownToggleText.addClass('is-default');
- });
-
- it('displays number of deploy keys', () => {
- const label = dropdown.toggleLabel();
-
- expect(label).toBe('2 users, 3 deploy keys');
- expect($dropdownToggleText).not.toHaveClass('is-default');
- });
- });
- });
-
- describe('userRowHtml', () => {
- it('escapes users name', () => {
- const user = {
- avatar_url: '',
- name: '<img src=x onerror=alert(document.domain)>',
- username: 'test',
- };
- const template = dropdown.userRowHtml(user);
-
- expect(template).not.toContain(user.name);
- });
-
- it('show user avatar correctly', () => {
- const user = {
- id: 613,
- avatar_url: 'some_valid_avatar.png',
- name: 'test',
- username: 'test',
- };
- const template = dropdown.userRowHtml(user);
-
- expect(template).toContain(user.avatar_url);
- expect(template).not.toContain('identicon');
- });
-
- it('show identicon when user do not have avatar', () => {
- const user = {
- id: 613,
- avatar_url: '',
- name: 'test',
- username: 'test',
- };
- const template = dropdown.userRowHtml(user);
-
- expect(template).toContain('identicon');
- });
- });
-
- describe('deployKeyRowHtml', () => {
- const deployKey = {
- id: 1,
- title: 'title <script>alert(document.domain)</script>',
- fullname: 'fullname <script>alert(document.domain)</script>',
- avatar_url: '',
- username: '',
- };
-
- it('escapes deploy key title and fullname', () => {
- const template = dropdown.deployKeyRowHtml(deployKey);
-
- expect(template).not.toContain(deployKey.title);
- expect(template).not.toContain(deployKey.fullname);
- });
- });
-});
diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
index ce696ee321b..0ed2e51e8c3 100644
--- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -14,13 +14,11 @@ import AccessDropdown, { i18n } from '~/projects/settings/components/access_drop
import { ACCESS_LEVELS, LEVEL_TYPES } from '~/projects/settings/constants';
jest.mock('~/projects/settings/api/access_dropdown_api', () => ({
- getGroups: jest.fn().mockResolvedValue({
- data: [
- { id: 4, name: 'group4' },
- { id: 5, name: 'group5' },
- { id: 6, name: 'group6' },
- ],
- }),
+ getGroups: jest.fn().mockResolvedValue([
+ { id: 4, name: 'group4' },
+ { id: 5, name: 'group5' },
+ { id: 6, name: 'group6' },
+ ]),
getUsers: jest.fn().mockResolvedValue({
data: [
{ id: 7, name: 'user7' },
@@ -50,6 +48,7 @@ jest.mock('~/projects/settings/api/access_dropdown_api', () => ({
describe('Access Level Dropdown', () => {
let wrapper;
+ const defaultToggleClass = 'gl-text-gray-500!';
const mockAccessLevelsData = [
{
id: 1,
@@ -63,6 +62,10 @@ describe('Access Level Dropdown', () => {
id: 3,
text: 'role3',
},
+ {
+ id: 0,
+ text: 'No one',
+ },
];
const createComponent = ({
@@ -140,7 +143,7 @@ describe('Access Level Dropdown', () => {
});
it('renders dropdown item for each access level type', () => {
- expect(findAllDropdownItems()).toHaveLength(12);
+ expect(findAllDropdownItems()).toHaveLength(13);
});
it.each`
@@ -177,26 +180,26 @@ describe('Access Level Dropdown', () => {
const customLabel = 'Set the access level';
createComponent({ label: customLabel });
expect(findDropdownToggleLabel()).toBe(customLabel);
- expect(findDropdown().props('toggleClass')).toBe('gl-text-gray-500!');
+ expect(findDropdown().props('toggleClass')[defaultToggleClass]).toBe(true);
});
it('when no items selected, displays a default fallback label and has default CSS class', () => {
- expect(findDropdownToggleLabel()).toBe(i18n.selectUsers);
- expect(findDropdown().props('toggleClass')).toBe('gl-text-gray-500!');
+ expect(findDropdownToggleLabel()).toBe(i18n.defaultLabel);
+ expect(findDropdown().props('toggleClass')[defaultToggleClass]).toBe(true);
});
- it('displays a number of selected items for each group level', async () => {
+ it('displays selected items for each group level', async () => {
dropdownItems.wrappers.forEach((item) => {
item.trigger('click');
});
await nextTick();
- expect(findDropdownToggleLabel()).toBe('3 roles, 3 users, 3 deploy keys, 3 groups');
+ expect(findDropdownToggleLabel()).toBe('No role, 3 users, 3 deploy keys, 3 groups');
});
it('with only role selected displays the role name and has no class applied', async () => {
await findItemByNameAndClick('role1');
expect(findDropdownToggleLabel()).toBe('role1');
- expect(findDropdown().props('toggleClass')).toBe('');
+ expect(findDropdown().props('toggleClass')[defaultToggleClass]).toBe(false);
});
it('with only groups selected displays the number of selected groups', async () => {
@@ -204,14 +207,14 @@ describe('Access Level Dropdown', () => {
await findItemByNameAndClick('group5');
await findItemByNameAndClick('group6');
expect(findDropdownToggleLabel()).toBe('3 groups');
- expect(findDropdown().props('toggleClass')).toBe('');
+ expect(findDropdown().props('toggleClass')[defaultToggleClass]).toBe(false);
});
it('with only users selected displays the number of selected users', async () => {
await findItemByNameAndClick('user7');
await findItemByNameAndClick('user8');
expect(findDropdownToggleLabel()).toBe('2 users');
- expect(findDropdown().props('toggleClass')).toBe('');
+ expect(findDropdown().props('toggleClass')[defaultToggleClass]).toBe(false);
});
it('with users and groups selected displays the number of selected users & groups', async () => {
@@ -220,7 +223,7 @@ describe('Access Level Dropdown', () => {
await findItemByNameAndClick('user7');
await findItemByNameAndClick('user9');
expect(findDropdownToggleLabel()).toBe('2 users, 2 groups');
- expect(findDropdown().props('toggleClass')).toBe('');
+ expect(findDropdown().props('toggleClass')[defaultToggleClass]).toBe(false);
});
it('with users and deploy keys selected displays the number of selected users & keys', async () => {
@@ -228,7 +231,7 @@ describe('Access Level Dropdown', () => {
await findItemByNameAndClick('key10');
await findItemByNameAndClick('key11');
expect(findDropdownToggleLabel()).toBe('1 user, 2 deploy keys');
- expect(findDropdown().props('toggleClass')).toBe('');
+ expect(findDropdown().props('toggleClass')[defaultToggleClass]).toBe(false);
});
});
@@ -393,4 +396,20 @@ describe('Access Level Dropdown', () => {
expect(wrapper.emitted('hidden')[0][0]).toStrictEqual([{ access_level: 2 }]);
});
});
+
+ describe('when no license and accessLevel is MERGE', () => {
+ beforeEach(async () => {
+ createComponent({ hasLicense: false, accessLevel: ACCESS_LEVELS.MERGE });
+ await waitForPromises();
+ });
+
+ it('dropdown is single-select', () => {
+ const dropdownItems = findAllDropdownItems();
+
+ findDropdownItemWithText(dropdownItems, mockAccessLevelsData[0].text).trigger('click');
+ findDropdownItemWithText(dropdownItems, mockAccessLevelsData[1].text).trigger('click');
+
+ expect(wrapper.emitted('select')[1]).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
index ded8b181c4e..9b012995ea4 100644
--- a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js
@@ -1,6 +1,8 @@
import { mount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
import CustomEmailForm from '~/projects/settings_service_desk/components/custom_email_form.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE } from '~/projects/settings_service_desk/custom_email_constants';
@@ -15,6 +17,7 @@ describe('CustomEmailForm', () => {
const findForm = () => wrapper.find('form');
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
+ const findLink = () => wrapper.findComponent(GlLink);
const findInputByTestId = (testId) => wrapper.findByTestId(testId).find('input');
const findCustomEmailInput = () => findInputByTestId('form-custom-email');
const findSmtpAddressInput = () => findInputByTestId('form-smtp-address');
@@ -35,6 +38,16 @@ describe('CustomEmailForm', () => {
wrapper = extendedWrapper(mount(CustomEmailForm, { propsData: { ...defaultProps, ...props } }));
};
+ it('displays help page link', () => {
+ createWrapper();
+
+ expect(findLink().attributes('href')).toBe(
+ helpPagePath('user/project/service_desk/configure.html', {
+ anchor: 'custom-email-address',
+ }),
+ );
+ });
+
it('renders a copy to clipboard button', () => {
createWrapper();
diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_wrapper_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_wrapper_spec.js
index e54d09cf82f..174e05ceeee 100644
--- a/spec/frontend/projects/settings_service_desk/components/custom_email_wrapper_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/custom_email_wrapper_spec.js
@@ -1,7 +1,8 @@
import { nextTick } from 'vue';
-import { GlLink, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_OK, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
@@ -40,19 +41,21 @@ describe('CustomEmailWrapper', () => {
const showToast = jest.fn();
const createWrapper = (props = {}) => {
- wrapper = mount(CustomEmailWrapper, {
- propsData: { ...defaultProps, ...props },
- mocks: {
- $toast: {
- show: showToast,
+ wrapper = extendedWrapper(
+ mount(CustomEmailWrapper, {
+ propsData: { ...defaultProps, ...props },
+ mocks: {
+ $toast: {
+ show: showToast,
+ },
},
- },
- });
+ }),
+ );
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findFeedbackLink = () => wrapper.findComponent(GlLink);
+ const findFeedbackLink = () => wrapper.findByTestId('feedback-link');
const findCustomEmailForm = () => wrapper.findComponent(CustomEmailForm);
const findCustomEmail = () => wrapper.findComponent(CustomEmail);
const findCustomEmailConfirmModal = () => wrapper.findComponent(CustomEmailConfirmModal);
diff --git a/spec/frontend/protected_branches/protected_branch_create_spec.js b/spec/frontend/protected_branches/protected_branch_create_spec.js
index 4b634c52b01..e2a0f02e0cf 100644
--- a/spec/frontend/protected_branches/protected_branch_create_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_create_spec.js
@@ -1,5 +1,8 @@
+import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
+import { ACCESS_LEVELS } from '~/protected_branches/constants';
+import axios from '~/lib/utils/axios_utils';
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
const CODE_OWNER_TOGGLE_TESTID = 'code-owner-toggle';
@@ -9,7 +12,12 @@ const IS_LOADING_CLASS = 'toggle-loading';
describe('ProtectedBranchCreate', () => {
beforeEach(() => {
- jest.spyOn(ProtectedBranchCreate.prototype, 'buildDropdowns').mockImplementation();
+ // eslint-disable-next-line no-unused-vars
+ const mock = new MockAdapter(axios);
+ window.gon = {
+ merge_access_levels: { roles: [] },
+ push_access_levels: { roles: [] },
+ };
});
const findForcePushToggle = () =>
@@ -34,6 +42,12 @@ describe('ProtectedBranchCreate', () => {
data-label="Toggle code owner approval"
data-is-checked="${codeOwnerToggleChecked}"
data-testid="${CODE_OWNER_TOGGLE_TESTID}"></span>
+ <div class="merge_access_levels-container">
+ <div class="js-allowed-to-merge"/>
+ </div>
+ <div class="push_access_levels-container">
+ <div class="js-allowed-to-push"/>
+ </div>
<input type="submit" />
</form>
`);
@@ -85,14 +99,6 @@ describe('ProtectedBranchCreate', () => {
forcePushToggleChecked: false,
codeOwnerToggleChecked: true,
});
-
- // Mock access levels. This should probably be improved in future iterations.
- protectedBranchCreate.merge_access_levels_dropdown = {
- getSelectedItems: () => [],
- };
- protectedBranchCreate.push_access_levels_dropdown = {
- getSelectedItems: () => [],
- };
});
afterEach(() => {
@@ -116,4 +122,31 @@ describe('ProtectedBranchCreate', () => {
});
});
});
+
+ describe('access dropdown', () => {
+ let protectedBranchCreate;
+
+ beforeEach(() => {
+ protectedBranchCreate = create();
+ });
+
+ it('should be initialized', () => {
+ expect(protectedBranchCreate[`${ACCESS_LEVELS.MERGE}_dropdown`]).toBeDefined();
+ expect(protectedBranchCreate[`${ACCESS_LEVELS.PUSH}_dropdown`]).toBeDefined();
+ });
+
+ describe('`select` event is emitted', () => {
+ const selected = ['foo', 'bar'];
+
+ it('should update selected merged access items', () => {
+ protectedBranchCreate[`${ACCESS_LEVELS.MERGE}_dropdown`].$emit('select', selected);
+ expect(protectedBranchCreate.selectedItems[ACCESS_LEVELS.MERGE]).toEqual(selected);
+ });
+
+ it('should update selected push access items', () => {
+ protectedBranchCreate[`${ACCESS_LEVELS.PUSH}_dropdown`].$emit('select', selected);
+ expect(protectedBranchCreate.selectedItems[ACCESS_LEVELS.PUSH]).toEqual(selected);
+ });
+ });
+ });
});
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index e1966908452..6422856ba22 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -20,7 +20,7 @@ describe('ProtectedBranchEdit', () => {
let mock;
beforeEach(() => {
- jest.spyOn(ProtectedBranchEdit.prototype, 'buildDropdowns').mockImplementation();
+ jest.spyOn(ProtectedBranchEdit.prototype, 'initDropdowns').mockImplementation();
mock = new MockAdapter(axios);
});
diff --git a/spec/frontend/protected_tags/mock_data.js b/spec/frontend/protected_tags/mock_data.js
new file mode 100644
index 00000000000..dacdecdfe74
--- /dev/null
+++ b/spec/frontend/protected_tags/mock_data.js
@@ -0,0 +1,18 @@
+export const mockAccessLevels = [
+ {
+ id: 30,
+ text: 'Developers + Maintainers',
+ },
+ {
+ id: 40,
+ text: 'Maintainers',
+ },
+ {
+ id: 60,
+ text: 'Instance admins',
+ },
+ {
+ id: 0,
+ text: 'No one',
+ },
+];
diff --git a/spec/frontend/protected_tags/protected_tag_edit_spec.js b/spec/frontend/protected_tags/protected_tag_edit_spec.js
new file mode 100644
index 00000000000..f56b3a70d1b
--- /dev/null
+++ b/spec/frontend/protected_tags/protected_tag_edit_spec.js
@@ -0,0 +1,113 @@
+import MockAdapter from 'axios-mock-adapter';
+import { ACCESS_LEVELS, LEVEL_TYPES } from '~/protected_tags/constants';
+import ProtectedTagEdit, { i18n } from '~/protected_tags/protected_tag_edit.vue';
+import AccessDropdown from '~/projects/settings/components/access_dropdown.vue';
+import axios from '~/lib/utils/axios_utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import { mockAccessLevels } from './mock_data';
+
+jest.mock('~/alert');
+
+describe('Protected Tag Edit', () => {
+ let wrapper;
+ let mockAxios;
+
+ const url = 'http://some.url';
+ const toggleClass = 'js-allowed-to-create gl-max-w-34';
+
+ const findAccessDropdown = () => wrapper.findComponent(AccessDropdown);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProtectedTagEdit, {
+ propsData: {
+ url,
+ accessLevelsData: mockAccessLevels,
+ searchEnabled: false,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ window.gon = {
+ api_version: 'v4',
+ deploy_access_levels: {
+ roles: [],
+ },
+ };
+ mockAxios = new MockAdapter(axios);
+ createComponent();
+ });
+
+ afterEach(() => {
+ mockAxios.restore();
+ });
+
+ it('renders access dropdown with correct props', () => {
+ expect(findAccessDropdown().props()).toMatchObject({
+ toggleClass,
+ accessLevel: ACCESS_LEVELS.CREATE,
+ accessLevelsData: mockAccessLevels,
+ searchEnabled: false,
+ });
+ });
+
+ describe('when dropdown is closed and has no changes', () => {
+ it('does not make a patch request to update permission', () => {
+ jest.spyOn(axios, 'patch');
+
+ findAccessDropdown().vm.$emit('hidden', []);
+
+ expect(axios.patch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when dropdown is closed and has changes', () => {
+ it('makes patch request to update permission', () => {
+ jest.spyOn(axios, 'patch');
+
+ const newPermissions = [{ id: 1, access_level: 30 }];
+ findAccessDropdown().vm.$emit('hidden', newPermissions);
+
+ expect(axios.patch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when permission is updated successfully', () => {
+ beforeEach(async () => {
+ const updatedPermissions = [
+ { user_id: 1, id: 1 },
+ { group_id: 1, id: 2 },
+ { access_level: 3, id: 3 },
+ ];
+ mockAxios.onPatch().replyOnce(HTTP_STATUS_OK, { [ACCESS_LEVELS.CREATE]: updatedPermissions });
+ findAccessDropdown().vm.$emit('hidden', [{ user_id: 1 }]);
+ await waitForPromises();
+ });
+
+ it('should update selected items', () => {
+ const newPreselected = [
+ { user_id: 1, id: 1, type: LEVEL_TYPES.USER },
+ { group_id: 1, id: 2, type: LEVEL_TYPES.GROUP },
+ { access_level: 3, id: 3, type: LEVEL_TYPES.ROLE },
+ ];
+ expect(findAccessDropdown().props('preselectedItems')).toEqual(newPreselected);
+ });
+ });
+
+ describe('when permission update fails', () => {
+ beforeEach(async () => {
+ mockAxios.onPatch().replyOnce(HTTP_STATUS_BAD_REQUEST, {});
+ findAccessDropdown().vm.$emit('hidden', [{ user_id: 1 }]);
+ await waitForPromises();
+ });
+
+ it('should show error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.failureMessage,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index 79792a4a0ea..c02c1bb959c 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -54,7 +54,19 @@ Object {
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
"createdAt": 2019-01-03T00:00:00.000Z,
- "descriptionHtml": "<p data-sourcepos=\\"1:1-1:23\\" dir=\\"auto\\">An okay release <gl-emoji title=\\"shrug\\" data-name=\\"shrug\\" data-unicode-version=\\"9.0\\">🤷</gl-emoji></p>",
+ "descriptionHtml": <p
+ data-sourcepos="1:1-1:23"
+ dir="auto"
+ >
+ An okay release
+ <gl-emoji
+ data-name="shrug"
+ data-unicode-version="9.0"
+ title="shrug"
+ >
+ 🤷
+ </gl-emoji>
+ </p>,
"evidences": Array [],
"historicalRelease": false,
"milestones": Array [],
@@ -148,7 +160,22 @@ Object {
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
"createdAt": 2018-12-03T00:00:00.000Z,
- "descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
+ "descriptionHtml": <p
+ data-sourcepos="1:1-1:33"
+ dir="auto"
+ >
+ Best. Release.
+ <strong>
+ Ever.
+ </strong>
+ <gl-emoji
+ data-name="rocket"
+ data-unicode-version="6.0"
+ title="rocket"
+ >
+ 🚀
+ </gl-emoji>
+ </p>,
"evidences": Array [
Object {
"__typename": "ReleaseEvidence",
@@ -368,7 +395,22 @@ Object {
},
"commitPath": "http://localhost/releases-namespace/releases-project/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0",
"createdAt": 2018-12-03T00:00:00.000Z,
- "descriptionHtml": "<p data-sourcepos=\\"1:1-1:33\\" dir=\\"auto\\">Best. Release. <strong>Ever.</strong> <gl-emoji title=\\"rocket\\" data-name=\\"rocket\\" data-unicode-version=\\"6.0\\">🚀</gl-emoji></p>",
+ "descriptionHtml": <p
+ data-sourcepos="1:1-1:33"
+ dir="auto"
+ >
+ Best. Release.
+ <strong>
+ Ever.
+ </strong>
+ <gl-emoji
+ data-name="rocket"
+ data-unicode-version="6.0"
+ title="rocket"
+ >
+ 🚀
+ </gl-emoji>
+ </p>,
"evidences": Array [
Object {
"__typename": "ReleaseEvidence",
diff --git a/spec/frontend/releases/components/__snapshots__/issuable_stats_spec.js.snap b/spec/frontend/releases/components/__snapshots__/issuable_stats_spec.js.snap
index e53ea6b2ec6..8f811d31af8 100644
--- a/spec/frontend/releases/components/__snapshots__/issuable_stats_spec.js.snap
+++ b/spec/frontend/releases/components/__snapshots__/issuable_stats_spec.js.snap
@@ -1,9 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`~/releases/components/issuable_stats.vue matches snapshot 1`] = `
-"<div class=\\"gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5\\"><span class=\\"gl-mb-2\\">
+<div
+ class="gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mb-5 gl-mr-6"
+>
+ <span
+ class="gl-mb-2"
+ >
Items
- <span class=\\"badge badge-muted badge-pill gl-badge sm\\"><!----> 10</span></span>
- <div class=\\"gl-display-flex\\"><span data-testid=\\"open-stat\\" class=\\"gl-white-space-pre-wrap\\">Open: <a href=\\"path/to/opened/items\\" class=\\"gl-link\\">1</a></span> <span class=\\"gl-mx-2\\">•</span> <span data-testid=\\"merged-stat\\" class=\\"gl-white-space-pre-wrap\\">Merged: <a href=\\"path/to/merged/items\\" class=\\"gl-link\\">7</a></span> <span class=\\"gl-mx-2\\">•</span> <span data-testid=\\"closed-stat\\" class=\\"gl-white-space-pre-wrap\\">Closed: <a href=\\"path/to/closed/items\\" class=\\"gl-link\\">2</a></span></div>
-</div>"
+ <span
+ class="badge badge-muted badge-pill gl-badge sm"
+ >
+ 10
+ </span>
+ </span>
+ <div
+ class="gl-display-flex"
+ >
+ <span
+ class="gl-white-space-pre-wrap"
+ data-testid="open-stat"
+ >
+ Open:
+ <a
+ class="gl-link"
+ href="path/to/opened/items"
+ >
+ 1
+ </a>
+ </span>
+ <span
+ class="gl-mx-2"
+ >
+ •
+ </span>
+ <span
+ class="gl-white-space-pre-wrap"
+ data-testid="merged-stat"
+ >
+ Merged:
+ <a
+ class="gl-link"
+ href="path/to/merged/items"
+ >
+ 7
+ </a>
+ </span>
+ <span
+ class="gl-mx-2"
+ >
+ •
+ </span>
+ <span
+ class="gl-white-space-pre-wrap"
+ data-testid="closed-stat"
+ >
+ Closed:
+ <a
+ class="gl-link"
+ href="path/to/closed/items"
+ >
+ 2
+ </a>
+ </span>
+ </div>
+</div>
`;
diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js
index b8030ae1fd2..26068b392d1 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -41,10 +41,10 @@ describe('Release block milestone info', () => {
const progressBar = milestoneProgressBarContainer().findComponent(GlProgressBar);
expect(progressBar.exists()).toBe(true);
- expect(progressBar.attributes()).toEqual(
+ expect(progressBar.vm.$attrs).toEqual(
expect.objectContaining({
- value: '4',
- max: '9',
+ value: 4,
+ max: 9,
}),
);
});
diff --git a/spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap b/spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap
index 836ae5c22e6..02f75edd57a 100644
--- a/spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/directory_download_links_spec.js.snap
@@ -2,14 +2,13 @@
exports[`Repository directory download links component renders downloads links for path app 1`] = `
<section
- class="border-top pt-1 mt-1"
+ class="border-top mt-1 pt-1"
>
<h5
- class="m-0 dropdown-bold-header"
+ class="dropdown-bold-header m-0"
>
Download this directory
</h5>
-
<div
class="dropdown-menu-content"
>
@@ -24,9 +23,7 @@ exports[`Repository directory download links component renders downloads links f
size="small"
variant="confirm"
>
-
zip
-
</gl-button-stub>
<gl-button-stub
buttontextclasses=""
@@ -36,9 +33,7 @@ exports[`Repository directory download links component renders downloads links f
size="small"
variant="default"
>
-
tar
-
</gl-button-stub>
</div>
</div>
@@ -47,14 +42,13 @@ exports[`Repository directory download links component renders downloads links f
exports[`Repository directory download links component renders downloads links for path app/assets 1`] = `
<section
- class="border-top pt-1 mt-1"
+ class="border-top mt-1 pt-1"
>
<h5
- class="m-0 dropdown-bold-header"
+ class="dropdown-bold-header m-0"
>
Download this directory
</h5>
-
<div
class="dropdown-menu-content"
>
@@ -69,9 +63,7 @@ exports[`Repository directory download links component renders downloads links f
size="small"
variant="confirm"
>
-
zip
-
</gl-button-stub>
<gl-button-stub
buttontextclasses=""
@@ -81,9 +73,7 @@ exports[`Repository directory download links component renders downloads links f
size="small"
variant="default"
>
-
tar
-
</gl-button-stub>
</div>
</div>
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index ede04390586..3f901dc61b8 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -2,10 +2,10 @@
exports[`Repository last commit component renders commit widget 1`] = `
<div
- class="well-segment commit gl-p-5 gl-w-full gl-display-flex"
+ class="commit gl-display-flex gl-p-5 gl-w-full well-segment"
>
<user-avatar-link-stub
- class="gl-my-2 gl-mr-4"
+ class="gl-mr-4 gl-my-2"
imgalt=""
imgcssclasses=""
imgcsswrapperclasses=""
@@ -18,9 +18,8 @@ exports[`Repository last commit component renders commit widget 1`] = `
tooltiptext=""
username=""
/>
-
<div
- class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
+ class="commit-detail flex-list gl-align-items-center gl-display-flex gl-flex-grow-1 gl-justify-content-space-between gl-min-w-0"
>
<div
class="commit-content"
@@ -32,9 +31,6 @@ exports[`Repository last commit component renders commit widget 1`] = `
>
Commit title
</gl-link-stub>
-
- <!---->
-
<div
class="committer"
>
@@ -42,12 +38,9 @@ exports[`Repository last commit component renders commit widget 1`] = `
class="commit-author-link js-user-link"
href="/test"
>
-
- Test
+ Test
</gl-link-stub>
-
- authored
-
+ authored
<timeago-tooltip-stub
cssclass=""
datetimeformat="DATE_WITH_TIME_FORMAT"
@@ -55,36 +48,24 @@ exports[`Repository last commit component renders commit widget 1`] = `
tooltipplacement="bottom"
/>
</div>
-
- <!---->
</div>
-
<div
class="gl-flex-grow-1"
/>
-
<div
- class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
+ class="commit-actions gl-align-items-center gl-display-flex gl-flex-align gl-flex-direction-row"
>
- <!---->
-
<div
class="ci-status-link"
>
- <gl-link-stub
+ <ci-badge-link-stub
+ aria-label="Pipeline: failed"
class="js-commit-pipeline"
- href="https://test.com/pipeline"
- title="Pipeline: failed"
- >
- <ci-icon-stub
- aria-label="Pipeline: failed"
- cssclasses=""
- size="24"
- status="[object Object]"
- />
- </gl-link-stub>
+ details-path="https://test.com/pipeline"
+ size="lg"
+ status="[object Object]"
+ />
</div>
-
<gl-button-group-stub
class="gl-ml-4 js-commit-sha-group"
>
@@ -100,7 +81,6 @@ exports[`Repository last commit component renders commit widget 1`] = `
>
12345678
</gl-button-stub>
-
<clipboard-button-stub
category="secondary"
class="input-group-text"
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 5ac2627dc5d..cc077e20e0b 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -8,11 +8,11 @@ import MockAdapter from 'axios-mock-adapter';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
-import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
@@ -20,8 +20,6 @@ import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
-import userInfoQuery from '~/repository/queries/user_info.query.graphql';
-import applicationInfoQuery from '~/repository/queries/application_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import * as urlUtility from '~/lib/utils/url_utility';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
@@ -34,8 +32,6 @@ import {
simpleViewerMock,
richViewerMock,
projectMock,
- userInfoMock,
- applicationInfoMock,
userPermissionsMock,
propsMock,
refMock,
@@ -46,12 +42,11 @@ jest.mock('~/repository/components/blob_viewers');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/lib/utils/common_utils');
jest.mock('~/blob/line_highlighter');
+jest.mock('~/alert');
let wrapper;
let blobInfoMockResolver;
-let userInfoMockResolver;
let projectInfoMockResolver;
-let applicationInfoMockResolver;
Vue.use(Vuex);
@@ -95,7 +90,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
const projectInfo = {
__typename: 'Project',
- id: '123',
+ id: projectMock.id,
userPermissions: {
pushCode,
forkProject,
@@ -121,19 +116,9 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
data: { isBinary, project: blobInfo },
});
- userInfoMockResolver = jest.fn().mockResolvedValue({
- data: { ...userInfoMock },
- });
-
- applicationInfoMockResolver = jest.fn().mockResolvedValue({
- data: { ...applicationInfoMock },
- });
-
const fakeApollo = createMockApollo([
[blobInfoQuery, blobInfoMockResolver],
- [userInfoQuery, userInfoMockResolver],
[projectInfoQuery, projectInfoMockResolver],
- [applicationInfoQuery, applicationInfoMockResolver],
]);
wrapper = extendedWrapper(
@@ -167,7 +152,6 @@ const execImmediately = (callback) => {
describe('Blob content viewer component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findBlobHeader = () => wrapper.findComponent(BlobHeader);
- const findWebIdeLink = () => wrapper.findComponent(WebIdeLink);
const findBlobContent = () => wrapper.findComponent(BlobContent);
const findBlobButtonGroup = () => wrapper.findComponent(BlobButtonGroup);
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
@@ -197,9 +181,22 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('hasRenderError')).toEqual(false);
expect(findBlobHeader().props('hideViewerSwitcher')).toEqual(true);
expect(findBlobHeader().props('blob')).toEqual(simpleViewerMock);
+ expect(findBlobHeader().props('showForkSuggestion')).toEqual(false);
+ expect(findBlobHeader().props('projectPath')).toEqual(propsMock.projectPath);
+ expect(findBlobHeader().props('projectId')).toEqual(projectMock.id);
expect(mockRouterPush).not.toHaveBeenCalled();
});
+ it('creates an alert when the BlobHeader component emits an error', async () => {
+ await createComponent();
+
+ findBlobHeader().vm.$emit('error');
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while loading the file. Please try again.',
+ });
+ });
+
it('copies blob text to clipboard', async () => {
jest.spyOn(navigator.clipboard, 'writeText');
await createComponent();
@@ -401,45 +398,6 @@ describe('Blob content viewer component', () => {
});
describe('BlobHeader action slot', () => {
- const { ideEditPath, editBlobPath } = simpleViewerMock;
-
- it('renders WebIdeLink button in simple viewer', async () => {
- await createComponent({ inject: { BlobContent: true, BlobReplace: true } }, mount);
-
- expect(findWebIdeLink().props()).toMatchObject({
- editUrl: editBlobPath,
- webIdeUrl: ideEditPath,
- showEditButton: true,
- showGitpodButton: applicationInfoMock.gitpodEnabled,
- gitpodEnabled: userInfoMock.currentUser.gitpodEnabled,
- showPipelineEditorButton: true,
- gitpodUrl: simpleViewerMock.gitpodBlobUrl,
- pipelineEditorUrl: simpleViewerMock.pipelineEditorPath,
- userPreferencesGitpodPath: userInfoMock.currentUser.preferencesGitpodPath,
- userProfileEnableGitpodPath: userInfoMock.currentUser.profileEnableGitpodPath,
- });
- });
-
- it('renders WebIdeLink button in rich viewer', async () => {
- await createComponent({ blob: richViewerMock }, mount);
-
- expect(findWebIdeLink().props()).toMatchObject({
- editUrl: editBlobPath,
- webIdeUrl: ideEditPath,
- showEditButton: true,
- });
- });
-
- it('renders WebIdeLink button for binary files', async () => {
- mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, axiosMockResponse);
- await createComponent({}, mount);
- expect(findWebIdeLink().props()).toMatchObject({
- editUrl: editBlobPath,
- webIdeUrl: ideEditPath,
- showEditButton: false,
- });
- });
-
describe('blob header binary file', () => {
it('passes the correct isBinary value when viewing a binary file', async () => {
mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, axiosMockResponse);
@@ -465,7 +423,6 @@ describe('Blob content viewer component', () => {
expect(findBlobHeader().props('hideViewerSwitcher')).toBe(true);
expect(findBlobHeader().props('isBinary')).toBe(true);
- expect(findWebIdeLink().props('showEditButton')).toBe(false);
});
});
@@ -538,12 +495,12 @@ describe('Blob content viewer component', () => {
beforeEach(() => createComponent({}, mount));
it('simple edit redirects to the simple editor', () => {
- findWebIdeLink().vm.$emit('edit', 'simple');
+ findBlobHeader().vm.$emit('edit', 'simple');
expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath); // eslint-disable-line import/no-deprecated
});
it('IDE edit redirects to the IDE editor', () => {
- findWebIdeLink().vm.$emit('edit', 'ide');
+ findBlobHeader().vm.$emit('edit', 'ide');
expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath); // eslint-disable-line import/no-deprecated
});
@@ -572,7 +529,7 @@ describe('Blob content viewer component', () => {
mount,
);
- findWebIdeLink().vm.$emit('edit', 'simple');
+ findBlobHeader().vm.$emit('edit', 'simple');
await nextTick();
expect(findForkSuggestion().exists()).toBe(showForkSuggestion);
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 85bf683fdf6..17ebdf8725d 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -5,10 +5,10 @@ exports[`Repository table row component renders a symlink table row 1`] = `
class="tree-item"
>
<td
- class="tree-item-file-name cursor-default position-relative"
+ class="cursor-default position-relative tree-item-file-name"
>
<a
- class="tree-item-link str-truncated"
+ class="str-truncated tree-item-link"
data-qa-selector="file_name_link"
href="https://test.com"
title="test"
@@ -26,11 +26,6 @@ exports[`Repository table row component renders a symlink table row 1`] = `
test
</span>
</a>
-
- <!---->
-
- <!---->
-
<gl-icon-stub
class="ml-1"
name="lock"
@@ -38,30 +33,25 @@ exports[`Repository table row component renders a symlink table row 1`] = `
title="Locked by Root"
/>
</td>
-
<td
- class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary"
+ class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
>
<gl-link-stub
- class="str-truncated-100 tree-commit-link gl-text-secondary"
+ class="gl-text-secondary str-truncated-100 tree-commit-link"
/>
-
- <gl-intersection-observer-stub>
- <!---->
- </gl-intersection-observer-stub>
+ <gl-intersection-observer-stub />
</td>
-
<td
- class="tree-time-ago text-right cursor-default gl-text-secondary"
+ class="cursor-default gl-text-secondary text-right tree-time-ago"
>
- <timeago-tooltip-stub
- cssclass=""
- datetimeformat="DATE_WITH_TIME_FORMAT"
- time="2019-01-01"
- tooltipplacement="top"
- />
-
- <!---->
+ <gl-intersection-observer-stub>
+ <timeago-tooltip-stub
+ cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
+ time="2019-01-01"
+ tooltipplacement="top"
+ />
+ </gl-intersection-observer-stub>
</td>
</tr>
`;
@@ -71,10 +61,10 @@ exports[`Repository table row component renders table row 1`] = `
class="tree-item"
>
<td
- class="tree-item-file-name cursor-default position-relative"
+ class="cursor-default position-relative tree-item-file-name"
>
<a
- class="tree-item-link str-truncated"
+ class="str-truncated tree-item-link"
data-qa-selector="file_name_link"
href="https://test.com"
title="test"
@@ -92,11 +82,6 @@ exports[`Repository table row component renders table row 1`] = `
test
</span>
</a>
-
- <!---->
-
- <!---->
-
<gl-icon-stub
class="ml-1"
name="lock"
@@ -104,30 +89,25 @@ exports[`Repository table row component renders table row 1`] = `
title="Locked by Root"
/>
</td>
-
<td
- class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary"
+ class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
>
<gl-link-stub
- class="str-truncated-100 tree-commit-link gl-text-secondary"
+ class="gl-text-secondary str-truncated-100 tree-commit-link"
/>
-
- <gl-intersection-observer-stub>
- <!---->
- </gl-intersection-observer-stub>
+ <gl-intersection-observer-stub />
</td>
-
<td
- class="tree-time-ago text-right cursor-default gl-text-secondary"
+ class="cursor-default gl-text-secondary text-right tree-time-ago"
>
- <timeago-tooltip-stub
- cssclass=""
- datetimeformat="DATE_WITH_TIME_FORMAT"
- time="2019-01-01"
- tooltipplacement="top"
- />
-
- <!---->
+ <gl-intersection-observer-stub>
+ <timeago-tooltip-stub
+ cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
+ time="2019-01-01"
+ tooltipplacement="top"
+ />
+ </gl-intersection-observer-stub>
</td>
</tr>
`;
@@ -137,10 +117,10 @@ exports[`Repository table row component renders table row for path with special
class="tree-item"
>
<td
- class="tree-item-file-name cursor-default position-relative"
+ class="cursor-default position-relative tree-item-file-name"
>
<a
- class="tree-item-link str-truncated"
+ class="str-truncated tree-item-link"
data-qa-selector="file_name_link"
href="https://test.com"
title="test"
@@ -158,11 +138,6 @@ exports[`Repository table row component renders table row for path with special
test
</span>
</a>
-
- <!---->
-
- <!---->
-
<gl-icon-stub
class="ml-1"
name="lock"
@@ -170,30 +145,25 @@ exports[`Repository table row component renders table row for path with special
title="Locked by Root"
/>
</td>
-
<td
- class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary"
+ class="cursor-default d-none d-sm-table-cell gl-text-secondary tree-commit"
>
<gl-link-stub
- class="str-truncated-100 tree-commit-link gl-text-secondary"
+ class="gl-text-secondary str-truncated-100 tree-commit-link"
/>
-
- <gl-intersection-observer-stub>
- <!---->
- </gl-intersection-observer-stub>
+ <gl-intersection-observer-stub />
</td>
-
<td
- class="tree-time-ago text-right cursor-default gl-text-secondary"
+ class="cursor-default gl-text-secondary text-right tree-time-ago"
>
- <timeago-tooltip-stub
- cssclass=""
- datetimeformat="DATE_WITH_TIME_FORMAT"
- time="2019-01-01"
- tooltipplacement="top"
- />
-
- <!---->
+ <gl-intersection-observer-stub>
+ <timeago-tooltip-stub
+ cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
+ time="2019-01-01"
+ tooltipplacement="top"
+ />
+ </gl-intersection-observer-stub>
</td>
</tr>
`;
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index e20849d1085..c60b6ace598 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -73,17 +73,6 @@ export const projectMock = {
},
};
-export const userInfoMock = {
- currentUser: {
- id: '123',
- gitpodEnabled: true,
- preferencesGitpodPath: '/-/profile/preferences#user_gitpod_enabled',
- profileEnableGitpodPath: '/-/profile?user%5Bgitpod_enabled%5D=true',
- },
-};
-
-export const applicationInfoMock = { gitpodEnabled: true };
-
export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
export const refMock = 'default-ref';
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index a063f20aca6..7bddc4b1c48 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -194,7 +194,7 @@ export const MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION = {
label: 'Projects',
scope: 'projects',
link: '/search?scope=projects&search=et',
- count_link: '/search/count?scope=projects&search=et',
+ count_link: null,
},
};
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index a4559c2dc34..8e23f9c1680 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -2,14 +2,26 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import {
+ SEARCH_TYPE_ZOEKT,
+ SEARCH_TYPE_ADVANCED,
+ SEARCH_TYPE_BASIC,
+} from '~/search/sidebar/constants';
import { MOCK_QUERY } from 'jest/search/mock_data';
+import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import IssuesFilters from '~/search/sidebar/components/issues_filters.vue';
import MergeRequestsFilters from '~/search/sidebar/components/merge_requests_filters.vue';
import BlobsFilters from '~/search/sidebar/components/blobs_filters.vue';
import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue';
+import NotesFilters from '~/search/sidebar/components/notes_filters.vue';
+import CommitsFilters from '~/search/sidebar/components/commits_filters.vue';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
+import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+
+jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager');
Vue.use(Vuex);
@@ -20,7 +32,7 @@ describe('GlobalSearchSidebar', () => {
currentScope: jest.fn(() => 'issues'),
};
- const createComponent = (initialState = {}, ff = false) => {
+ const createComponent = (initialState = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
@@ -33,7 +45,8 @@ describe('GlobalSearchSidebar', () => {
store,
provide: {
glFeatures: {
- searchProjectsHideArchived: ff,
+ searchNotesHideArchivedProjects: true,
+ searchCommitsHideArchivedProjects: true,
},
},
});
@@ -44,69 +57,111 @@ describe('GlobalSearchSidebar', () => {
const findMergeRequestsFilters = () => wrapper.findComponent(MergeRequestsFilters);
const findBlobsFilters = () => wrapper.findComponent(BlobsFilters);
const findProjectsFilters = () => wrapper.findComponent(ProjectsFilters);
+ const findNotesFilters = () => wrapper.findComponent(NotesFilters);
+ const findCommitsFilters = () => wrapper.findComponent(CommitsFilters);
const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation);
+ const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation);
const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation);
+ const findDomElementListener = () => wrapper.findComponent(DomElementListener);
describe('renders properly', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
+
it(`shows section`, () => {
expect(findSidebarSection().exists()).toBe(true);
});
});
describe.each`
- scope | filter
- ${'issues'} | ${findIssuesFilters}
- ${'merge_requests'} | ${findMergeRequestsFilters}
- ${'blobs'} | ${findBlobsFilters}
- `('with sidebar $scope scope:', ({ scope, filter }) => {
+ scope | filter | searchType | isShown
+ ${'issues'} | ${findIssuesFilters} | ${SEARCH_TYPE_BASIC} | ${true}
+ ${'merge_requests'} | ${findMergeRequestsFilters} | ${SEARCH_TYPE_BASIC} | ${true}
+ ${'projects'} | ${findProjectsFilters} | ${SEARCH_TYPE_BASIC} | ${true}
+ ${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_BASIC} | ${false}
+ ${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
+ ${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_ZOEKT} | ${false}
+ ${'notes'} | ${findNotesFilters} | ${SEARCH_TYPE_BASIC} | ${false}
+ ${'notes'} | ${findNotesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
+ ${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_BASIC} | ${false}
+ ${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
+ `('with sidebar $scope scope:', ({ scope, filter, searchType, isShown }) => {
beforeEach(() => {
getterSpies.currentScope = jest.fn(() => scope);
- createComponent({ urlQuery: { scope } });
+ createComponent({ urlQuery: { scope }, searchType });
});
- it(`shows filter ${filter.name.replace('find', '')}`, () => {
- expect(filter().exists()).toBe(true);
+ it(`renders correctly filter ${filter.name.replace(
+ 'find',
+ '',
+ )} when search_type ${searchType}`, () => {
+ expect(filter().exists()).toBe(isShown);
});
});
- describe.each`
- featureFlag
- ${false}
- ${true}
- `('with sidebar $scope scope:', ({ featureFlag }) => {
+ describe('filters for blobs will not load if zoekt is enabled', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: { scope: 'blobs' }, searchType: SEARCH_TYPE_ZOEKT });
+ });
+
+ it("doesn't render blobs filters", () => {
+ expect(findBlobsFilters().exists()).toBe(false);
+ });
+ });
+
+ describe('with sidebar scope: projects', () => {
beforeEach(() => {
getterSpies.currentScope = jest.fn(() => 'projects');
- createComponent({ urlQuery: { scope: 'projects' } }, featureFlag);
+ createComponent({ urlQuery: { scope: 'projects' } });
});
- it(`shows filter ProjectsFilters}`, () => {
- expect(findProjectsFilters().exists()).toBe(featureFlag);
+ it(`shows filter ProjectsFilters`, () => {
+ expect(findProjectsFilters().exists()).toBe(true);
});
});
describe.each`
currentScope | sidebarNavShown | legacyNavShown
${'issues'} | ${false} | ${true}
- ${''} | ${false} | ${false}
+ ${'test'} | ${false} | ${true}
${'issues'} | ${true} | ${false}
- ${''} | ${true} | ${false}
- `('renders navigation', ({ currentScope, sidebarNavShown, legacyNavShown }) => {
- beforeEach(() => {
- getterSpies.currentScope = jest.fn(() => currentScope);
- createComponent({ useSidebarNavigation: sidebarNavShown });
- });
+ ${'test'} | ${true} | ${false}
+ `(
+ 'renders navigation for scope $currentScope',
+ ({ currentScope, sidebarNavShown, legacyNavShown }) => {
+ beforeEach(() => {
+ getterSpies.currentScope = jest.fn(() => currentScope);
+ createComponent({ useSidebarNavigation: sidebarNavShown });
+ });
- it(`${!legacyNavShown ? 'hides' : 'shows'} the legacy navigation`, () => {
- expect(findScopeLegacyNavigation().exists()).toBe(legacyNavShown);
- });
+ it(`renders navigation correctly with legacyNavShown ${legacyNavShown}`, () => {
+ expect(findScopeLegacyNavigation().exists()).toBe(legacyNavShown);
+ expect(findSmallScreenDrawerNavigation().exists()).toBe(legacyNavShown);
+ });
- it(`${!sidebarNavShown ? 'hides' : 'shows'} the sidebar navigation`, () => {
- expect(findScopeSidebarNavigation().exists()).toBe(sidebarNavShown);
- });
+ it(`renders navigation correctly with sidebarNavShown ${sidebarNavShown}`, () => {
+ expect(findScopeSidebarNavigation().exists()).toBe(sidebarNavShown);
+ });
+ },
+ );
+ });
+
+ describe('when useSidebarNavigation=true', () => {
+ beforeEach(() => {
+ createComponent({ useSidebarNavigation: true });
+ });
+
+ it('toggles super sidebar when button is clicked', () => {
+ const elListener = findDomElementListener();
+
+ expect(toggleSuperSidebarCollapsed).not.toHaveBeenCalled();
+
+ elListener.vm.$emit('click');
+
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1);
+ expect(elListener.props('selector')).toBe('#js-open-mobile-filters');
});
});
});
diff --git a/spec/frontend/search/sidebar/components/blobs_filters_spec.js b/spec/frontend/search/sidebar/components/blobs_filters_spec.js
index ff93e6f32e4..729fae44c19 100644
--- a/spec/frontend/search/sidebar/components/blobs_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/blobs_filters_spec.js
@@ -1,28 +1,93 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
+import { MOCK_QUERY } from 'jest/search/mock_data';
import BlobsFilters from '~/search/sidebar/components/blobs_filters.vue';
import LanguageFilter from '~/search/sidebar/components/language_filter/index.vue';
-import FiltersTemplate from '~/search/sidebar/components/filters_template.vue';
+import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
+import { SEARCH_TYPE_ADVANCED } from '~/search/sidebar/constants';
+
+Vue.use(Vuex);
describe('GlobalSearch BlobsFilters', () => {
let wrapper;
- const findLanguageFilter = () => wrapper.findComponent(LanguageFilter);
- const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate);
+ const defaultGetters = {
+ currentScope: () => 'blobs',
+ };
- const createComponent = () => {
- wrapper = shallowMount(BlobsFilters);
+ const createComponent = ({ initialState = {}, searchBlobsHideArchivedProjects = true } = {}) => {
+ const store = new Vuex.Store({
+ state: {
+ urlQuery: MOCK_QUERY,
+ useSidebarNavigation: false,
+ searchType: SEARCH_TYPE_ADVANCED,
+ ...initialState,
+ },
+ getters: defaultGetters,
+ });
+
+ wrapper = shallowMount(BlobsFilters, {
+ store,
+ provide: {
+ glFeatures: {
+ searchBlobsHideArchivedProjects,
+ },
+ },
+ });
};
- describe('Renders correctly', () => {
+ const findLanguageFilter = () => wrapper.findComponent(LanguageFilter);
+ const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
+ const findDividers = () => wrapper.findAll('hr');
+
+ describe.each`
+ description | searchBlobsHideArchivedProjects
+ ${'Renders correctly with Archived Filter enabled'} | ${true}
+ ${'Renders correctly with Archived Filter disabled'} | ${false}
+ `('$description', ({ searchBlobsHideArchivedProjects }) => {
+ beforeEach(() => {
+ createComponent({
+ searchBlobsHideArchivedProjects,
+ });
+ });
+
+ it('renders LanguageFilter', () => {
+ expect(findLanguageFilter().exists()).toBe(true);
+ });
+
+ it(`renders correctly ArchivedFilter when searchBlobsHideArchivedProjects is ${searchBlobsHideArchivedProjects}`, () => {
+ expect(findArchivedFilter().exists()).toBe(searchBlobsHideArchivedProjects);
+ });
+
+ it('renders divider correctly', () => {
+ const dividersCount = searchBlobsHideArchivedProjects ? 1 : 0;
+ expect(findDividers()).toHaveLength(dividersCount);
+ });
+ });
+
+ describe('Renders correctly in new nav', () => {
beforeEach(() => {
- createComponent();
+ createComponent({
+ initialState: {
+ searchType: SEARCH_TYPE_ADVANCED,
+ useSidebarNavigation: true,
+ },
+ searchBlobsHideArchivedProjects: true,
+ });
});
- it('renders FiltersTemplate', () => {
+
+ it('renders correctly LanguageFilter', () => {
expect(findLanguageFilter().exists()).toBe(true);
});
- it('renders ConfidentialityFilter', () => {
- expect(findFiltersTemplate().exists()).toBe(true);
+ it('renders correctly ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
+
+ it("doesn't render dividers", () => {
+ expect(findDividers()).toHaveLength(0);
});
});
});
diff --git a/spec/frontend/search/sidebar/components/commits_filters_spec.js b/spec/frontend/search/sidebar/components/commits_filters_spec.js
new file mode 100644
index 00000000000..cb47c6833ef
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/commits_filters_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import CommitsFilters from '~/search/sidebar/components/projects_filters.vue';
+import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
+import FiltersTemplate from '~/search/sidebar/components/filters_template.vue';
+
+describe('GlobalSearch CommitsFilters', () => {
+ let wrapper;
+
+ const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
+ const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate);
+
+ const createComponent = () => {
+ wrapper = shallowMount(CommitsFilters);
+ };
+
+ describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+ it('renders ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
+
+ it('renders FiltersTemplate', () => {
+ expect(findFiltersTemplate().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/issues_filters_spec.js b/spec/frontend/search/sidebar/components/issues_filters_spec.js
index 84c4258cbdb..39d10cbb8b4 100644
--- a/spec/frontend/search/sidebar/components/issues_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/issues_filters_spec.js
@@ -7,6 +7,8 @@ import IssuesFilters from '~/search/sidebar/components/issues_filters.vue';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter/index.vue';
import StatusFilter from '~/search/sidebar/components/status_filter/index.vue';
import LabelFilter from '~/search/sidebar/components/label_filter/index.vue';
+import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
+import { SEARCH_TYPE_ADVANCED, SEARCH_TYPE_BASIC } from '~/search/sidebar/constants';
Vue.use(Vuex);
@@ -17,10 +19,16 @@ describe('GlobalSearch IssuesFilters', () => {
currentScope: () => 'issues',
};
- const createComponent = (initialState, ff = true) => {
+ const createComponent = ({
+ initialState = {},
+ searchIssueLabelAggregation = true,
+ searchIssuesHideArchivedProjects = true,
+ } = {}) => {
const store = new Vuex.Store({
state: {
urlQuery: MOCK_QUERY,
+ useSidebarNavigation: false,
+ searchType: SEARCH_TYPE_ADVANCED,
...initialState,
},
getters: defaultGetters,
@@ -30,7 +38,8 @@ describe('GlobalSearch IssuesFilters', () => {
store,
provide: {
glFeatures: {
- searchIssueLabelAggregation: ff,
+ searchIssueLabelAggregation,
+ searchIssuesHideArchivedProjects,
},
},
});
@@ -39,12 +48,23 @@ describe('GlobalSearch IssuesFilters', () => {
const findStatusFilter = () => wrapper.findComponent(StatusFilter);
const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
const findLabelFilter = () => wrapper.findComponent(LabelFilter);
+ const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
const findDividers = () => wrapper.findAll('hr');
- describe('Renders correctly with FF enabled', () => {
+ describe.each`
+ description | searchIssueLabelAggregation | searchIssuesHideArchivedProjects
+ ${'Renders correctly with Label Filter disabled'} | ${false} | ${true}
+ ${'Renders correctly with Archived Filter disabled'} | ${true} | ${false}
+ ${'Renders correctly with Archived Filter and Label Filter disabled'} | ${false} | ${false}
+ ${'Renders correctly with Archived Filter and Label Filter enabled'} | ${true} | ${true}
+ `('$description', ({ searchIssueLabelAggregation, searchIssuesHideArchivedProjects }) => {
beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY });
+ createComponent({
+ searchIssueLabelAggregation,
+ searchIssuesHideArchivedProjects,
+ });
});
+
it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
});
@@ -53,18 +73,30 @@ describe('GlobalSearch IssuesFilters', () => {
expect(findConfidentialityFilter().exists()).toBe(true);
});
- it('renders LabelFilter', () => {
- expect(findLabelFilter().exists()).toBe(true);
+ it(`renders correctly LabelFilter when searchIssueLabelAggregation is ${searchIssueLabelAggregation}`, () => {
+ expect(findLabelFilter().exists()).toBe(searchIssueLabelAggregation);
});
- it('renders dividers correctly', () => {
- expect(findDividers()).toHaveLength(2);
+ it(`renders correctly ArchivedFilter when searchIssuesHideArchivedProjects is ${searchIssuesHideArchivedProjects}`, () => {
+ expect(findArchivedFilter().exists()).toBe(searchIssuesHideArchivedProjects);
+ });
+
+ it('renders divider correctly', () => {
+ // one divider can't be disabled
+ let dividersCount = 1;
+ if (searchIssueLabelAggregation) {
+ dividersCount += 1;
+ }
+ if (searchIssuesHideArchivedProjects) {
+ dividersCount += 1;
+ }
+ expect(findDividers()).toHaveLength(dividersCount);
});
});
- describe('Renders correctly with FF disabled', () => {
+ describe('Renders correctly with basic search', () => {
beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY }, false);
+ createComponent({ initialState: { searchType: SEARCH_TYPE_BASIC } });
});
it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
@@ -78,15 +110,51 @@ describe('GlobalSearch IssuesFilters', () => {
expect(findLabelFilter().exists()).toBe(false);
});
- it('renders divider correctly', () => {
+ it("doesn't render ArchivedFilter", () => {
+ expect(findArchivedFilter().exists()).toBe(false);
+ });
+
+ it('renders 1 divider', () => {
expect(findDividers()).toHaveLength(1);
});
});
+ describe('Renders correctly in new nav', () => {
+ beforeEach(() => {
+ createComponent({
+ initialState: {
+ searchType: SEARCH_TYPE_ADVANCED,
+ useSidebarNavigation: true,
+ },
+ searchIssueLabelAggregation: true,
+ searchIssuesHideArchivedProjects: true,
+ });
+ });
+ it('renders StatusFilter', () => {
+ expect(findStatusFilter().exists()).toBe(true);
+ });
+
+ it('renders ConfidentialityFilter', () => {
+ expect(findConfidentialityFilter().exists()).toBe(true);
+ });
+
+ it('renders LabelFilter', () => {
+ expect(findLabelFilter().exists()).toBe(true);
+ });
+
+ it('renders ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
+
+ it("doesn't render dividers", () => {
+ expect(findDividers()).toHaveLength(0);
+ });
+ });
+
describe('Renders correctly with wrong scope', () => {
beforeEach(() => {
- defaultGetters.currentScope = () => 'blobs';
- createComponent({ urlQuery: MOCK_QUERY });
+ defaultGetters.currentScope = () => 'test';
+ createComponent();
});
it("doesn't render StatusFilter", () => {
expect(findStatusFilter().exists()).toBe(false);
@@ -100,6 +168,10 @@ describe('GlobalSearch IssuesFilters', () => {
expect(findLabelFilter().exists()).toBe(false);
});
+ it("doesn't render ArchivedFilter", () => {
+ expect(findArchivedFilter().exists()).toBe(false);
+ });
+
it("doesn't render dividers", () => {
expect(findDividers()).toHaveLength(0);
});
diff --git a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
index 0932f8e47d2..b50f348be69 100644
--- a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
@@ -1,28 +1,131 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+// eslint-disable-next-line no-restricted-imports
+import Vuex from 'vuex';
+import { MOCK_QUERY } from 'jest/search/mock_data';
import MergeRequestsFilters from '~/search/sidebar/components/merge_requests_filters.vue';
import StatusFilter from '~/search/sidebar/components/status_filter/index.vue';
-import FiltersTemplate from '~/search/sidebar/components/filters_template.vue';
+import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
+import { SEARCH_TYPE_ADVANCED, SEARCH_TYPE_BASIC } from '~/search/sidebar/constants';
+
+Vue.use(Vuex);
describe('GlobalSearch MergeRequestsFilters', () => {
let wrapper;
- const findStatusFilter = () => wrapper.findComponent(StatusFilter);
- const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate);
+ const defaultGetters = {
+ currentScope: () => 'merge_requests',
+ };
- const createComponent = () => {
- wrapper = shallowMount(MergeRequestsFilters);
+ const createComponent = ({
+ initialState = {},
+ searchMergeRequestsHideArchivedProjects = true,
+ } = {}) => {
+ const store = new Vuex.Store({
+ state: {
+ urlQuery: MOCK_QUERY,
+ useSidebarNavigation: false,
+ searchType: SEARCH_TYPE_ADVANCED,
+ ...initialState,
+ },
+ getters: defaultGetters,
+ });
+
+ wrapper = shallowMount(MergeRequestsFilters, {
+ store,
+ provide: {
+ glFeatures: {
+ searchMergeRequestsHideArchivedProjects,
+ },
+ },
+ });
};
- describe('Renders correctly', () => {
+ const findStatusFilter = () => wrapper.findComponent(StatusFilter);
+ const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
+ const findDividers = () => wrapper.findAll('hr');
+
+ describe.each`
+ description | searchMergeRequestsHideArchivedProjects
+ ${'Renders correctly with Archived Filter disabled'} | ${false}
+ ${'Renders correctly with Archived Filter enabled'} | ${true}
+ `('$description', ({ searchMergeRequestsHideArchivedProjects }) => {
beforeEach(() => {
- createComponent();
+ createComponent({
+ searchMergeRequestsHideArchivedProjects,
+ });
+ });
+
+ it('renders StatusFilter', () => {
+ expect(findStatusFilter().exists()).toBe(true);
+ });
+
+ it(`renders correctly ArchivedFilter when searchMergeRequestsHideArchivedProjects is ${searchMergeRequestsHideArchivedProjects}`, () => {
+ expect(findArchivedFilter().exists()).toBe(searchMergeRequestsHideArchivedProjects);
});
- it('renders ConfidentialityFilter', () => {
+
+ it('renders divider correctly', () => {
+ const dividersCount = searchMergeRequestsHideArchivedProjects ? 1 : 0;
+ expect(findDividers()).toHaveLength(dividersCount);
+ });
+ });
+
+ describe('Renders correctly with basic search', () => {
+ beforeEach(() => {
+ createComponent({ initialState: { searchType: SEARCH_TYPE_BASIC } });
+ });
+
+ it('renders StatusFilter', () => {
expect(findStatusFilter().exists()).toBe(true);
});
- it('renders FiltersTemplate', () => {
- expect(findFiltersTemplate().exists()).toBe(true);
+ it("doesn't render ArchivedFilter", () => {
+ expect(findArchivedFilter().exists()).toBe(false);
+ });
+
+ it('renders 1 divider', () => {
+ expect(findDividers()).toHaveLength(0);
+ });
+ });
+
+ describe('Renders correctly in new nav', () => {
+ beforeEach(() => {
+ createComponent({
+ initialState: {
+ searchType: SEARCH_TYPE_ADVANCED,
+ useSidebarNavigation: true,
+ },
+ searchMergeRequestsHideArchivedProjects: true,
+ });
+ });
+ it('renders StatusFilter', () => {
+ expect(findStatusFilter().exists()).toBe(true);
+ });
+
+ it('renders ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
+
+ it("doesn't render divider", () => {
+ expect(findDividers()).toHaveLength(0);
+ });
+ });
+
+ describe('Renders correctly with wrong scope', () => {
+ beforeEach(() => {
+ defaultGetters.currentScope = () => 'test';
+ createComponent();
+ });
+ it("doesn't render StatusFilter", () => {
+ expect(findStatusFilter().exists()).toBe(false);
+ });
+
+ it("doesn't render ArchivedFilter", () => {
+ expect(findArchivedFilter().exists()).toBe(false);
+ });
+
+ it("doesn't render dividers", () => {
+ expect(findDividers()).toHaveLength(0);
});
});
});
diff --git a/spec/frontend/search/sidebar/components/notes_filters_spec.js b/spec/frontend/search/sidebar/components/notes_filters_spec.js
new file mode 100644
index 00000000000..2fb8e731ef5
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/notes_filters_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import NotesFilters from '~/search/sidebar/components/projects_filters.vue';
+import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
+import FiltersTemplate from '~/search/sidebar/components/filters_template.vue';
+
+describe('GlobalSearch ProjectsFilters', () => {
+ let wrapper;
+
+ const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
+ const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate);
+
+ const createComponent = () => {
+ wrapper = shallowMount(NotesFilters);
+ };
+
+ describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+ it('renders ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
+
+ it('renders FiltersTemplate', () => {
+ expect(findFiltersTemplate().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/projects_filters_specs.js b/spec/frontend/search/sidebar/components/projects_filters_spec.js
index 15e3254e289..930b7263ea4 100644
--- a/spec/frontend/search/sidebar/components/projects_filters_specs.js
+++ b/spec/frontend/search/sidebar/components/projects_filters_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue';
-import ArchivedFilter from '~/search/sidebar/components/language_filter/index.vue';
+import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
import FiltersTemplate from '~/search/sidebar/components/filters_template.vue';
describe('GlobalSearch ProjectsFilters', () => {
diff --git a/spec/frontend/search/sidebar/components/small_screen_drawer_navigation_spec.js b/spec/frontend/search/sidebar/components/small_screen_drawer_navigation_spec.js
new file mode 100644
index 00000000000..5ab4afba7f0
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/small_screen_drawer_navigation_spec.js
@@ -0,0 +1,68 @@
+import { nextTick } from 'vue';
+import { GlDrawer } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
+import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
+
+describe('ScopeLegacyNavigation', () => {
+ let wrapper;
+ let closeSpy;
+ let toggleSpy;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(SmallScreenDrawerNavigation, {
+ slots: {
+ default: '<div data-testid="default-slot-content">test</div>',
+ },
+ });
+ };
+
+ const findGlDrawer = () => wrapper.findComponent(GlDrawer);
+ const findTitle = () => wrapper.findComponent('h2');
+ const findSlot = () => wrapper.findByTestId('default-slot-content');
+ const findDomElementListener = () => wrapper.findComponent(DomElementListener);
+
+ describe('small screen navigation', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders drawer', () => {
+ expect(findGlDrawer().exists()).toBe(true);
+ expect(findGlDrawer().attributes('zindex')).toBe(DRAWER_Z_INDEX.toString());
+ expect(findGlDrawer().attributes('headerheight')).toBe('0');
+ });
+
+ it('renders title', () => {
+ expect(findTitle().exists()).toBe(true);
+ });
+
+ it('renders slots', () => {
+ expect(findSlot().exists()).toBe(true);
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ closeSpy = jest.spyOn(SmallScreenDrawerNavigation.methods, 'closeSmallScreenFilters');
+ toggleSpy = jest.spyOn(SmallScreenDrawerNavigation.methods, 'toggleSmallScreenFilters');
+ createComponent();
+ });
+
+ it('calls onClose', () => {
+ findGlDrawer().vm.$emit('close');
+ expect(closeSpy).toHaveBeenCalled();
+ });
+
+ it('calls toggleSmallScreenFilters', async () => {
+ expect(findGlDrawer().props('open')).toBe(false);
+
+ findDomElementListener().vm.$emit('click');
+ await nextTick();
+
+ expect(toggleSpy).toHaveBeenCalled();
+ expect(findGlDrawer().props('open')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index cc9c555b6c7..889260fc478 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { mapValues } from 'lodash';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { createAlert } from '~/alert';
@@ -312,6 +313,21 @@ describe('Global Search Store Actions', () => {
});
});
+ describe('fetchSidebarCount with no count_link', () => {
+ beforeEach(() => {
+ state.navigation = mapValues(MOCK_NAVIGATION_DATA, (navItem) => ({
+ ...navItem,
+ count_link: null,
+ }));
+ });
+
+ it('should not request anything', async () => {
+ await testAction({ action: actions.fetchSidebarCount, state, expectedMutations: [] });
+
+ expect(mock.history.get.length).toBe(0);
+ });
+ });
+
describe.each`
action | axiosMock | type | expectedMutations | errorLogs
${actions.fetchAllAggregation} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION} | ${0}
diff --git a/spec/frontend/security_configuration/components/continuous_vulnerability_scan_spec.js b/spec/frontend/security_configuration/components/continuous_vulnerability_scan_spec.js
new file mode 100644
index 00000000000..84a468e4dd8
--- /dev/null
+++ b/spec/frontend/security_configuration/components/continuous_vulnerability_scan_spec.js
@@ -0,0 +1,124 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlBadge, GlToggle } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import ProjectSetContinuousVulnerabilityScanning from '~/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql';
+import ContinuousVulnerabilityScan from '~/security_configuration/components/continuous_vulnerability_scan.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+
+Vue.use(VueApollo);
+
+const setCVSMockResponse = {
+ data: {
+ projectSetContinuousVulnerabilityScanning: {
+ continuousVulnerabilityScanningEnabled: true,
+ errors: [],
+ },
+ },
+};
+
+const defaultProvide = {
+ continuousVulnerabilityScansEnabled: true,
+ projectFullPath: 'project/full/path',
+};
+
+describe('ContinuousVulnerabilityScan', () => {
+ let wrapper;
+ let apolloProvider;
+ let requestHandlers;
+
+ const createComponent = (options) => {
+ requestHandlers = {
+ setCVSMutationHandler: jest.fn().mockResolvedValue(setCVSMockResponse),
+ };
+
+ apolloProvider = createMockApollo([
+ [ProjectSetContinuousVulnerabilityScanning, requestHandlers.setCVSMutationHandler],
+ ]);
+
+ wrapper = shallowMount(ContinuousVulnerabilityScan, {
+ propsData: {
+ feature: {
+ available: true,
+ configured: true,
+ },
+ },
+ provide: {
+ glFeatures: {
+ dependencyScanningOnAdvisoryIngestion: true,
+ },
+ ...defaultProvide,
+ },
+ apolloProvider,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ apolloProvider = null;
+ });
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findToggle = () => wrapper.findComponent(GlToggle);
+
+ it('renders the component', () => {
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('renders the correct title', () => {
+ expect(wrapper.text()).toContain('Continuous Vulnerability Scan');
+ });
+
+ it('renders the badge and toggle component with correct values', () => {
+ expect(findBadge().exists()).toBe(true);
+ expect(findBadge().text()).toBe('Experiment');
+
+ expect(findToggle().exists()).toBe(true);
+ expect(findToggle().props('value')).toBe(defaultProvide.continuousVulnerabilityScansEnabled);
+ });
+
+ it('should disable toggle when feature is not configured', () => {
+ createComponent({
+ propsData: {
+ feature: {
+ available: true,
+ configured: false,
+ },
+ },
+ });
+ expect(findToggle().props('disabled')).toBe(true);
+ });
+
+ it('calls mutation on toggle change with correct payload', () => {
+ findToggle().vm.$emit('change', true);
+
+ expect(requestHandlers.setCVSMutationHandler).toHaveBeenCalledWith({
+ input: {
+ projectPath: 'project/full/path',
+ enable: true,
+ },
+ });
+ });
+
+ describe('when feature flag is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: {
+ dependencyScanningOnAdvisoryIngestion: false,
+ },
+ ...defaultProvide,
+ },
+ });
+ });
+
+ it('should not render toggle and badge', () => {
+ expect(findToggle().exists()).toBe(false);
+ expect(findBadge().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index 983a66a7fd3..c715d01dd58 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -1,5 +1,6 @@
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import Vue from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { securityFeatures } from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
@@ -13,6 +14,10 @@ import {
import { manageViaMRErrorMessage } from '../constants';
import { makeFeature } from './utils';
+const MockComponent = Vue.component('MockComponent', {
+ render: (createElement) => createElement('span'),
+});
+
describe('FeatureCard component', () => {
let feature;
let wrapper;
@@ -389,4 +394,17 @@ describe('FeatureCard component', () => {
});
});
});
+
+ describe('when a slot component is passed', () => {
+ beforeEach(() => {
+ feature = makeFeature({
+ slotComponent: MockComponent,
+ });
+ createComponent({ feature });
+ });
+
+ it('renders the component properly', () => {
+ expect(wrapper.findComponent(MockComponent).exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/security_configuration/utils_spec.js b/spec/frontend/security_configuration/utils_spec.js
index 6e731e45da2..ea04e9e7993 100644
--- a/spec/frontend/security_configuration/utils_spec.js
+++ b/spec/frontend/security_configuration/utils_spec.js
@@ -34,6 +34,33 @@ describe('augmentFeatures', () => {
},
];
+ const mockSecurityFeaturesDast = [
+ {
+ name: 'DAST',
+ type: 'dast',
+ },
+ ];
+
+ const mockValidCustomFeatureWithOnDemandAvailableFalse = [
+ {
+ name: 'DAST',
+ type: 'dast',
+ customField: 'customvalue',
+ onDemandAvailable: false,
+ badge: {},
+ },
+ ];
+
+ const mockValidCustomFeatureWithOnDemandAvailableTrue = [
+ {
+ name: 'DAST',
+ type: 'dast',
+ customField: 'customvalue',
+ onDemandAvailable: true,
+ badge: {},
+ },
+ ];
+
const mockValidCustomFeatureSnakeCase = [
{
name: 'SAST',
@@ -54,6 +81,29 @@ describe('augmentFeatures', () => {
augmentedSecurityFeatures: mockValidCustomFeature,
};
+ const expectedOutputCustomFeatureWithOnDemandAvailableFalse = {
+ augmentedSecurityFeatures: [
+ {
+ name: 'DAST',
+ type: 'dast',
+ customField: 'customvalue',
+ onDemandAvailable: false,
+ },
+ ],
+ };
+
+ const expectedOutputCustomFeatureWithOnDemandAvailableTrue = {
+ augmentedSecurityFeatures: [
+ {
+ name: 'DAST',
+ type: 'dast',
+ customField: 'customvalue',
+ onDemandAvailable: true,
+ badge: {},
+ },
+ ],
+ };
+
describe('returns an object with augmentedSecurityFeatures when', () => {
it('given an empty array', () => {
expect(augmentFeatures(mockSecurityFeatures, [])).toEqual(expectedOutputDefault);
@@ -85,6 +135,20 @@ describe('augmentFeatures', () => {
);
});
});
+
+ describe('follows onDemandAvailable', () => {
+ it('deletes badge when false', () => {
+ expect(
+ augmentFeatures(mockSecurityFeaturesDast, mockValidCustomFeatureWithOnDemandAvailableFalse),
+ ).toEqual(expectedOutputCustomFeatureWithOnDemandAvailableFalse);
+ });
+
+ it('keeps badge when true', () => {
+ expect(
+ augmentFeatures(mockSecurityFeaturesDast, mockValidCustomFeatureWithOnDemandAvailableTrue),
+ ).toEqual(expectedOutputCustomFeatureWithOnDemandAvailableTrue);
+ });
+ });
});
describe('translateScannerNames', () => {
diff --git a/spec/frontend/sentry/index_spec.js b/spec/frontend/sentry/index_spec.js
deleted file mode 100644
index 3130e01cc9e..00000000000
--- a/spec/frontend/sentry/index_spec.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import index from '~/sentry/index';
-
-import LegacySentryConfig from '~/sentry/legacy_sentry_config';
-import SentryConfig from '~/sentry/sentry_config';
-
-describe('Sentry init', () => {
- const version = '1.0.0';
- const dsn = 'https://123@sentry.gitlab.test/123';
- const environment = 'test';
- const currentUserId = '1';
- const gitlabUrl = 'gitlabUrl';
- const revision = 'revision';
- const featureCategory = 'my_feature_category';
-
- beforeEach(() => {
- window.gon = {
- version,
- sentry_dsn: dsn,
- sentry_environment: environment,
- current_user_id: currentUserId,
- gitlab_url: gitlabUrl,
- revision,
- feature_category: featureCategory,
- };
-
- jest.spyOn(LegacySentryConfig, 'init').mockImplementation();
- jest.spyOn(SentryConfig, 'init').mockImplementation();
- });
-
- it('exports new version of Sentry in the global object', () => {
- // eslint-disable-next-line no-underscore-dangle
- expect(window._Sentry.SDK_VERSION).not.toMatch(/^5\./);
- });
-
- describe('when called', () => {
- beforeEach(() => {
- index();
- });
-
- it('configures sentry', () => {
- expect(SentryConfig.init).toHaveBeenCalledTimes(1);
- expect(SentryConfig.init).toHaveBeenCalledWith({
- dsn,
- currentUserId,
- allowUrls: [gitlabUrl, 'webpack-internal://'],
- environment,
- release: version,
- tags: {
- revision,
- feature_category: featureCategory,
- },
- });
- });
-
- it('does not configure legacy sentry', () => {
- expect(LegacySentryConfig.init).not.toHaveBeenCalled();
- });
- });
-
- describe('with "data-page" attr in body', () => {
- const mockPage = 'projects:show';
-
- beforeEach(() => {
- document.body.dataset.page = mockPage;
-
- index();
- });
-
- afterEach(() => {
- delete document.body.dataset.page;
- });
-
- it('configures sentry with a "page" tag', () => {
- expect(SentryConfig.init).toHaveBeenCalledTimes(1);
- expect(SentryConfig.init).toHaveBeenCalledWith(
- expect.objectContaining({
- tags: {
- revision,
- page: mockPage,
- feature_category: featureCategory,
- },
- }),
- );
- });
- });
-
- describe('with no tags configuration', () => {
- beforeEach(() => {
- window.gon.revision = undefined;
- window.gon.feature_category = undefined;
-
- index();
- });
-
- it('configures sentry with no tags', () => {
- expect(SentryConfig.init).toHaveBeenCalledTimes(1);
- expect(SentryConfig.init).toHaveBeenCalledWith(
- expect.objectContaining({
- tags: {},
- }),
- );
- });
- });
-});
diff --git a/spec/frontend/sentry/init_sentry_spec.js b/spec/frontend/sentry/init_sentry_spec.js
new file mode 100644
index 00000000000..e31068b935b
--- /dev/null
+++ b/spec/frontend/sentry/init_sentry_spec.js
@@ -0,0 +1,177 @@
+import {
+ BrowserClient,
+ defaultStackParser,
+ makeFetchTransport,
+ defaultIntegrations,
+
+ // exports
+ captureException,
+ captureMessage,
+ withScope,
+ SDK_VERSION,
+} from 'sentrybrowser';
+import * as Sentry from 'sentrybrowser';
+
+import { initSentry } from '~/sentry/init_sentry';
+
+const mockDsn = 'https://123@sentry.gitlab.test/123';
+const mockEnvironment = 'development';
+const mockCurrentUserId = 1;
+const mockGitlabUrl = 'https://gitlab.com';
+const mockVersion = '1.0.0';
+const mockRevision = '00112233';
+const mockFeatureCategory = 'my_feature_category';
+const mockPage = 'index:page';
+const mockSentryClientsideTracesSampleRate = 0.1;
+
+jest.mock('sentrybrowser', () => {
+ return {
+ ...jest.createMockFromModule('sentrybrowser'),
+
+ // unmock actual configuration options
+ defaultStackParser: jest.requireActual('sentrybrowser').defaultStackParser,
+ makeFetchTransport: jest.requireActual('sentrybrowser').makeFetchTransport,
+ defaultIntegrations: jest.requireActual('sentrybrowser').defaultIntegrations,
+ };
+});
+
+describe('SentryConfig', () => {
+ let mockBindClient;
+ let mockSetTags;
+ let mockSetUser;
+ let mockBrowserClient;
+ let mockStartSession;
+ let mockCaptureSession;
+
+ beforeEach(() => {
+ window.gon = {
+ sentry_dsn: mockDsn,
+ sentry_environment: mockEnvironment,
+ current_user_id: mockCurrentUserId,
+ gitlab_url: mockGitlabUrl,
+ version: mockVersion,
+ revision: mockRevision,
+ feature_category: mockFeatureCategory,
+ sentry_clientside_traces_sample_rate: mockSentryClientsideTracesSampleRate,
+ };
+
+ document.body.dataset.page = mockPage;
+
+ mockBindClient = jest.fn();
+ mockSetTags = jest.fn();
+ mockSetUser = jest.fn();
+ mockStartSession = jest.fn();
+ mockCaptureSession = jest.fn();
+ mockBrowserClient = jest.spyOn(Sentry, 'BrowserClient');
+
+ jest.spyOn(Sentry, 'getCurrentHub').mockReturnValue({
+ bindClient: mockBindClient,
+ setTags: mockSetTags,
+ setUser: mockSetUser,
+ startSession: mockStartSession,
+ captureSession: mockCaptureSession,
+ });
+ });
+
+ afterEach(() => {
+ // eslint-disable-next-line no-underscore-dangle
+ window._Sentry = undefined;
+ });
+
+ describe('initSentry', () => {
+ describe('when sentry is initialized', () => {
+ beforeEach(() => {
+ initSentry();
+ });
+
+ it('creates BrowserClient with gon values and configuration', () => {
+ expect(mockBrowserClient).toHaveBeenCalledWith(
+ expect.objectContaining({
+ dsn: mockDsn,
+ release: mockVersion,
+ allowUrls: [mockGitlabUrl, 'webpack-internal://'],
+ environment: mockEnvironment,
+ tracesSampleRate: mockSentryClientsideTracesSampleRate,
+ tracePropagationTargets: [/^\//],
+
+ transport: makeFetchTransport,
+ stackParser: defaultStackParser,
+ integrations: defaultIntegrations,
+ }),
+ );
+ });
+
+ it('binds the BrowserClient to the hub', () => {
+ expect(mockBindClient).toHaveBeenCalledTimes(1);
+ expect(mockBindClient).toHaveBeenCalledWith(expect.any(BrowserClient));
+ });
+
+ it('calls Sentry.setTags with gon values', () => {
+ expect(mockSetTags).toHaveBeenCalledTimes(1);
+ expect(mockSetTags).toHaveBeenCalledWith({
+ page: mockPage,
+ revision: mockRevision,
+ feature_category: mockFeatureCategory,
+ });
+ });
+
+ it('calls Sentry.setUser with gon values', () => {
+ expect(mockSetUser).toHaveBeenCalledTimes(1);
+ expect(mockSetUser).toHaveBeenCalledWith({
+ id: mockCurrentUserId,
+ });
+ });
+
+ it('sets global sentry', () => {
+ // eslint-disable-next-line no-underscore-dangle
+ expect(window._Sentry).toEqual({
+ captureException,
+ captureMessage,
+ withScope,
+ SDK_VERSION,
+ });
+ });
+ });
+
+ describe('when user is not logged in', () => {
+ beforeEach(() => {
+ window.gon.current_user_id = undefined;
+ initSentry();
+ });
+
+ it('does not call Sentry.setUser', () => {
+ expect(mockSetUser).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when gon is not defined', () => {
+ beforeEach(() => {
+ window.gon = undefined;
+ initSentry();
+ });
+
+ it('Sentry.init is not called', () => {
+ expect(mockBrowserClient).not.toHaveBeenCalled();
+ expect(mockBindClient).not.toHaveBeenCalled();
+
+ // eslint-disable-next-line no-underscore-dangle
+ expect(window._Sentry).toBe(undefined);
+ });
+ });
+
+ describe('when dsn is not configured', () => {
+ beforeEach(() => {
+ window.gon.sentry_dsn = undefined;
+ initSentry();
+ });
+
+ it('Sentry.init is not called', () => {
+ expect(mockBrowserClient).not.toHaveBeenCalled();
+ expect(mockBindClient).not.toHaveBeenCalled();
+
+ // eslint-disable-next-line no-underscore-dangle
+ expect(window._Sentry).toBe(undefined);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sentry/legacy_index_spec.js b/spec/frontend/sentry/legacy_index_spec.js
index 493b4dfde67..fad1760ffc5 100644
--- a/spec/frontend/sentry/legacy_index_spec.js
+++ b/spec/frontend/sentry/legacy_index_spec.js
@@ -1,7 +1,6 @@
import index from '~/sentry/legacy_index';
import LegacySentryConfig from '~/sentry/legacy_sentry_config';
-import SentryConfig from '~/sentry/sentry_config';
describe('Sentry init', () => {
const dsn = 'https://123@sentry.gitlab.test/123';
@@ -22,7 +21,6 @@ describe('Sentry init', () => {
};
jest.spyOn(LegacySentryConfig, 'init').mockImplementation();
- jest.spyOn(SentryConfig, 'init').mockImplementation();
});
it('exports legacy version of Sentry in the global object', () => {
@@ -49,9 +47,5 @@ describe('Sentry init', () => {
},
});
});
-
- it('does not configure new sentry', () => {
- expect(SentryConfig.init).not.toHaveBeenCalled();
- });
});
});
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
deleted file mode 100644
index 34c5221ef0d..00000000000
--- a/spec/frontend/sentry/sentry_config_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import * as Sentry from 'sentrybrowser7';
-
-import SentryConfig from '~/sentry/sentry_config';
-
-describe('SentryConfig', () => {
- describe('init', () => {
- const options = {
- currentUserId: 1,
- };
-
- beforeEach(() => {
- jest.spyOn(SentryConfig, 'configure');
- jest.spyOn(SentryConfig, 'setUser');
-
- SentryConfig.init(options);
- });
-
- it('should set the options property', () => {
- expect(SentryConfig.options).toEqual(options);
- });
-
- it('should call the configure method', () => {
- expect(SentryConfig.configure).toHaveBeenCalled();
- });
-
- it('should call setUser', () => {
- expect(SentryConfig.setUser).toHaveBeenCalled();
- });
-
- it('should not call setUser if there is no current user ID', () => {
- SentryConfig.setUser.mockClear();
- SentryConfig.init({ currentUserId: undefined });
-
- expect(SentryConfig.setUser).not.toHaveBeenCalled();
- });
- });
-
- describe('configure', () => {
- const sentryConfig = {};
- const options = {
- dsn: 'https://123@sentry.gitlab.test/123',
- allowUrls: ['//gitlabUrl', 'webpack-internal://'],
- environment: 'test',
- release: 'revision',
- tags: {
- revision: 'revision',
- feature_category: 'my_feature_category',
- },
- };
-
- beforeEach(() => {
- jest.spyOn(Sentry, 'init').mockImplementation();
- jest.spyOn(Sentry, 'setTags').mockImplementation();
-
- sentryConfig.options = options;
-
- SentryConfig.configure.call(sentryConfig);
- });
-
- it('should call Sentry.init', () => {
- expect(Sentry.init).toHaveBeenCalledWith({
- dsn: options.dsn,
- release: options.release,
- allowUrls: options.allowUrls,
- environment: options.environment,
- });
- });
-
- it('should call Sentry.setTags', () => {
- expect(Sentry.setTags).toHaveBeenCalledWith(options.tags);
- });
-
- it('should set environment from options', () => {
- sentryConfig.options.environment = 'development';
-
- SentryConfig.configure.call(sentryConfig);
-
- expect(Sentry.init).toHaveBeenCalledWith({
- dsn: options.dsn,
- release: options.release,
- allowUrls: options.allowUrls,
- environment: 'development',
- });
- });
- });
-
- describe('setUser', () => {
- let sentryConfig;
-
- beforeEach(() => {
- sentryConfig = { options: { currentUserId: 1 } };
- jest.spyOn(Sentry, 'setUser');
-
- SentryConfig.setUser.call(sentryConfig);
- });
-
- it('should call .setUser', () => {
- expect(Sentry.setUser).toHaveBeenCalledWith({
- id: sentryConfig.options.currentUserId,
- });
- });
- });
-});
diff --git a/spec/frontend/service_desk/components/service_desk_list_app_spec.js b/spec/frontend/service_desk/components/service_desk_list_app_spec.js
deleted file mode 100644
index bdb6a48895e..00000000000
--- a/spec/frontend/service_desk/components/service_desk_list_app_spec.js
+++ /dev/null
@@ -1,376 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { cloneDeep } from 'lodash';
-import VueRouter from 'vue-router';
-import * as Sentry from '@sentry/browser';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import setWindowLocation from 'helpers/set_window_location_helper';
-import { TEST_HOST } from 'helpers/test_constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
-import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
-import { TYPENAME_USER } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { STATUS_CLOSED, STATUS_OPEN, STATUS_ALL } from '~/service_desk/constants';
-import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues.query.graphql';
-import getServiceDeskIssuesCountsQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql';
-import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue';
-import InfoBanner from '~/service_desk/components/info_banner.vue';
-import EmptyStateWithAnyIssues from '~/service_desk/components/empty_state_with_any_issues.vue';
-import EmptyStateWithoutAnyIssues from '~/service_desk/components/empty_state_without_any_issues.vue';
-
-import {
- TOKEN_TYPE_ASSIGNEE,
- TOKEN_TYPE_AUTHOR,
- TOKEN_TYPE_CONFIDENTIAL,
- TOKEN_TYPE_LABEL,
- TOKEN_TYPE_MILESTONE,
- TOKEN_TYPE_MY_REACTION,
- TOKEN_TYPE_RELEASE,
- TOKEN_TYPE_SEARCH_WITHIN,
-} from '~/vue_shared/components/filtered_search_bar/constants';
-import {
- getServiceDeskIssuesQueryResponse,
- getServiceDeskIssuesQueryEmptyResponse,
- getServiceDeskIssuesCountsQueryResponse,
- filteredTokens,
- urlParams,
- locationSearch,
-} from '../mock_data';
-
-jest.mock('@sentry/browser');
-
-describe('CE ServiceDeskListApp', () => {
- let wrapper;
- let router;
-
- Vue.use(VueApollo);
- Vue.use(VueRouter);
-
- const defaultProvide = {
- releasesPath: 'releases/path',
- autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
- hasIterationsFeature: true,
- hasIssueWeightsFeature: true,
- hasIssuableHealthStatusFeature: true,
- groupPath: 'group/path',
- emptyStateSvgPath: 'empty-state.svg',
- isProject: true,
- isSignedIn: true,
- fullPath: 'path/to/project',
- isServiceDeskSupported: true,
- hasAnyIssues: true,
- initialSort: '',
- issuablesLoading: false,
- };
-
- let defaultQueryResponse = getServiceDeskIssuesQueryResponse;
- if (IS_EE) {
- defaultQueryResponse = cloneDeep(getServiceDeskIssuesQueryResponse);
- defaultQueryResponse.data.project.issues.nodes[0].healthStatus = null;
- defaultQueryResponse.data.project.issues.nodes[0].weight = 5;
- }
-
- const mockServiceDeskIssuesQueryResponseHandler = jest
- .fn()
- .mockResolvedValue(defaultQueryResponse);
- const mockServiceDeskIssuesQueryEmptyResponseHandler = jest
- .fn()
- .mockResolvedValue(getServiceDeskIssuesQueryEmptyResponse);
- const mockServiceDeskIssuesCountsQueryResponseHandler = jest
- .fn()
- .mockResolvedValue(getServiceDeskIssuesCountsQueryResponse);
-
- const findIssuableList = () => wrapper.findComponent(IssuableList);
- const findInfoBanner = () => wrapper.findComponent(InfoBanner);
- const findLabelsToken = () =>
- findIssuableList()
- .props('searchTokens')
- .find((token) => token.type === TOKEN_TYPE_LABEL);
-
- const createComponent = ({
- provide = {},
- serviceDeskIssuesQueryResponseHandler = mockServiceDeskIssuesQueryResponseHandler,
- serviceDeskIssuesCountsQueryResponseHandler = mockServiceDeskIssuesCountsQueryResponseHandler,
- } = {}) => {
- const requestHandlers = [
- [getServiceDeskIssuesQuery, serviceDeskIssuesQueryResponseHandler],
- [getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponseHandler],
- ];
-
- router = new VueRouter({ mode: 'history' });
-
- return shallowMount(ServiceDeskListApp, {
- apolloProvider: createMockApollo(
- requestHandlers,
- {},
- {
- typePolicies: {
- Query: {
- fields: {
- project: {
- merge: true,
- },
- },
- },
- },
- },
- ),
- router,
- provide: {
- ...defaultProvide,
- ...provide,
- },
- });
- };
-
- beforeEach(() => {
- setWindowLocation(TEST_HOST);
- wrapper = createComponent();
- return waitForPromises();
- });
-
- it('renders the issuable list with skeletons while fetching service desk issues', async () => {
- wrapper = createComponent();
- await nextTick();
-
- expect(findIssuableList().props('issuablesLoading')).toBe(true);
-
- await waitForPromises();
-
- expect(findIssuableList().props('issuablesLoading')).toBe(false);
- });
-
- it('fetches service desk issues and renders them in the issuable list', () => {
- expect(findIssuableList().props()).toMatchObject({
- namespace: 'service-desk',
- recentSearchesStorageKey: 'service-desk-issues',
- issuables: defaultQueryResponse.data.project.issues.nodes,
- tabs: issuableListTabs,
- currentTab: STATUS_OPEN,
- tabCounts: {
- opened: 1,
- closed: 1,
- all: 1,
- },
- });
- });
-
- describe('InfoBanner', () => {
- it('renders when Service Desk is supported and has any number of issues', () => {
- expect(findInfoBanner().exists()).toBe(true);
- });
-
- it('does not render when Service Desk is not supported and has any number of issues', () => {
- wrapper = createComponent({ provide: { isServiceDeskSupported: false } });
-
- expect(findInfoBanner().exists()).toBe(false);
- });
-
- it('does not render, when there are no issues', () => {
- wrapper = createComponent({
- serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler,
- });
-
- expect(findInfoBanner().exists()).toBe(false);
- });
- });
-
- describe('Empty states', () => {
- describe('when there are issues', () => {
- it('shows EmptyStateWithAnyIssues component', () => {
- setWindowLocation(locationSearch);
- wrapper = createComponent({
- serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler,
- });
-
- expect(wrapper.findComponent(EmptyStateWithAnyIssues).props()).toEqual({
- hasSearch: true,
- isOpenTab: true,
- });
- });
- });
-
- describe('when there are no issues', () => {
- it('shows EmptyStateWithoutAnyIssues component', () => {
- wrapper = createComponent({
- provide: { hasAnyIssues: false },
- serviceDeskIssuesQueryResponseHandler: mockServiceDeskIssuesQueryEmptyResponseHandler,
- });
-
- expect(wrapper.findComponent(EmptyStateWithoutAnyIssues).exists()).toBe(true);
- });
- });
- });
-
- describe('Initial url params', () => {
- describe('search', () => {
- it('is set from the url params', () => {
- setWindowLocation(locationSearch);
- wrapper = createComponent();
-
- expect(router.history.current.query).toMatchObject({ search: 'find issues' });
- });
- });
-
- describe('state', () => {
- it('is set from the url params', async () => {
- const initialState = STATUS_ALL;
- setWindowLocation(`?state=${initialState}`);
- wrapper = createComponent();
- await waitForPromises();
-
- expect(findIssuableList().props('currentTab')).toBe(initialState);
- });
- });
-
- describe('filter tokens', () => {
- it('are set from the url params', () => {
- setWindowLocation(locationSearch);
- wrapper = createComponent();
-
- expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
- });
- });
- });
-
- describe('Tokens', () => {
- const mockCurrentUser = {
- id: 1,
- name: 'Administrator',
- username: 'root',
- avatar_url: 'avatar/url',
- };
-
- describe('when user is signed out', () => {
- beforeEach(() => {
- wrapper = createComponent({ provide: { isSignedIn: false } });
- return waitForPromises();
- });
-
- it('does not render My-Reaction or Confidential tokens', () => {
- expect(findIssuableList().props('searchTokens')).not.toMatchObject([
- { type: TOKEN_TYPE_AUTHOR, preloadedUsers: [mockCurrentUser] },
- { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers: [mockCurrentUser] },
- { type: TOKEN_TYPE_MY_REACTION },
- { type: TOKEN_TYPE_CONFIDENTIAL },
- ]);
- });
- });
-
- describe('when all tokens are available', () => {
- beforeEach(() => {
- window.gon = {
- current_user_id: mockCurrentUser.id,
- current_user_fullname: mockCurrentUser.name,
- current_username: mockCurrentUser.username,
- current_user_avatar_url: mockCurrentUser.avatar_url,
- };
-
- wrapper = createComponent();
- return waitForPromises();
- });
-
- it('renders all tokens alphabetically', () => {
- const preloadedUsers = [
- { ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
- ];
-
- expect(findIssuableList().props('searchTokens')).toMatchObject([
- { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
- { type: TOKEN_TYPE_CONFIDENTIAL },
- { type: TOKEN_TYPE_LABEL },
- { type: TOKEN_TYPE_MILESTONE },
- { type: TOKEN_TYPE_MY_REACTION },
- { type: TOKEN_TYPE_RELEASE },
- { type: TOKEN_TYPE_SEARCH_WITHIN },
- ]);
- });
- });
- });
-
- describe('Events', () => {
- describe('when "click-tab" event is emitted by IssuableList', () => {
- beforeEach(async () => {
- wrapper = createComponent();
- router.push = jest.fn();
- await waitForPromises();
-
- findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
- });
-
- it('updates ui to the new tab', () => {
- expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
- });
-
- it('updates url to the new tab', () => {
- expect(router.push).toHaveBeenCalledWith({
- query: expect.objectContaining({ state: STATUS_CLOSED }),
- });
- });
- });
-
- describe('when "filter" event is emitted by IssuableList', () => {
- it('updates IssuableList with url params', async () => {
- wrapper = createComponent();
- router.push = jest.fn();
- await waitForPromises();
-
- findIssuableList().vm.$emit('filter', filteredTokens);
- await nextTick();
-
- expect(router.push).toHaveBeenCalledWith({
- query: expect.objectContaining(urlParams),
- });
- });
- });
- });
-
- describe('Errors', () => {
- describe.each`
- error | responseHandler
- ${'fetching issues'} | ${'serviceDeskIssuesQueryResponseHandler'}
- ${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponseHandler'}
- `('when there is an error $error', ({ responseHandler }) => {
- beforeEach(() => {
- wrapper = createComponent({
- [responseHandler]: jest.fn().mockRejectedValue(new Error('ERROR')),
- });
- return waitForPromises();
- });
-
- it('shows an error message', () => {
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
- });
- });
- });
-
- describe('When providing token for labels', () => {
- it('passes function to fetchLatestLabels property if frontend caching is enabled', async () => {
- wrapper = createComponent({
- provide: {
- glFeatures: {
- frontendCaching: true,
- },
- },
- });
- await waitForPromises();
-
- expect(typeof findLabelsToken().fetchLatestLabels).toBe('function');
- });
-
- it('passes null to fetchLatestLabels property if frontend caching is disabled', async () => {
- wrapper = createComponent({
- provide: {
- glFeatures: {
- frontendCaching: false,
- },
- },
- });
- await waitForPromises();
-
- expect(findLabelsToken().fetchLatestLabels).toBe(null);
- });
- });
-});
diff --git a/spec/frontend/sidebar/components/assignees/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js
index 65a07382ebc..2767d36ac3d 100644
--- a/spec/frontend/sidebar/components/assignees/assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js
@@ -145,7 +145,7 @@ describe('Assignee component', () => {
});
expect(findAllAvatarLinks()).toHaveLength(users.length);
- expect(wrapper.find('.user-list-more').exists()).toBe(false);
+ expect(wrapper.find('[data-testid="user-list-more"]').exists()).toBe(false);
});
it('shows sorted assignee where "can merge" users are sorted first', () => {
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
index 501048bf056..8c42e61548f 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
@@ -26,7 +26,7 @@ describe('Sidebar invite members component', () => {
});
it('has expected attributes on the trigger', () => {
- expect(findDirectInviteLink().props('triggerSource')).toBe('issue-assignee-dropdown');
+ expect(findDirectInviteLink().props('triggerSource')).toBe('issue_assignee_dropdown');
});
});
});
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index c74a714cca4..9e7a198d32c 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -24,7 +24,7 @@ describe('UncollapsedAssigneeList component', () => {
});
}
- const findMoreButton = () => wrapper.find('.user-list-more button');
+ const findMoreButton = () => wrapper.find('[data-testid="user-list-more-button"]');
describe('One assignee/user', () => {
let user;
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
index 1ca20dad1c6..3588e92d515 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -4,7 +4,7 @@ import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
-import { confidentialityQueries } from '~/sidebar/constants';
+import { confidentialityQueries } from '~/sidebar/queries/constants';
jest.mock('~/alert');
@@ -38,6 +38,23 @@ describe('Sidebar Confidentiality Form', () => {
});
};
+ const confidentialityMutation = (confidential, workspacePath) => {
+ return {
+ mutation: confidentialityQueries[wrapper.vm.issuableType].mutation,
+ variables: {
+ input: {
+ confidential,
+ iid: '1',
+ ...workspacePath,
+ },
+ },
+ };
+ };
+
+ const clickConfidentialToggle = () => {
+ findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
+ };
+
it('emits a `closeForm` event when Cancel button is clicked', () => {
createComponent();
findCancelButton().vm.$emit('click');
@@ -94,17 +111,10 @@ describe('Sidebar Confidentiality Form', () => {
});
it('calls a mutation to set confidential to true on button click', () => {
- findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: confidentialityQueries[wrapper.vm.issuableType].mutation,
- variables: {
- input: {
- confidential: true,
- iid: '1',
- projectPath: 'group/project',
- },
- },
- });
+ clickConfidentialToggle();
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
+ confidentialityMutation(true, { projectPath: 'group/project' }),
+ );
});
});
@@ -150,17 +160,49 @@ describe('Sidebar Confidentiality Form', () => {
});
it('calls a mutation to set epic confidentiality with correct parameters', () => {
- findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
+ clickConfidentialToggle();
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
+ confidentialityMutation(false, { groupPath: 'group/project' }),
+ );
+ });
+ });
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: confidentialityQueries[wrapper.vm.issuableType].mutation,
- variables: {
- input: {
- confidential: false,
- iid: '1',
- groupPath: 'group/project',
- },
- },
+ describe('when issuable type is `test_case`', () => {
+ describe('when test case is confidential', () => {
+ beforeEach(() => {
+ createComponent({ props: { confidential: true, issuableType: 'test_case' } });
+ });
+
+ it('renders a message about making a test case non-confidential', () => {
+ expect(findWarningMessage().text()).toBe(
+ 'You are going to turn off the confidentiality. This means everyone will be able to see this test case.',
+ );
+ });
+
+ it('calls a mutation to set confidential to false on button click', () => {
+ clickConfidentialToggle();
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
+ confidentialityMutation(false, { projectPath: 'group/project' }),
+ );
+ });
+ });
+
+ describe('when test case is not confidential', () => {
+ beforeEach(() => {
+ createComponent({ props: { issuableType: 'test_case' } });
+ });
+
+ it('renders a message about making a test case confidential', () => {
+ expect(findWarningMessage().text()).toBe(
+ 'You are going to turn on confidentiality. Only project members with at least the Reporter role can view or be notified about this test case.',
+ );
+ });
+
+ it('calls a mutation to set confidential to true on button click', () => {
+ clickConfidentialToggle();
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith(
+ confidentialityMutation(true, { projectPath: 'group/project' }),
+ );
});
});
});
diff --git a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
index 00b57b4916e..f3d50f17e2d 100644
--- a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
@@ -11,11 +11,8 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SidebarEscalationStatus from '~/sidebar/components/incidents/sidebar_escalation_status.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import {
- escalationStatusQuery,
- escalationStatusMutation,
- STATUS_ACKNOWLEDGED,
-} from '~/sidebar/constants';
+import { STATUS_ACKNOWLEDGED } from '~/sidebar/constants';
+import { escalationStatusQuery, escalationStatusMutation } from '~/sidebar/queries/constants';
import waitForPromises from 'helpers/wait_for_promises';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
import { createAlert } from '~/alert';
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
index 9c8d9656955..5e2ff73878f 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js
@@ -5,11 +5,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import { workspaceLabelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries, workspaceCreateLabelMutation } from '~/sidebar/queries/constants';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
-import createLabelMutation from '~/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
import { DEFAULT_LABEL_COLOR } from '~/sidebar/components/labels/labels_select_widget/constants';
import {
+ mockCreateLabelResponse as createAbuseReportLabelSuccessfulResponse,
+ mockLabelsQueryResponse as abuseReportLabelsQueryResponse,
+} from '../../../../admin/abuse_report/mock_data';
+import {
mockRegularLabel,
mockSuggestedColors,
createLabelSuccessfulResponse,
@@ -38,6 +41,9 @@ const titleTakenError = {
};
const createLabelSuccessHandler = jest.fn().mockResolvedValue(createLabelSuccessfulResponse);
+const createAbuseReportLabelSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(createAbuseReportLabelSuccessfulResponse);
const createLabelUserRecoverableErrorHandler = jest.fn().mockResolvedValue(userRecoverableError);
const createLabelDuplicateErrorHandler = jest.fn().mockResolvedValue(titleTakenError);
const createLabelErrorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
@@ -66,6 +72,7 @@ describe('DropdownContentsCreateView', () => {
labelsResponse = workspaceLabelsQueryResponse,
searchTerm = '',
} = {}) => {
+ const createLabelMutation = workspaceCreateLabelMutation[workspaceType];
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
query: workspaceLabelsQueries[workspaceType].query,
@@ -203,6 +210,22 @@ describe('DropdownContentsCreateView', () => {
});
});
+ it('calls the correct mutation when workspaceType is `abuseReport`', () => {
+ createComponent({
+ mutationHandler: createAbuseReportLabelSuccessHandler,
+ labelCreateType: '',
+ workspaceType: 'abuseReport',
+ labelsResponse: abuseReportLabelsQueryResponse,
+ });
+ fillLabelAttributes();
+ findCreateButton().vm.$emit('click');
+
+ expect(createAbuseReportLabelSuccessHandler).toHaveBeenCalledWith({
+ color: '#009966',
+ title: 'Test title',
+ });
+ });
+
it('calls createAlert is mutation has a user-recoverable error', async () => {
createComponent({ mutationHandler: createLabelUserRecoverableErrorHandler });
fillLabelAttributes();
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
index ad1edaa6671..7a1131b8cce 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js
@@ -1,12 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
describe('DropdownFooter', () => {
let wrapper;
const createComponent = ({ props = {}, injected = {} } = {}) => {
- wrapper = shallowMount(DropdownFooter, {
+ wrapper = shallowMountExtended(DropdownFooter, {
propsData: {
footerCreateLabelTitle: 'create',
footerManageLabelTitle: 'manage',
@@ -20,7 +19,8 @@ describe('DropdownFooter', () => {
});
};
- const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
+ const findCreateLabelButton = () => wrapper.findByTestId('create-label-button');
+ const findManageLabelsButton = () => wrapper.findByTestId('manage-labels-button');
describe('Labels view', () => {
beforeEach(() => {
@@ -42,12 +42,37 @@ describe('DropdownFooter', () => {
expect(findCreateLabelButton().exists()).toBe(true);
});
- it('emits `toggleDropdownContentsCreateView` event on create label button click', async () => {
+ it('emits `toggleDropdownContentsCreateView` event on create label button click', () => {
findCreateLabelButton().trigger('click');
- await nextTick();
expect(wrapper.emitted('toggleDropdownContentsCreateView')).toEqual([[]]);
});
});
+
+ describe('manage labels button', () => {
+ it('is rendered', () => {
+ expect(findManageLabelsButton().exists()).toBe(true);
+ });
+
+ describe('when footerManageLabelTitle is not given', () => {
+ beforeEach(() => {
+ createComponent({ props: { footerManageLabelTitle: undefined } });
+ });
+
+ it('does not render manage labels button', () => {
+ expect(findManageLabelsButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when labelsManagePath is not provided', () => {
+ beforeEach(() => {
+ createComponent({ injected: { labelsManagePath: '' } });
+ });
+
+ it('does not render manage labels button', () => {
+ expect(findManageLabelsButton().exists()).toBe(false);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
index 18d4df297df..d5bbd3bb3c9 100644
--- a/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
+++ b/spec/frontend/sidebar/components/lock/__snapshots__/edit_form_spec.js.snap
@@ -12,7 +12,6 @@ exports[`Edit Form Dropdown In issue page when locked the appropriate warning te
message="Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
/>
</p>
-
<edit-form-buttons-stub
islocked="true"
issuabledisplayname="issue"
@@ -32,7 +31,6 @@ exports[`Edit Form Dropdown In issue page when unlocked the appropriate warning
message="Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
/>
</p>
-
<edit-form-buttons-stub
issuabledisplayname="issue"
/>
@@ -51,7 +49,6 @@ exports[`Edit Form Dropdown In merge request page when locked the appropriate wa
message="Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
/>
</p>
-
<edit-form-buttons-stub
islocked="true"
issuabledisplayname="merge request"
@@ -71,7 +68,6 @@ exports[`Edit Form Dropdown In merge request page when unlocked the appropriate
message="Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
/>
</p>
-
<edit-form-buttons-stub
issuabledisplayname="merge request"
/>
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index f43fb17ca37..5dd54d4867e 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -120,11 +120,11 @@ describe('Issuable Time Tracker', () => {
describe('Remaining meter', () => {
it('should display the remaining meter with the correct width', () => {
- expect(findTimeRemainingProgress().attributes('value')).toBe('5');
+ expect(findTimeRemainingProgress().vm.$attrs.value).toBe(5);
});
it('should display the remaining meter with the correct background color when within estimate', () => {
- expect(findTimeRemainingProgress().attributes('variant')).toBe('primary');
+ expect(findTimeRemainingProgress().vm.$attrs.variant).toBe('primary');
});
it('should display the remaining meter with the correct background color when over estimate', () => {
@@ -138,7 +138,7 @@ describe('Issuable Time Tracker', () => {
},
});
- expect(findTimeRemainingProgress().attributes('variant')).toBe('danger');
+ expect(findTimeRemainingProgress().vm.$attrs.variant).toBe('danger');
});
});
});
diff --git a/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
index fd525474923..b5d8d31f88f 100644
--- a/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
@@ -3,7 +3,7 @@
exports[`SidebarTodo template renders component container element with proper data attributes 1`] = `
<button
aria-label="Mark as done"
- class="gl-button btn btn-default btn-todo issuable-header-btn float-right"
+ class="btn btn-default btn-todo float-right gl-button issuable-header-btn"
data-issuable-id="1"
data-issuable-type="epic"
type="button"
@@ -14,13 +14,11 @@ exports[`SidebarTodo template renders component container element with proper da
size="16"
style="display: none;"
/>
-
<span
class="issuable-todo-inner"
>
Mark as done
</span>
-
<gl-loading-icon-stub
color="dark"
inline="true"
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 05a7f504fd4..9d8392ad5f0 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -414,6 +414,33 @@ export const searchQueryResponse = {
},
};
+export const searchAutocompleteQueryResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: '',
+ users: [
+ {
+ id: '1',
+ avatarUrl: '/avatar',
+ name: 'root',
+ username: 'root',
+ webUrl: 'root',
+ status: null,
+ },
+ {
+ id: '2',
+ avatarUrl: '/avatar2',
+ name: 'rookie',
+ username: 'rookie',
+ webUrl: 'rookie',
+ status: null,
+ },
+ ],
+ },
+ },
+};
+
export const updateIssueAssigneesMutationResponse = {
data: {
issuableSetAssignees: {
@@ -545,6 +572,29 @@ export const searchResponseOnMR = {
},
};
+export const searchAutocompleteResponseOnMR = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: '1',
+ users: [
+ {
+ ...mockUser1,
+ mergeRequestInteraction: {
+ canMerge: true,
+ },
+ },
+ {
+ ...mockUser2,
+ mergeRequestInteraction: {
+ canMerge: false,
+ },
+ },
+ ],
+ },
+ },
+};
+
export const projectMembersResponse = {
data: {
workspace: {
@@ -585,6 +635,36 @@ export const projectMembersResponse = {
},
};
+export const projectAutocompleteMembersResponse = {
+ data: {
+ workspace: {
+ id: '1',
+ __typename: 'Project',
+ users: [
+ // Remove nulls https://gitlab.com/gitlab-org/gitlab/-/issues/329750
+ null,
+ null,
+ // Remove duplicated entry https://gitlab.com/gitlab-org/gitlab/-/issues/327822
+ mockUser1,
+ mockUser1,
+ mockUser2,
+ {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/2',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon',
+ name: 'Jacki Kub',
+ username: 'francina.skiles',
+ webUrl: '/franc',
+ status: {
+ availability: 'BUSY',
+ },
+ },
+ ],
+ },
+ },
+};
+
export const groupMembersResponse = {
data: {
workspace: {
diff --git a/spec/frontend/silent_mode_settings/components/app_spec.js b/spec/frontend/silent_mode_settings/components/app_spec.js
new file mode 100644
index 00000000000..5997bfd1b5f
--- /dev/null
+++ b/spec/frontend/silent_mode_settings/components/app_spec.js
@@ -0,0 +1,133 @@
+import { GlToggle, GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import toast from '~/vue_shared/plugins/global_toast';
+import { updateApplicationSettings } from '~/rest_api';
+import SilentModeSettingsApp from '~/silent_mode_settings/components/app.vue';
+
+jest.mock('~/rest_api.js');
+jest.mock('~/alert');
+jest.mock('~/vue_shared/plugins/global_toast');
+
+const MOCK_DEFAULT_SILENT_MODE_ENABLED = false;
+
+describe('SilentModeSettingsApp', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ const defaultProps = {
+ isSilentModeEnabled: MOCK_DEFAULT_SILENT_MODE_ENABLED,
+ };
+
+ wrapper = shallowMount(SilentModeSettingsApp, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findGlToggle = () => wrapper.findComponent(GlToggle);
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+
+ describe('template', () => {
+ describe('experiment badge', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders properly', () => {
+ expect(findGlBadge().exists()).toBe(true);
+ });
+ });
+
+ describe('when silent mode is already enabled', () => {
+ beforeEach(() => {
+ createComponent({ isSilentModeEnabled: true });
+ });
+
+ it('renders the component with the GlToggle set to true', () => {
+ expect(findGlToggle().attributes('value')).toBe('true');
+ });
+ });
+
+ describe('when silent mode is no already enabled', () => {
+ beforeEach(() => {
+ createComponent({ isSilentModeEnabled: false });
+ });
+
+ it('renders the component with the GlToggle set to undefined', () => {
+ expect(findGlToggle().attributes('value')).toBeUndefined();
+ });
+ });
+ });
+
+ describe.each`
+ enabled | message
+ ${false} | ${'Silent mode disabled'}
+ ${true} | ${'Silent mode enabled'}
+ `(`toast message`, ({ enabled, message }) => {
+ beforeEach(() => {
+ updateApplicationSettings.mockImplementation(() => Promise.resolve());
+ createComponent();
+ });
+
+ it(`when successfully toggled to ${enabled}, toast message is ${message}`, async () => {
+ await findGlToggle().vm.$emit('change', enabled);
+ await waitForPromises();
+
+ expect(toast).toHaveBeenCalledWith(message);
+ });
+ });
+
+ describe.each`
+ description | mockApi | toastMsg | error
+ ${'onSuccess'} | ${() => Promise.resolve()} | ${'Silent mode enabled'} | ${false}
+ ${'onError'} | ${() => Promise.reject()} | ${false} | ${'There was an error updating the Silent Mode Settings.'}
+ `(`when submitting the form $description`, ({ mockApi, toastMsg, error }) => {
+ beforeEach(() => {
+ updateApplicationSettings.mockImplementation(mockApi);
+
+ createComponent();
+ });
+
+ it('calls updateApplicationSettings correctly', () => {
+ findGlToggle().vm.$emit('change', !MOCK_DEFAULT_SILENT_MODE_ENABLED);
+
+ expect(updateApplicationSettings).toHaveBeenCalledWith({
+ silent_mode_enabled: !MOCK_DEFAULT_SILENT_MODE_ENABLED,
+ });
+ });
+
+ it('handles the loading icon correctly', async () => {
+ expect(findGlToggle().props('isLoading')).toBe(false);
+
+ await findGlToggle().vm.$emit('change', !MOCK_DEFAULT_SILENT_MODE_ENABLED);
+
+ expect(findGlToggle().props('isLoading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findGlToggle().props('isLoading')).toBe(false);
+ });
+
+ it(`does ${toastMsg ? '' : 'not '}render an success toast message`, async () => {
+ await findGlToggle().vm.$emit('change', !MOCK_DEFAULT_SILENT_MODE_ENABLED);
+ await waitForPromises();
+
+ return toastMsg
+ ? expect(toast).toHaveBeenCalledWith(toastMsg)
+ : expect(toast).not.toHaveBeenCalled();
+ });
+
+ it(`does ${error ? '' : 'not '}render an error message`, async () => {
+ await findGlToggle().vm.$emit('change', !MOCK_DEFAULT_SILENT_MODE_ENABLED);
+ await waitForPromises();
+
+ return error
+ ? expect(createAlert).toHaveBeenCalledWith({ message: error })
+ : expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
index 76e84fa183c..1c60c3af310 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
@@ -8,11 +8,10 @@ exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
<blob-header-edit-stub
candelete="true"
data-qa-selector="file_name_field"
- id="blob_local_7_file_path"
+ id="reference-0"
showdelete="true"
value="foo/bar/test.md"
/>
-
<source-editor-stub
debouncevalue="250"
editoroptions="[object Object]"
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index e783927f87b..5ed3b520b70 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -5,16 +5,15 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
class="form-group js-description-input"
>
<label
- for="snippet-description"
+ for="reference-0"
>
Description (optional)
</label>
-
<div
class="js-collapsible-input"
>
<div
- class="js-collapsed d-none"
+ class="d-none js-collapsed"
>
<gl-form-input-stub
class="form-control"
@@ -22,9 +21,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
placeholder="Describe what your snippet does or how to use it…"
/>
</div>
-
<div
- class="js-vue-markdown-field md-area position-relative gfm-form gl-overflow-hidden js-expanded"
+ class="gfm-form gl-overflow-hidden js-expanded js-vue-markdown-field md-area position-relative"
data-uploads-path=""
>
<markdown-header-stub
@@ -36,23 +34,22 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
suggestionstartindex="0"
uploadspath=""
/>
-
<div
class="md-write-holder"
>
<div
- class="zen-backdrop div-dropzone-wrapper"
+ class="div-dropzone-wrapper zen-backdrop"
>
<div
class="div-dropzone js-invalid-dropzone"
>
<textarea
aria-label="Description"
- class="note-textarea js-gfm-input js-autosize markdown-area js-gfm-input-initialized"
+ class="js-autosize js-gfm-input js-gfm-input-initialized markdown-area note-textarea"
data-qa-selector="snippet_description_field"
data-supports-quick-actions="false"
dir="auto"
- id="snippet-description"
+ id="reference-0"
placeholder="Write a comment or drag your files here…"
style="overflow-x: hidden; word-wrap: break-word; overflow-y: hidden;"
/>
@@ -68,10 +65,9 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
</svg>
</div>
</div>
-
<a
aria-label="Leave zen mode"
- class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
+ class="gl-text-gray-500 js-zen-leave zen-control zen-control-leave"
href="#"
>
<gl-icon-stub
@@ -79,7 +75,6 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
size="16"
/>
</a>
-
<markdown-toolbar-stub
canattachfile="true"
markdowndocspath="help/"
@@ -87,19 +82,14 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
/>
</div>
</div>
-
<div
- class="js-vue-md-preview md-preview-holder gl-px-5"
+ class="gl-px-5 js-vue-md-preview md-preview-holder"
style="display: none;"
>
<div
class="md"
/>
</div>
-
- <!---->
-
- <!---->
</div>
</div>
</div>
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap
index 9fb43815cbc..2b2335036f6 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap
@@ -6,7 +6,7 @@ exports[`Snippet Description component matches the snapshot 1`] = `
data-qa-selector="snippet_description_content"
>
<div
- class="md js-snippet-description"
+ class="js-snippet-description md"
>
<h2>
The property of Thor
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
index ed54582ca29..3274f41e4af 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
@@ -5,9 +5,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
class="form-group"
>
<label>
-
Visibility level
-
<gl-link-stub
href="/foo/bar"
target="_blank"
@@ -18,10 +16,9 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/>
</gl-link-stub>
</label>
-
<gl-form-group-stub
class="gl-mb-0"
- id="visibility-level-setting"
+ id="reference-0"
labeldescription=""
optionaltext="(optional)"
>
@@ -39,15 +36,14 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
value="private"
>
<div
- class="d-flex align-items-center"
+ class="align-items-center d-flex"
>
<gl-icon-stub
name="lock"
size="16"
/>
-
<span
- class="font-weight-bold ml-1 js-visibility-option"
+ class="font-weight-bold js-visibility-option ml-1"
data-qa-selector="visibility_content"
data-qa-visibility="Private"
>
@@ -60,15 +56,14 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
value="internal"
>
<div
- class="d-flex align-items-center"
+ class="align-items-center d-flex"
>
<gl-icon-stub
name="shield"
size="16"
/>
-
<span
- class="font-weight-bold ml-1 js-visibility-option"
+ class="font-weight-bold js-visibility-option ml-1"
data-qa-selector="visibility_content"
data-qa-visibility="Internal"
>
@@ -81,15 +76,14 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
value="public"
>
<div
- class="d-flex align-items-center"
+ class="align-items-center d-flex"
>
<gl-icon-stub
name="earth"
size="16"
/>
-
<span
- class="font-weight-bold ml-1 js-visibility-option"
+ class="font-weight-bold js-visibility-option ml-1"
data-qa-selector="visibility_content"
data-qa-visibility="Public"
>
@@ -99,12 +93,9 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub>
</gl-form-radio-group-stub>
</gl-form-group-stub>
-
<div
class="text-muted"
data-testid="restricted-levels-info"
- >
- <!---->
- </div>
+ />
</div>
`;
diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js
index d8c6ad3278a..cb9b9800bfe 100644
--- a/spec/frontend/snippets/components/embed_dropdown_spec.js
+++ b/spec/frontend/snippets/components/embed_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlFormInputGroup } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { escape as esc } from 'lodash';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
@@ -10,56 +10,24 @@ describe('snippets/components/embed_dropdown', () => {
let wrapper;
const createComponent = () => {
- wrapper = mount(EmbedDropdown, {
+ wrapper = shallowMountExtended(EmbedDropdown, {
propsData: {
url: TEST_URL,
},
});
};
- const findSectionsData = () => {
- const sections = [];
- let current = {};
-
- wrapper.findAll('[data-testid="header"],[data-testid="input"]').wrappers.forEach((x) => {
- const type = x.attributes('data-testid');
-
- if (type === 'header') {
- current = {
- header: x.text(),
- };
-
- sections.push(current);
- } else {
- const value = x.findComponent(GlFormInputGroup).props('value');
- const copyValue = x.find('button[title="Copy"]').attributes('data-clipboard-text');
-
- Object.assign(current, {
- value,
- copyValue,
- });
- }
- });
-
- return sections;
- };
+ const findEmbedSection = () => wrapper.findByTestId('section-Embed');
+ const findShareSection = () => wrapper.findByTestId('section-Share');
it('renders dropdown items', () => {
createComponent();
const embedValue = `<script src="${esc(TEST_URL)}.js"></script>`;
- expect(findSectionsData()).toEqual([
- {
- header: 'Embed',
- value: embedValue,
- copyValue: embedValue,
- },
- {
- header: 'Share',
- value: TEST_URL,
- copyValue: TEST_URL,
- },
- ]);
+ expect(findEmbedSection().text()).toBe('Embed');
+ expect(findShareSection().text()).toBe('Share');
+ expect(findEmbedSection().findComponent(GlFormInputGroup).attributes('value')).toBe(embedValue);
+ expect(findShareSection().findComponent(GlFormInputGroup).attributes('value')).toBe(TEST_URL);
});
});
diff --git a/spec/frontend/super_sidebar/components/context_header_spec.js b/spec/frontend/super_sidebar/components/context_header_spec.js
deleted file mode 100644
index 943b659c997..00000000000
--- a/spec/frontend/super_sidebar/components/context_header_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { GlAvatar } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ContextHeader from '~/super_sidebar/components/context_header.vue';
-
-describe('ContextHeader component', () => {
- let wrapper;
-
- const context = {
- id: 1,
- title: 'Title',
- avatar: '/path/to/avatar.png',
- };
-
- const findGlAvatar = () => wrapper.getComponent(GlAvatar);
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMountExtended(ContextHeader, {
- propsData: {
- context,
- expanded: false,
- ...props,
- },
- });
- };
-
- describe('with an avatar', () => {
- it('passes the correct props to GlAvatar', () => {
- createWrapper();
- const avatar = findGlAvatar();
-
- expect(avatar.props('shape')).toBe('rect');
- expect(avatar.props('entityName')).toBe(context.title);
- expect(avatar.props('entityId')).toBe(context.id);
- expect(avatar.props('src')).toBe(context.avatar);
- });
-
- it('renders the avatar with a custom shape', () => {
- const customShape = 'circle';
- createWrapper({
- context: {
- ...context,
- avatar_shape: customShape,
- },
- });
- const avatar = findGlAvatar();
-
- expect(avatar.props('shape')).toBe(customShape);
- });
- });
-});
diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js
deleted file mode 100644
index dd8f39e7cb7..00000000000
--- a/spec/frontend/super_sidebar/components/context_switcher_spec.js
+++ /dev/null
@@ -1,302 +0,0 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlDisclosureDropdown, GlSearchBoxByType, GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import { s__ } from '~/locale';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
-import ContextSwitcherToggle from '~/super_sidebar/components/context_switcher_toggle.vue';
-import NavItem from '~/super_sidebar/components/nav_item.vue';
-import ProjectsList from '~/super_sidebar/components/projects_list.vue';
-import GroupsList from '~/super_sidebar/components/groups_list.vue';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import searchUserProjectsAndGroupsQuery from '~/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql';
-import { trackContextAccess, formatContextSwitcherItems } from '~/super_sidebar/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import { stubComponent } from 'helpers/stub_component';
-import { contextSwitcherLinks, searchUserProjectsAndGroupsResponseMock } from '../mock_data';
-
-jest.mock('~/super_sidebar/utils', () => ({
- getStorageKeyFor: jest.requireActual('~/super_sidebar/utils').getStorageKeyFor,
- getTopFrequentItems: jest.requireActual('~/super_sidebar/utils').getTopFrequentItems,
- formatContextSwitcherItems: jest.requireActual('~/super_sidebar/utils')
- .formatContextSwitcherItems,
- trackContextAccess: jest.fn(),
-}));
-const focusInputMock = jest.fn();
-
-const username = 'root';
-const projectsPath = 'projectsPath';
-const groupsPath = 'groupsPath';
-const contextHeader = { avatar_shape: 'circle' };
-
-Vue.use(VueApollo);
-
-describe('ContextSwitcher component', () => {
- let wrapper;
- let mockApollo;
-
- const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findContextSwitcherToggle = () => wrapper.findComponent(ContextSwitcherToggle);
- const findNavItems = () => wrapper.findAllComponents(NavItem);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
- const findProjectsList = () => wrapper.findComponent(ProjectsList);
- const findGroupsList = () => wrapper.findComponent(GroupsList);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- const triggerSearchQuery = async () => {
- findSearchBox().vm.$emit('input', 'foo');
- await nextTick();
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- return waitForPromises();
- };
-
- const searchUserProjectsAndGroupsHandlerSuccess = jest
- .fn()
- .mockResolvedValue(searchUserProjectsAndGroupsResponseMock);
-
- const createWrapper = ({ props = {}, requestHandlers = {} } = {}) => {
- mockApollo = createMockApollo([
- [
- searchUserProjectsAndGroupsQuery,
- requestHandlers.searchUserProjectsAndGroupsQueryHandler ??
- searchUserProjectsAndGroupsHandlerSuccess,
- ],
- ]);
-
- wrapper = shallowMountExtended(ContextSwitcher, {
- apolloProvider: mockApollo,
- provide: {
- contextSwitcherLinks,
- },
- propsData: {
- username,
- projectsPath,
- groupsPath,
- contextHeader,
- ...props,
- },
- stubs: {
- GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
- template: `
- <div>
- <slot name="toggle" />
- <slot />
- </div>
- `,
- }),
- GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
- props: ['placeholder'],
- methods: { focusInput: focusInputMock },
- }),
- ProjectsList: stubComponent(ProjectsList, {
- props: ['username', 'viewAllLink', 'isSearch', 'searchResults'],
- }),
- GroupsList: stubComponent(GroupsList, {
- props: ['username', 'viewAllLink', 'isSearch', 'searchResults'],
- }),
- },
- });
- };
-
- describe('default', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders the context switcher links', () => {
- const navItems = findNavItems();
- const firstNavItem = navItems.at(0);
-
- expect(navItems.length).toBe(contextSwitcherLinks.length);
- expect(firstNavItem.props('item')).toBe(contextSwitcherLinks[0]);
- expect(firstNavItem.props('linkClasses')).toEqual({
- [contextSwitcherLinks[0].link_classes]: contextSwitcherLinks[0].link_classes,
- });
- });
-
- it('passes the placeholder to the search box', () => {
- expect(findSearchBox().props('placeholder')).toBe(
- s__('Navigation|Search your projects or groups'),
- );
- });
-
- it('passes the correct props to the frequent projects list', () => {
- expect(findProjectsList().props()).toEqual({
- username,
- viewAllLink: projectsPath,
- isSearch: false,
- searchResults: [],
- });
- });
-
- it('passes the correct props to the frequent groups list', () => {
- expect(findGroupsList().props()).toEqual({
- username,
- viewAllLink: groupsPath,
- isSearch: false,
- searchResults: [],
- });
- });
-
- it('does not trigger the search query on mount', () => {
- expect(searchUserProjectsAndGroupsHandlerSuccess).not.toHaveBeenCalled();
- });
-
- it('shows a loading spinner when search query is typed in', async () => {
- findSearchBox().vm.$emit('input', 'foo');
- await nextTick();
-
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('passes the correct props to the toggle', () => {
- expect(findContextSwitcherToggle().props('context')).toEqual(contextHeader);
- expect(findContextSwitcherToggle().props('expanded')).toEqual(false);
- });
-
- it('does not emit the `toggle` event initially', () => {
- expect(wrapper.emitted('toggle')).toBe(undefined);
- });
- });
-
- describe('visibility changes', () => {
- beforeEach(() => {
- createWrapper();
- findDisclosureDropdown().vm.$emit('shown');
- });
-
- it('emits the `toggle` event, focuses the search input and puts the toggle in the expanded state when opened', () => {
- expect(wrapper.emitted('toggle')).toHaveLength(1);
- expect(wrapper.emitted('toggle')[0]).toEqual([true]);
- expect(focusInputMock).toHaveBeenCalledTimes(1);
- expect(findContextSwitcherToggle().props('expanded')).toBe(true);
- });
-
- it("emits the `toggle` event, does not attempt to focus the input, and resets the toggle's `expanded` props to `false` when closed", async () => {
- findDisclosureDropdown().vm.$emit('hidden');
- await nextTick();
-
- expect(wrapper.emitted('toggle')).toHaveLength(2);
- expect(wrapper.emitted('toggle')[1]).toEqual([false]);
- expect(focusInputMock).toHaveBeenCalledTimes(1);
- expect(findContextSwitcherToggle().props('expanded')).toBe(false);
- });
- });
-
- describe('item access tracking', () => {
- it('does not track anything if not within a trackable context', () => {
- createWrapper();
-
- expect(trackContextAccess).not.toHaveBeenCalled();
- });
-
- it('tracks item access if within a trackable context', () => {
- const currentContext = { namespace: 'groups' };
- createWrapper({
- props: {
- currentContext,
- },
- });
-
- expect(trackContextAccess).toHaveBeenCalledWith(username, currentContext);
- });
- });
-
- describe('on search', () => {
- beforeEach(() => {
- createWrapper();
- return triggerSearchQuery();
- });
-
- it('hides persistent links', () => {
- expect(findNavItems().length).toBe(0);
- });
-
- it('triggers the search query on search', () => {
- expect(searchUserProjectsAndGroupsHandlerSuccess).toHaveBeenCalled();
- });
-
- it('hides the loading spinner', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('passes the projects to the frequent projects list', () => {
- expect(findProjectsList().props('isSearch')).toBe(true);
- expect(findProjectsList().props('searchResults')).toEqual(
- formatContextSwitcherItems(searchUserProjectsAndGroupsResponseMock.data.projects.nodes),
- );
- });
-
- it('passes the groups to the frequent groups list', () => {
- expect(findGroupsList().props('isSearch')).toBe(true);
- expect(findGroupsList().props('searchResults')).toEqual(
- formatContextSwitcherItems(searchUserProjectsAndGroupsResponseMock.data.user.groups.nodes),
- );
- });
- });
-
- describe('when search query does not match any items', () => {
- beforeEach(() => {
- createWrapper({
- requestHandlers: {
- searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({
- data: {
- projects: {
- nodes: [],
- },
- user: {
- id: '1',
- groups: {
- nodes: [],
- },
- },
- },
- }),
- },
- });
- return triggerSearchQuery();
- });
-
- it('passes empty results to the lists', () => {
- expect(findProjectsList().props('isSearch')).toBe(true);
- expect(findProjectsList().props('searchResults')).toEqual([]);
- expect(findGroupsList().props('isSearch')).toBe(true);
- expect(findGroupsList().props('searchResults')).toEqual([]);
- });
- });
-
- describe('when search query fails', () => {
- beforeEach(() => {
- jest.spyOn(Sentry, 'captureException');
- });
-
- it('captures exception and shows an alert if response is formatted incorrectly', async () => {
- createWrapper({
- requestHandlers: {
- searchUserProjectsAndGroupsQueryHandler: jest.fn().mockResolvedValue({
- data: {},
- }),
- },
- });
- await triggerSearchQuery();
-
- expect(Sentry.captureException).toHaveBeenCalled();
- expect(findAlert().exists()).toBe(true);
- });
-
- it('captures exception and shows an alert if query fails', async () => {
- createWrapper({
- requestHandlers: {
- searchUserProjectsAndGroupsQueryHandler: jest.fn().mockRejectedValue(),
- },
- });
- await triggerSearchQuery();
-
- expect(Sentry.captureException).toHaveBeenCalled();
- expect(findAlert().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js
deleted file mode 100644
index c20d3c2745f..00000000000
--- a/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { GlIcon } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ContextSwitcherToggle from '~/super_sidebar/components/context_switcher_toggle.vue';
-
-describe('ContextSwitcherToggle component', () => {
- let wrapper;
-
- const context = {
- id: 1,
- title: 'Title',
- avatar: '/path/to/avatar.png',
- };
-
- const findGlIcon = () => wrapper.getComponent(GlIcon);
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMountExtended(ContextSwitcherToggle, {
- propsData: {
- context,
- expanded: false,
- ...props,
- },
- });
- };
-
- it('renders "chevron-down" icon when not expanded', () => {
- createWrapper();
-
- expect(findGlIcon().props('name')).toBe('chevron-down');
- });
-
- it('renders "chevron-up" icon when expanded', () => {
- createWrapper({
- expanded: true,
- });
-
- expect(findGlIcon().props('name')).toBe('chevron-up');
- });
-});
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
index 510a3f5b913..b967fb18a39 100644
--- a/spec/frontend/super_sidebar/components/create_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -1,7 +1,6 @@
import { nextTick } from 'vue';
import {
GlDisclosureDropdown,
- GlTooltip,
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
@@ -9,6 +8,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createNewMenuGroups } from '../mock_data';
describe('CreateMenu component', () => {
@@ -18,7 +18,6 @@ describe('CreateMenu component', () => {
const findGlDisclosureDropdownGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup);
const findGlDisclosureDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
- const findGlTooltip = () => wrapper.findComponent(GlTooltip);
const createWrapper = ({ provide = {} } = {}) => {
wrapper = shallowMountExtended(CreateMenu, {
@@ -33,6 +32,9 @@ describe('CreateMenu component', () => {
InviteMembersTrigger,
GlDisclosureDropdown,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
@@ -45,7 +47,7 @@ describe('CreateMenu component', () => {
createWrapper();
expect(findGlDisclosureDropdown().props('dropdownOffset')).toEqual({
- crossAxis: -147,
+ crossAxis: -179,
mainAxis: 4,
});
});
@@ -74,16 +76,12 @@ describe('CreateMenu component', () => {
expect(findInviteMembersTrigger().exists()).toBe(true);
});
- it("sets the toggle ID and tooltip's target", () => {
- expect(findGlDisclosureDropdown().props('toggleId')).toBe(wrapper.vm.$options.toggleId);
- expect(findGlTooltip().props('target')).toBe(`#${wrapper.vm.$options.toggleId}`);
- });
-
it('hides the tooltip when the dropdown is opened', async () => {
findGlDisclosureDropdown().vm.$emit('shown');
await nextTick();
- expect(findGlTooltip().exists()).toBe(false);
+ const tooltip = getBinding(findGlDisclosureDropdown().element, 'gl-tooltip');
+ expect(tooltip.value).toBe('');
});
it('shows the tooltip when the dropdown is closed', async () => {
@@ -91,7 +89,8 @@ describe('CreateMenu component', () => {
findGlDisclosureDropdown().vm.$emit('hidden');
await nextTick();
- expect(findGlTooltip().exists()).toBe(true);
+ const tooltip = getBinding(findGlDisclosureDropdown().element, 'gl-tooltip');
+ expect(tooltip.value).toBe('Create new...');
});
});
@@ -99,7 +98,7 @@ describe('CreateMenu component', () => {
createWrapper({ provide: { isImpersonating: true } });
expect(findGlDisclosureDropdown().props('dropdownOffset')).toEqual({
- crossAxis: -115,
+ crossAxis: -147,
mainAxis: 4,
});
});
diff --git a/spec/frontend/super_sidebar/components/flyout_menu_spec.js b/spec/frontend/super_sidebar/components/flyout_menu_spec.js
index b894d29c875..bf24de870d9 100644
--- a/spec/frontend/super_sidebar/components/flyout_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/flyout_menu_spec.js
@@ -1,16 +1,26 @@
-import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import FlyoutMenu from '~/super_sidebar/components/flyout_menu.vue';
jest.mock('@floating-ui/dom');
describe('FlyoutMenu', () => {
let wrapper;
+ let dummySection;
const createComponent = () => {
- wrapper = shallowMount(FlyoutMenu, {
+ dummySection = document.createElement('section');
+ dummySection.addEventListener = jest.fn();
+
+ dummySection.getBoundingClientRect = jest.fn();
+ dummySection.getBoundingClientRect.mockReturnValue({ top: 0, bottom: 5, width: 10 });
+
+ document.querySelector = jest.fn();
+ document.querySelector.mockReturnValue(dummySection);
+
+ wrapper = mountExtended(FlyoutMenu, {
propsData: {
targetId: 'section-1',
- items: [],
+ items: [{ id: 1, title: 'item 1', link: 'https://example.com' }],
},
});
};
diff --git a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
deleted file mode 100644
index 63dd941974a..00000000000
--- a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import { s__ } from '~/locale';
-import FrequentItemsList from '~/super_sidebar/components//frequent_items_list.vue';
-import ItemsList from '~/super_sidebar/components/items_list.vue';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { cachedFrequentProjects } from '../mock_data';
-
-const title = s__('Navigation|FREQUENT PROJECTS');
-const pristineText = s__('Navigation|Projects you visit often will appear here.');
-const storageKey = 'storageKey';
-const maxItems = 5;
-
-describe('FrequentItemsList component', () => {
- useLocalStorageSpy();
-
- let wrapper;
-
- const findListTitle = () => wrapper.findByTestId('list-title');
- const findItemsList = () => wrapper.findComponent(ItemsList);
- const findEmptyText = () => wrapper.findByTestId('empty-text');
- const findRemoveItemButton = () => wrapper.findByTestId('item-remove');
-
- const createWrapperFactory = (mountFn = shallowMountExtended) => () => {
- wrapper = mountFn(FrequentItemsList, {
- propsData: {
- title,
- pristineText,
- storageKey,
- maxItems,
- },
- });
- };
- const createWrapper = createWrapperFactory();
- const createFullWrapper = createWrapperFactory(mountExtended);
-
- describe('default', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it("renders the list's title", () => {
- expect(findListTitle().text()).toBe(title);
- });
-
- it('renders the empty text', () => {
- expect(findEmptyText().exists()).toBe(true);
- expect(findEmptyText().text()).toBe(pristineText);
- });
- });
-
- describe('when there are cached frequent items', () => {
- beforeEach(() => {
- window.localStorage.setItem(storageKey, cachedFrequentProjects);
- createWrapper();
- });
-
- it('attempts to retrieve the items from the local storage', () => {
- expect(window.localStorage.getItem).toHaveBeenCalledTimes(1);
- expect(window.localStorage.getItem).toHaveBeenCalledWith(storageKey);
- });
-
- it('renders the maximum amount of items', () => {
- expect(findItemsList().props('items').length).toBe(maxItems);
- });
-
- it('does not render the empty text slot', () => {
- expect(findEmptyText().exists()).toBe(false);
- });
- });
-
- describe('items editing', () => {
- beforeEach(() => {
- window.localStorage.setItem(storageKey, cachedFrequentProjects);
- createFullWrapper();
- });
-
- it('remove-item event emission from items-list causes list item to be removed', async () => {
- const localStorageProjects = findItemsList().props('items');
- await findRemoveItemButton().trigger('click');
-
- expect(findItemsList().props('items')).toHaveLength(maxItems - 1);
- expect(findItemsList().props('items')).not.toContain(localStorageProjects[0]);
- });
- });
-});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap b/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap
index d16d137db2f..e6635672ccf 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/__snapshots__/search_item_spec.js.snap
@@ -2,7 +2,7 @@
exports[`SearchItem should render the item 1`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-avatar-stub
alt="avatar"
@@ -14,33 +14,25 @@ exports[`SearchItem should render the item 1`] = `
size="16"
src="https://www.gravatar.com/avatar/a9638f4ec70148d51e56bf05ad41e993?s=80&d=identicon"
/>
-
- <!---->
-
<span
class="gl-display-flex gl-flex-direction-column"
>
<span
class="gl-text-gray-900"
/>
-
- <!---->
</span>
</div>
`;
exports[`SearchItem should render the item 2`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
- <!---->
-
<gl-icon-stub
class="gl-mr-3"
name="users"
size="16"
/>
-
<span
class="gl-display-flex gl-flex-direction-column"
>
@@ -49,15 +41,13 @@ exports[`SearchItem should render the item 2`] = `
>
Manage &gt; Activity
</span>
-
- <!---->
</span>
</div>
`;
exports[`SearchItem should render the item 3`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-avatar-stub
alt="avatar"
@@ -69,9 +59,6 @@ exports[`SearchItem should render the item 3`] = `
size="32"
src="/project/avatar/1/avatar.png"
/>
-
- <!---->
-
<span
class="gl-display-flex gl-flex-direction-column"
>
@@ -80,7 +67,6 @@ exports[`SearchItem should render the item 3`] = `
>
MockProject1
</span>
-
<span
class="gl-font-sm gl-text-gray-500"
>
@@ -92,7 +78,7 @@ exports[`SearchItem should render the item 3`] = `
exports[`SearchItem should render the item 4`] = `
<div
- class="gl-display-flex gl-align-items-center"
+ class="gl-align-items-center gl-display-flex"
>
<gl-avatar-stub
alt="avatar"
@@ -104,9 +90,6 @@ exports[`SearchItem should render the item 4`] = `
size="16"
src=""
/>
-
- <!---->
-
<span
class="gl-display-flex gl-flex-direction-column"
>
@@ -115,8 +98,6 @@ exports[`SearchItem should render the item 4`] = `
>
Dismiss Cipher with no integrity
</span>
-
- <!---->
</span>
</div>
`;
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
index 85eb7e2e241..7d85dbcbdd3 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
@@ -9,6 +9,7 @@ import {
PATH_GROUP_TITLE,
USER_HANDLE,
PATH_HANDLE,
+ PROJECT_HANDLE,
SEARCH_SCOPE,
MAX_ROWS,
} from '~/super_sidebar/components/global_search/command_palette/constants';
@@ -20,6 +21,7 @@ import {
import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { COMMANDS, LINKS, USERS, FILES } from './mock_data';
@@ -32,7 +34,7 @@ describe('CommandPaletteItems', () => {
const projectFilesPath = 'project/files/path';
const projectBlobPath = '/blob/main';
- const createComponent = (props) => {
+ const createComponent = (props, options = {}) => {
wrapper = shallowMount(CommandPaletteItems, {
propsData: {
handle: COMMAND_HANDLE,
@@ -51,6 +53,7 @@ describe('CommandPaletteItems', () => {
projectFilesPath,
projectBlobPath,
},
+ ...options,
});
};
@@ -227,4 +230,41 @@ describe('CommandPaletteItems', () => {
expect(axios.get).toHaveBeenCalledTimes(1);
});
});
+
+ describe('Tracking', () => {
+ let trackingSpy;
+ let mockAxios;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ mockAxios = new MockAdapter(axios);
+ createComponent({ attachTo: document.body });
+ });
+
+ afterEach(() => {
+ mockAxios.restore();
+ });
+
+ it('tracks event immediately', () => {
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_command_palette', {
+ label: 'command',
+ });
+ });
+
+ it.each`
+ handle | label
+ ${USER_HANDLE} | ${'user'}
+ ${PROJECT_HANDLE} | ${'project'}
+ ${PATH_HANDLE} | ${'path'}
+ `('tracks changing the handle to "$handle"', async ({ handle, label }) => {
+ trackingSpy.mockClear();
+
+ await wrapper.setProps({ handle });
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'activate_command_palette', {
+ label,
+ });
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
index d01e5c85741..25a23433b1e 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
@@ -69,24 +69,41 @@ export const TRANSFORMED_LINKS = [
icon: 'users',
keywords: 'Manage',
text: 'Manage',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'item_without_id',
+ 'data-track-extra': '{"title":"Manage"}',
+ },
},
{
href: '/flightjs/Flight/activity',
icon: 'users',
keywords: 'Activity',
text: 'Manage > Activity',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'activity',
+ },
},
{
href: '/flightjs/Flight/-/project_members',
icon: 'users',
keywords: 'Members',
text: 'Manage > Members',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'members',
+ },
},
{
href: '/flightjs/Flight/-/labels',
icon: 'users',
keywords: 'Labels',
text: 'Manage > Labels',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'labels',
+ },
},
];
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
index ebc52e2d910..76768bd8da9 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
@@ -26,6 +26,10 @@ describe('fileMapper', () => {
icon: 'doc-code',
text: file,
href: `${projectBlobPath}/${file}`,
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'file',
+ },
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
index c6126a348f5..f91c8034fe9 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_places_spec.js
@@ -67,10 +67,26 @@ describe('GlobalSearchDefaultPlaces', () => {
{
text: 'Explore',
href: '/explore',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-extra': '{"title":"Explore"}',
+ 'data-track-label': 'item_without_id',
+ 'data-track-property': 'nav_panel_unknown',
+ 'data-testid': 'places-item-link',
+ 'data-qa-places-item': 'Explore',
+ },
},
{
text: 'Admin area',
href: '/admin',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-extra': '{"title":"Admin area"}',
+ 'data-track-label': 'item_without_id',
+ 'data-track-property': 'nav_panel_unknown',
+ 'data-testid': 'places-item-link',
+ 'data-qa-places-item': 'Admin area',
+ },
},
]);
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index f9a6690a391..038c7a96adc 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -23,7 +23,6 @@ import {
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
} from '~/super_sidebar/components/global_search/constants';
-import { SEARCH_GITLAB } from '~/vue_shared/global_search/constants';
import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -52,7 +51,7 @@ describe('GlobalSearchModal', () => {
clearAutocomplete: jest.fn(),
};
- const deafaultMockState = {
+ const defaultMockState = {
searchContext: {
project: MOCK_PROJECT,
group: MOCK_GROUP,
@@ -66,15 +65,14 @@ describe('GlobalSearchModal', () => {
};
const createComponent = ({
- initialState = deafaultMockState,
+ initialState = defaultMockState,
mockGetters = defaultMockGetters,
stubs,
- glFeatures = { commandPalette: false },
...mountOptions
} = {}) => {
const store = new Vuex.Store({
state: {
- ...deafaultMockState,
+ ...defaultMockState,
...initialState,
},
actions: actionSpies,
@@ -89,7 +87,6 @@ describe('GlobalSearchModal', () => {
wrapper = shallowMountExtended(GlobalSearchModal, {
store,
stubs,
- provide: { glFeatures },
...mountOptions,
});
};
@@ -271,49 +268,28 @@ describe('GlobalSearchModal', () => {
});
describe('Command palette', () => {
- describe('when FF `command_palette` is disabled', () => {
+ describe.each([...COMMON_HANDLES, PATH_HANDLE])('when search handle is %s', (handle) => {
beforeEach(() => {
- createComponent();
+ createComponent({
+ initialState: { search: handle },
+ });
});
- it('should not render command mode components', () => {
- expect(findCommandPaletteItems().exists()).toBe(false);
- expect(findFakeSearchInput().exists()).toBe(false);
+ it('should render command mode components', () => {
+ expect(findCommandPaletteItems().exists()).toBe(true);
+ expect(findFakeSearchInput().exists()).toBe(true);
});
- it('should provide default placeholder to the search input', () => {
- expect(findGlobalSearchInput().attributes('placeholder')).toBe(SEARCH_GITLAB);
+ it('should provide an alternative placeholder to the search input', () => {
+ expect(findGlobalSearchInput().attributes('placeholder')).toBe(
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
+ );
});
- });
-
- describe.each([...COMMON_HANDLES, PATH_HANDLE])(
- 'when FF `command_palette` is enabled and search handle is %s',
- (handle) => {
- beforeEach(() => {
- createComponent({
- initialState: { search: handle },
- glFeatures: {
- commandPalette: true,
- },
- });
- });
- it('should render command mode components', () => {
- expect(findCommandPaletteItems().exists()).toBe(true);
- expect(findFakeSearchInput().exists()).toBe(true);
- });
-
- it('should provide an alternative placeholder to the search input', () => {
- expect(findGlobalSearchInput().attributes('placeholder')).toBe(
- SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
- );
- });
-
- it('should not render the scope token', () => {
- expect(findScopeToken().exists()).toBe(false);
- });
- },
- );
+ it('should not render the scope token', () => {
+ expect(findScopeToken().exists()).toBe(false);
+ });
+ });
});
});
@@ -373,9 +349,6 @@ describe('GlobalSearchModal', () => {
beforeEach(() => {
createComponent({
initialState: { search: '>' },
- glFeatures: {
- commandPalette: true,
- },
});
submitSearch();
});
diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js
index dfa8b458844..61ddfb6cae1 100644
--- a/spec/frontend/super_sidebar/components/global_search/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -109,6 +109,10 @@ export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
href: MOCK_PROJECT.path,
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'scoped_in_project',
+ },
},
{
text: 'scoped-in-group',
@@ -116,11 +120,19 @@ export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
scopeCategory: GROUPS_CATEGORY,
icon: ICON_GROUP,
href: MOCK_GROUP.path,
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'scoped_in_group',
+ },
},
{
text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
href: MOCK_ALL_PATH,
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'scoped_in_all',
+ },
},
];
export const MOCK_SCOPED_SEARCH_OPTIONS = [
@@ -263,6 +275,10 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
avatar_size: 32,
entity_id: 1,
entity_name: 'MockGroup1',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'groups',
+ },
},
],
},
@@ -281,6 +297,10 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
avatar_size: 32,
entity_id: 1,
entity_name: 'MockProject1',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'projects',
+ },
},
{
category: 'Projects',
@@ -294,6 +314,10 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
avatar_size: 32,
entity_id: 2,
entity_name: 'MockProject2',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'projects',
+ },
},
],
},
@@ -307,6 +331,10 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
href: 'help/gitlab',
avatar_size: 16,
entity_name: 'GitLab Help',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'help',
+ },
},
],
},
@@ -325,6 +353,10 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
avatar_size: 32,
entity_id: 1,
entity_name: 'MockGroup1',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'groups',
+ },
},
{
avatar_size: 32,
@@ -338,6 +370,10 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
namespace: 'Gitlab Org / MockProject1',
text: 'MockProject1',
value: 'MockProject1',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'projects',
+ },
},
{
avatar_size: 32,
@@ -351,6 +387,10 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
namespace: 'Gitlab Org / MockProject2',
text: 'MockProject2',
value: 'MockProject2',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'projects',
+ },
},
{
avatar_size: 16,
@@ -359,6 +399,10 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
label: 'GitLab Help',
text: 'GitLab Help',
href: 'help/gitlab',
+ extraAttrs: {
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': 'help',
+ },
},
];
diff --git a/spec/frontend/super_sidebar/components/global_search/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
index 3b12063e733..3c30445e936 100644
--- a/spec/frontend/super_sidebar/components/global_search/utils_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
@@ -13,48 +13,58 @@ import {
describe('getFormattedItem', () => {
describe.each`
- item | avatarSize | searchContext | entityId | entityName
- ${{ category: PROJECTS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 29 } }} | ${29} | ${'project1'}
- ${{ category: GROUPS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 12 } }} | ${12} | ${'project1'}
- ${{ category: 'Help', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'}
- ${{ category: 'Settings', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'}
- ${{ category: GROUPS_CATEGORY, value: 'group1', label: 'Group 1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${1} | ${'group1'}
- ${{ category: PROJECTS_CATEGORY, value: 'group2', label: 'Group2' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${2} | ${'group2'}
- ${{ category: ISSUES_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${3} | ${'test3'}
- ${{ category: MERGE_REQUEST_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${4} | ${'test4'}
- ${{ category: RECENT_EPICS_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${5} | ${'test5'}
- ${{ category: GROUPS_CATEGORY, group_id: 6, group_name: 'test6' }} | ${LARGE_AVATAR_PX} | ${null} | ${6} | ${'test6'}
- ${{ category: PROJECTS_CATEGORY, project_id: 7, project_name: 'test7' }} | ${LARGE_AVATAR_PX} | ${null} | ${7} | ${'test7'}
- ${{ category: ISSUES_CATEGORY, project_id: 8, project_name: 'test8' }} | ${SMALL_AVATAR_PX} | ${null} | ${8} | ${'test8'}
- ${{ category: MERGE_REQUEST_CATEGORY, project_id: 9, project_name: 'test9' }} | ${SMALL_AVATAR_PX} | ${null} | ${9} | ${'test9'}
- ${{ category: RECENT_EPICS_CATEGORY, group_id: 10, group_name: 'test10' }} | ${SMALL_AVATAR_PX} | ${null} | ${10} | ${'test10'}
- ${{ category: GROUPS_CATEGORY, group_id: 11, group_name: 'test11' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${11} | ${'test11'}
- ${{ category: PROJECTS_CATEGORY, project_id: 12, project_name: 'test12' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${12} | ${'test12'}
- ${{ category: ISSUES_CATEGORY, project_id: 13, project_name: 'test13' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${13} | ${'test13'}
- ${{ category: MERGE_REQUEST_CATEGORY, project_id: 14, project_name: 'test14' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${14} | ${'test14'}
- ${{ category: RECENT_EPICS_CATEGORY, group_id: 15, group_name: 'test15' }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${15} | ${'test15'}
- `('formats the item', ({ item, avatarSize, searchContext, entityId, entityName }) => {
- describe(`when item is ${JSON.stringify(item)}`, () => {
- let formattedItem;
- beforeEach(() => {
- formattedItem = getFormattedItem(item, searchContext);
- });
+ item | avatarSize | searchContext | entityId | entityName | trackingLabel
+ ${{ category: PROJECTS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 29 } }} | ${29} | ${'project1'} | ${'projects'}
+ ${{ category: GROUPS_CATEGORY, label: 'project1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 12 } }} | ${12} | ${'project1'} | ${'groups'}
+ ${{ category: 'Help', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'} | ${'help'}
+ ${{ category: 'Settings', label: 'project1' }} | ${SMALL_AVATAR_PX} | ${null} | ${undefined} | ${'project1'} | ${'settings'}
+ ${{ category: GROUPS_CATEGORY, value: 'group1', label: 'Group 1' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${1} | ${'group1'} | ${'groups'}
+ ${{ category: PROJECTS_CATEGORY, value: 'group2', label: 'Group2' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${2} | ${'group2'} | ${'projects'}
+ ${{ category: ISSUES_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${3} | ${'test3'} | ${'recent_issues'}
+ ${{ category: MERGE_REQUEST_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${4} | ${'test4'} | ${'recent_merge_requests'}
+ ${{ category: RECENT_EPICS_CATEGORY }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${5} | ${'test5'} | ${'recent_epics'}
+ ${{ category: GROUPS_CATEGORY, group_id: 6, group_name: 'test6' }} | ${LARGE_AVATAR_PX} | ${null} | ${6} | ${'test6'} | ${'groups'}
+ ${{ category: PROJECTS_CATEGORY, project_id: 7, project_name: 'test7' }} | ${LARGE_AVATAR_PX} | ${null} | ${7} | ${'test7'} | ${'projects'}
+ ${{ category: ISSUES_CATEGORY, project_id: 8, project_name: 'test8' }} | ${SMALL_AVATAR_PX} | ${null} | ${8} | ${'test8'} | ${'recent_issues'}
+ ${{ category: MERGE_REQUEST_CATEGORY, project_id: 9, project_name: 'test9' }} | ${SMALL_AVATAR_PX} | ${null} | ${9} | ${'test9'} | ${'recent_merge_requests'}
+ ${{ category: RECENT_EPICS_CATEGORY, group_id: 10, group_name: 'test10' }} | ${SMALL_AVATAR_PX} | ${null} | ${10} | ${'test10'} | ${'recent_epics'}
+ ${{ category: GROUPS_CATEGORY, group_id: 11, group_name: 'test11' }} | ${LARGE_AVATAR_PX} | ${{ group: { id: 1, name: 'test1' } }} | ${11} | ${'test11'} | ${'groups'}
+ ${{ category: PROJECTS_CATEGORY, project_id: 12, project_name: 'test12' }} | ${LARGE_AVATAR_PX} | ${{ project: { id: 2, name: 'test2' } }} | ${12} | ${'test12'} | ${'projects'}
+ ${{ category: ISSUES_CATEGORY, project_id: 13, project_name: 'test13' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 3, name: 'test3' } }} | ${13} | ${'test13'} | ${'recent_issues'}
+ ${{ category: MERGE_REQUEST_CATEGORY, project_id: 14, project_name: 'test14' }} | ${SMALL_AVATAR_PX} | ${{ project: { id: 4, name: 'test4' } }} | ${14} | ${'test14'} | ${'recent_merge_requests'}
+ ${{ category: RECENT_EPICS_CATEGORY, group_id: 15, group_name: 'test15' }} | ${SMALL_AVATAR_PX} | ${{ group: { id: 5, name: 'test5' } }} | ${15} | ${'test15'} | ${'recent_epics'}
+ `(
+ 'formats the item',
+ ({ item, avatarSize, searchContext, entityId, entityName, trackingLabel }) => {
+ describe(`when item is ${JSON.stringify(item)}`, () => {
+ let formattedItem;
+ beforeEach(() => {
+ formattedItem = getFormattedItem(item, searchContext);
+ });
- it(`should set text to ${item.value || item.label}`, () => {
- expect(formattedItem.text).toBe(item.value || item.label);
- });
+ it(`should set text to ${item.value || item.label}`, () => {
+ expect(formattedItem.text).toBe(item.value || item.label);
+ });
- it(`should set avatarSize to ${avatarSize}`, () => {
- expect(formattedItem.avatar_size).toBe(avatarSize);
- });
+ it(`should set avatarSize to ${avatarSize}`, () => {
+ expect(formattedItem.avatar_size).toBe(avatarSize);
+ });
- it(`should set avatar entityId to ${entityId}`, () => {
- expect(formattedItem.entity_id).toBe(entityId);
- });
+ it(`should set avatar entityId to ${entityId}`, () => {
+ expect(formattedItem.entity_id).toBe(entityId);
+ });
+
+ it(`should set avatar entityName to ${entityName}`, () => {
+ expect(formattedItem.entity_name).toBe(entityName);
+ });
- it(`should set avatar entityName to ${entityName}`, () => {
- expect(formattedItem.entity_name).toBe(entityName);
+ it('should add tracking label', () => {
+ expect(formattedItem.extraAttrs).toEqual({
+ 'data-track-action': 'click_command_palette_item',
+ 'data-track-label': trackingLabel,
+ });
+ });
});
- });
- });
+ },
+ );
});
diff --git a/spec/frontend/super_sidebar/components/groups_list_spec.js b/spec/frontend/super_sidebar/components/groups_list_spec.js
deleted file mode 100644
index 4fa3303c12f..00000000000
--- a/spec/frontend/super_sidebar/components/groups_list_spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { s__ } from '~/locale';
-import GroupsList from '~/super_sidebar/components/groups_list.vue';
-import SearchResults from '~/super_sidebar/components/search_results.vue';
-import FrequentItemsList from '~/super_sidebar/components/frequent_items_list.vue';
-import NavItem from '~/super_sidebar/components/nav_item.vue';
-import { MAX_FREQUENT_GROUPS_COUNT } from '~/super_sidebar/constants';
-
-const username = 'root';
-const viewAllLink = '/path/to/groups';
-const storageKey = `${username}/frequent-groups`;
-
-describe('GroupsList component', () => {
- let wrapper;
-
- const findSearchResults = () => wrapper.findComponent(SearchResults);
- const findFrequentItemsList = () => wrapper.findComponent(FrequentItemsList);
- const findViewAllLink = () => wrapper.findComponent(NavItem);
-
- const itRendersViewAllItem = () => {
- it('renders the "View all..." item', () => {
- const link = findViewAllLink();
-
- expect(link.props('item')).toEqual({
- icon: 'group',
- link: viewAllLink,
- title: s__('Navigation|View all your groups'),
- });
- expect(link.props('linkClasses')).toEqual({ 'dashboard-shortcuts-groups': true });
- });
- };
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMountExtended(GroupsList, {
- propsData: {
- username,
- viewAllLink,
- ...props,
- },
- });
- };
-
- describe('when displaying search results', () => {
- const searchResults = ['A search result'];
-
- beforeEach(() => {
- createWrapper({
- isSearch: true,
- searchResults,
- });
- });
-
- it('renders the search results component', () => {
- expect(findSearchResults().exists()).toBe(true);
- expect(findFrequentItemsList().exists()).toBe(false);
- });
-
- it('passes the correct props to the search results component', () => {
- expect(findSearchResults().props()).toEqual({
- title: s__('Navigation|Groups'),
- noResultsText: s__('Navigation|No group matches found'),
- searchResults,
- });
- });
-
- itRendersViewAllItem();
- });
-
- describe('when displaying frequent groups', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders the frequent items list', () => {
- expect(findFrequentItemsList().exists()).toBe(true);
- expect(findSearchResults().exists()).toBe(false);
- });
-
- it('passes the correct props to the frequent items list', () => {
- expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequently visited groups'),
- storageKey,
- maxItems: MAX_FREQUENT_GROUPS_COUNT,
- pristineText: s__('Navigation|Groups you visit often will appear here.'),
- });
- });
-
- itRendersViewAllItem();
- });
-});
diff --git a/spec/frontend/super_sidebar/components/items_list_spec.js b/spec/frontend/super_sidebar/components/items_list_spec.js
deleted file mode 100644
index 8e00984f500..00000000000
--- a/spec/frontend/super_sidebar/components/items_list_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ItemsList from '~/super_sidebar/components/items_list.vue';
-import NavItem from '~/super_sidebar/components/nav_item.vue';
-import { cachedFrequentProjects } from '../mock_data';
-
-const mockItems = JSON.parse(cachedFrequentProjects);
-const [firstMockedProject] = mockItems;
-
-describe('ItemsList component', () => {
- let wrapper;
-
- const findNavItems = () => wrapper.findAllComponents(NavItem);
-
- const createWrapper = ({ props = {}, slots = {} } = {}) => {
- wrapper = shallowMountExtended(ItemsList, {
- propsData: {
- ...props,
- },
- slots,
- });
- };
-
- it('does not render nav items when there are no items', () => {
- createWrapper();
-
- expect(findNavItems().length).toBe(0);
- });
-
- it('renders one nav item per item', () => {
- createWrapper({
- props: {
- items: mockItems,
- },
- });
-
- expect(findNavItems().length).not.toBe(0);
- expect(findNavItems().length).toBe(mockItems.length);
- });
-
- it('passes the correct props to the nav items', () => {
- createWrapper({
- props: {
- items: mockItems,
- },
- });
- const firstNavItem = findNavItems().at(0);
-
- expect(firstNavItem.props('item')).toEqual(firstMockedProject);
- });
-
- it('renders the `view-all-items` slot', () => {
- const testId = 'view-all-items';
- createWrapper({
- slots: {
- 'view-all-items': {
- template: `<div data-testid="${testId}" />`,
- },
- },
- });
-
- expect(wrapper.findByTestId(testId).exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/super_sidebar/components/menu_section_spec.js b/spec/frontend/super_sidebar/components/menu_section_spec.js
index 288e317d4c6..e76bb699301 100644
--- a/spec/frontend/super_sidebar/components/menu_section_spec.js
+++ b/spec/frontend/super_sidebar/components/menu_section_spec.js
@@ -79,39 +79,55 @@ describe('MenuSection component', () => {
});
describe('when hasFlyout is true', () => {
- it('is rendered', () => {
+ it('is not yet rendered', () => {
createWrapper({ title: 'Asdf' }, { 'has-flyout': true });
- expect(findFlyout().exists()).toBe(true);
+ expect(findFlyout().exists()).toBe(false);
});
describe('on mouse hover', () => {
describe('when section is expanded', () => {
- it('is not shown', async () => {
+ it('is not rendered', async () => {
createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: true });
await findButton().trigger('pointerover', { pointerType: 'mouse' });
- expect(findFlyout().isVisible()).toBe(false);
+ expect(findFlyout().exists()).toBe(false);
});
});
describe('when section is not expanded', () => {
- it('is shown', async () => {
- createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false });
- await findButton().trigger('pointerover', { pointerType: 'mouse' });
- expect(findFlyout().isVisible()).toBe(true);
+ describe('when section has no items', () => {
+ it('is not rendered', async () => {
+ createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false });
+ await findButton().trigger('pointerover', { pointerType: 'mouse' });
+ expect(findFlyout().exists()).toBe(false);
+ });
+ });
+
+ describe('when section has items', () => {
+ it('is rendered and shown', async () => {
+ createWrapper(
+ { title: 'Asdf', items: [{ title: 'Item1', href: '/item1' }] },
+ { 'has-flyout': true, expanded: false },
+ );
+ await findButton().trigger('pointerover', { pointerType: 'mouse' });
+ expect(findFlyout().isVisible()).toBe(true);
+ });
});
});
});
describe('when section gets closed', () => {
beforeEach(async () => {
- createWrapper({ title: 'Asdf' }, { expanded: true, 'has-flyout': true });
+ createWrapper(
+ { title: 'Asdf', items: [{ title: 'Item1', href: '/item1' }] },
+ { expanded: true, 'has-flyout': true },
+ );
await findButton().trigger('click');
await findButton().trigger('pointerover', { pointerType: 'mouse' });
});
it('shows the flyout only after section title gets hovered out and in again', async () => {
expect(findCollapse().props('visible')).toBe(false);
- expect(findFlyout().isVisible()).toBe(false);
+ expect(findFlyout().exists()).toBe(false);
await findButton().trigger('pointerleave');
await findButton().trigger('pointerover', { pointerType: 'mouse' });
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
index f41f6954ed1..89d774c4b43 100644
--- a/spec/frontend/super_sidebar/components/nav_item_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -1,5 +1,6 @@
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlButton, GlAvatar } from '@gitlab/ui';
import { RouterLinkStub } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import NavItemRouterLink from '~/super_sidebar/components/nav_item_router_link.vue';
@@ -13,8 +14,10 @@ import {
describe('NavItem component', () => {
let wrapper;
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
const findLink = () => wrapper.findByTestId('nav-item-link');
const findPill = () => wrapper.findComponent(GlBadge);
+ const findPinButton = () => wrapper.findComponent(GlButton);
const findNavItemRouterLink = () => extendedWrapper(wrapper.findComponent(NavItemRouterLink));
const findNavItemLink = () => extendedWrapper(wrapper.findComponent(NavItemLink));
@@ -59,6 +62,66 @@ describe('NavItem component', () => {
);
});
+ describe('pins', () => {
+ describe('when pins are not supported', () => {
+ it('does not render pin button', () => {
+ createWrapper({
+ item: { title: 'Foo' },
+ provide: {
+ panelSupportsPins: false,
+ },
+ });
+
+ expect(findPinButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when pins are supported', () => {
+ beforeEach(() => {
+ createWrapper({
+ item: { title: 'Foo' },
+ provide: {
+ panelSupportsPins: true,
+ },
+ });
+ });
+
+ it('renders pin button', () => {
+ expect(findPinButton().exists()).toBe(true);
+ });
+
+ it('contains an aria-label', () => {
+ expect(findPinButton().attributes('aria-label')).toBe('Pin Foo');
+ });
+
+ it('toggles pointer events on after CSS fade-in', async () => {
+ const pinButton = findPinButton();
+
+ expect(pinButton.classes()).toContain('gl-pointer-events-none');
+
+ wrapper.trigger('mouseenter');
+ pinButton.vm.$emit('transitionend');
+ await nextTick();
+
+ expect(pinButton.classes()).not.toContain('gl-pointer-events-none');
+ });
+
+ it('does not toggle pointer events if mouse leaves before CSS fade-in ends', async () => {
+ const pinButton = findPinButton();
+
+ expect(pinButton.classes()).toContain('gl-pointer-events-none');
+
+ wrapper.trigger('mouseenter');
+ wrapper.trigger('mousemove');
+ wrapper.trigger('mouseleave');
+ pinButton.vm.$emit('transitionend');
+ await nextTick();
+
+ expect(pinButton.classes()).toContain('gl-pointer-events-none');
+ });
+ });
+ });
+
it('applies custom link classes', () => {
const customClass = 'customClass';
createWrapper({
@@ -153,4 +216,36 @@ describe('NavItem component', () => {
});
});
});
+
+ describe('when `item` prop has `entity_id` attribute', () => {
+ it('renders an avatar', () => {
+ createWrapper({
+ item: { title: 'Foo', entity_id: 123, avatar: '/avatar.png', avatar_shape: 'circle' },
+ });
+
+ expect(findAvatar().props()).toMatchObject({
+ entityId: 123,
+ shape: 'circle',
+ src: '/avatar.png',
+ });
+ });
+ });
+
+ describe('when `item.is_active` is true', () => {
+ it('scrolls into view', () => {
+ createWrapper({
+ item: { is_active: true },
+ });
+ expect(wrapper.element.scrollIntoView).toHaveBeenNthCalledWith(1, false);
+ });
+ });
+
+ describe('when `item.is_active` is false', () => {
+ it('scrolls not into view', () => {
+ createWrapper({
+ item: { is_active: false },
+ });
+ expect(wrapper.element.scrollIntoView).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/pinned_section_spec.js b/spec/frontend/super_sidebar/components/pinned_section_spec.js
index 00cc7cf29c9..fe1653f1177 100644
--- a/spec/frontend/super_sidebar/components/pinned_section_spec.js
+++ b/spec/frontend/super_sidebar/components/pinned_section_spec.js
@@ -87,4 +87,33 @@ describe('PinnedSection component', () => {
});
});
});
+
+ describe('ambiguous settings names', () => {
+ it('get renamed to be unambiguous', () => {
+ createWrapper({
+ items: [
+ { title: 'CI/CD', id: 'ci_cd' },
+ { title: 'Merge requests', id: 'merge_request_settings' },
+ { title: 'Monitor', id: 'monitor' },
+ { title: 'Repository', id: 'repository' },
+ { title: 'Repository', id: 'code' },
+ { title: 'Something else', id: 'not_a_setting' },
+ ],
+ });
+
+ expect(
+ wrapper
+ .findComponent(MenuSection)
+ .props('item')
+ .items.map((i) => i.title),
+ ).toEqual([
+ 'CI/CD settings',
+ 'Merge requests settings',
+ 'Monitor settings',
+ 'Repository settings',
+ 'Repository',
+ 'Something else',
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/projects_list_spec.js b/spec/frontend/super_sidebar/components/projects_list_spec.js
deleted file mode 100644
index 93a414e1e8c..00000000000
--- a/spec/frontend/super_sidebar/components/projects_list_spec.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { s__ } from '~/locale';
-import ProjectsList from '~/super_sidebar/components/projects_list.vue';
-import SearchResults from '~/super_sidebar/components/search_results.vue';
-import FrequentItemsList from '~/super_sidebar/components/frequent_items_list.vue';
-import NavItem from '~/super_sidebar/components/nav_item.vue';
-import { MAX_FREQUENT_PROJECTS_COUNT } from '~/super_sidebar/constants';
-
-const username = 'root';
-const viewAllLink = '/path/to/projects';
-const storageKey = `${username}/frequent-projects`;
-
-describe('ProjectsList component', () => {
- let wrapper;
-
- const findSearchResults = () => wrapper.findComponent(SearchResults);
- const findFrequentItemsList = () => wrapper.findComponent(FrequentItemsList);
- const findViewAllLink = () => wrapper.findComponent(NavItem);
-
- const itRendersViewAllItem = () => {
- it('renders the "View all..." item', () => {
- const link = findViewAllLink();
-
- expect(link.props('item')).toEqual({
- icon: 'project',
- link: viewAllLink,
- title: s__('Navigation|View all your projects'),
- });
- expect(link.props('linkClasses')).toEqual({ 'dashboard-shortcuts-projects': true });
- });
- };
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMountExtended(ProjectsList, {
- propsData: {
- username,
- viewAllLink,
- ...props,
- },
- });
- };
-
- describe('when displaying search results', () => {
- const searchResults = ['A search result'];
-
- beforeEach(() => {
- createWrapper({
- isSearch: true,
- searchResults,
- });
- });
-
- it('renders the search results component', () => {
- expect(findSearchResults().exists()).toBe(true);
- expect(findFrequentItemsList().exists()).toBe(false);
- });
-
- it('passes the correct props to the search results component', () => {
- expect(findSearchResults().props()).toEqual({
- title: s__('Navigation|Projects'),
- noResultsText: s__('Navigation|No project matches found'),
- searchResults,
- });
- });
-
- itRendersViewAllItem();
- });
-
- describe('when displaying frequent projects', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('passes the correct props to the frequent items list', () => {
- expect(findFrequentItemsList().props()).toEqual({
- title: s__('Navigation|Frequently visited projects'),
- storageKey,
- maxItems: MAX_FREQUENT_PROJECTS_COUNT,
- pristineText: s__('Navigation|Projects you visit often will appear here.'),
- });
- });
-
- itRendersViewAllItem();
- });
-});
diff --git a/spec/frontend/super_sidebar/components/search_results_spec.js b/spec/frontend/super_sidebar/components/search_results_spec.js
deleted file mode 100644
index daec5c2a9b4..00000000000
--- a/spec/frontend/super_sidebar/components/search_results_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { GlCollapse } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { s__ } from '~/locale';
-import SearchResults from '~/super_sidebar/components/search_results.vue';
-import ItemsList from '~/super_sidebar/components/items_list.vue';
-import { stubComponent } from 'helpers/stub_component';
-
-const title = s__('Navigation|PROJECTS');
-const noResultsText = s__('Navigation|No project matches found');
-
-describe('SearchResults component', () => {
- let wrapper;
-
- const findSearchResultsToggle = () => wrapper.findByTestId('search-results-toggle');
- const findCollapsibleSection = () => wrapper.findComponent(GlCollapse);
- const findItemsList = () => wrapper.findComponent(ItemsList);
- const findEmptyText = () => wrapper.findByTestId('empty-text');
-
- const createWrapper = ({ props = {} } = {}) => {
- wrapper = shallowMountExtended(SearchResults, {
- propsData: {
- title,
- noResultsText,
- ...props,
- },
- stubs: {
- GlCollapse: stubComponent(GlCollapse, {
- props: ['visible'],
- }),
- },
- });
- };
-
- describe('default state', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it("renders the list's title", () => {
- expect(findSearchResultsToggle().text()).toBe(title);
- });
-
- it('is expanded', () => {
- expect(findCollapsibleSection().props('visible')).toBe(true);
- });
-
- it('renders the empty text', () => {
- expect(findEmptyText().exists()).toBe(true);
- expect(findEmptyText().text()).toBe(noResultsText);
- });
- });
-
- describe('when displaying search results', () => {
- it('shows search results', () => {
- const searchResults = [{ id: 1 }];
- createWrapper({ props: { isSearch: true, searchResults } });
-
- expect(findItemsList().props('items')[0]).toEqual(searchResults[0]);
- });
-
- it('shows the no results text if search results are empty', () => {
- const searchResults = [];
- createWrapper({ props: { isSearch: true, searchResults } });
-
- expect(findItemsList().props('items').length).toEqual(0);
- expect(findEmptyText().text()).toBe(noResultsText);
- });
- });
-});
diff --git a/spec/frontend/super_sidebar/components/sidebar_hover_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_hover_peek_behavior_spec.js
new file mode 100644
index 00000000000..75b834ee7c9
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/sidebar_hover_peek_behavior_spec.js
@@ -0,0 +1,213 @@
+import { mount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import {
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+ JS_TOGGLE_EXPAND_CLASS,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
+} from '~/super_sidebar/constants';
+import SidebarHoverPeek from '~/super_sidebar/components/sidebar_hover_peek_behavior.vue';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { moveMouse, mouseEnter, mouseLeave, moveMouseOutOfDocument } from '../mocks';
+
+// This is measured at runtime in the browser, but statically defined here
+// since Jest does not do layout/styling.
+const X_SIDEBAR_EDGE = 10;
+
+jest.mock('~/lib/utils/css_utils', () => ({
+ getCssClassDimensions: () => ({ width: X_SIDEBAR_EDGE }),
+}));
+
+describe('SidebarHoverPeek component', () => {
+ let wrapper;
+ let toggle;
+ let trackingSpy = null;
+
+ const createComponent = (props = { isMouseOverSidebar: false }) => {
+ wrapper = mount(SidebarHoverPeek, {
+ propsData: props,
+ });
+
+ return nextTick();
+ };
+
+ const lastNChangeEvents = (n = 1) => wrapper.emitted('change').slice(-n).flat();
+
+ beforeEach(() => {
+ toggle = document.createElement('button');
+ toggle.classList.add(JS_TOGGLE_EXPAND_CLASS);
+ document.body.appendChild(toggle);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ // We destroy the wrapper ourselves as that needs to happen before the toggle is removed.
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
+ toggle?.remove();
+ });
+
+ it('begins in the closed state', async () => {
+ await createComponent();
+
+ expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED]);
+ });
+
+ describe('when mouse enters the toggle', () => {
+ beforeEach(async () => {
+ await createComponent();
+ mouseEnter(toggle);
+ });
+
+ it('does not emit duplicate events in a region', () => {
+ mouseEnter(toggle);
+
+ expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED, STATE_WILL_OPEN]);
+ });
+
+ it('transitions to will-open when hovering the toggle', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+ });
+
+ describe('when transitioning away from the will-open state', () => {
+ beforeEach(() => {
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1);
+ });
+
+ it('transitions to open after delay', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+
+ jest.advanceTimersByTime(1);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hover_peek', {
+ label: 'nav_sidebar_toggle',
+ property: 'nav_sidebar',
+ });
+ });
+
+ it('cancels transition to open if mouse out of toggle', () => {
+ mouseLeave(toggle);
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(3)).toEqual([STATE_WILL_OPEN, STATE_WILL_CLOSE, STATE_CLOSED]);
+ });
+
+ it('transitions to closed if cursor leaves document', () => {
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_CLOSED]);
+ });
+ });
+
+ describe('when transitioning away from the will-close state', () => {
+ beforeEach(() => {
+ jest.runOnlyPendingTimers();
+ moveMouse(X_SIDEBAR_EDGE);
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1);
+ });
+
+ it('transitions to closed after delay', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]);
+
+ jest.advanceTimersByTime(1);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_CLOSE, STATE_CLOSED]);
+ });
+
+ it('cancels transition to close if mouse moves back to toggle', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]);
+
+ mouseEnter(toggle);
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(4)).toEqual([
+ STATE_OPEN,
+ STATE_WILL_CLOSE,
+ STATE_WILL_OPEN,
+ STATE_OPEN,
+ ]);
+ });
+ });
+
+ describe('when transitioning away from the open state', () => {
+ beforeEach(() => {
+ jest.runOnlyPendingTimers();
+ });
+
+ it('transitions to will-close if mouse out of sidebar region', () => {
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+
+ moveMouse(X_SIDEBAR_EDGE);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]);
+ });
+
+ it('transitions to will-close if cursor leaves document', () => {
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]);
+ });
+ });
+
+ it('cleans up its mouseleave listener before destroy', () => {
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+
+ wrapper.destroy();
+ mouseLeave(toggle);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+ });
+
+ it('cleans up its timers before destroy', () => {
+ wrapper.destroy();
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+ });
+
+ it('cleans up document mouseleave listener before destroy', () => {
+ mouseEnter(toggle);
+
+ wrapper.destroy();
+
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(1)).not.toEqual([STATE_CLOSED]);
+ });
+ });
+
+ describe('when mouse is over sidebar child element', () => {
+ beforeEach(async () => {
+ await createComponent({ isMouseOverSidebar: true });
+ });
+
+ it('does not transition to will-close or closed when mouse is over sidebar child element', () => {
+ mouseEnter(toggle);
+ jest.runOnlyPendingTimers();
+ mouseLeave(toggle);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+ });
+ });
+
+ it('cleans up its mouseenter listener before destroy', async () => {
+ await createComponent();
+
+ mouseLeave(toggle);
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_CLOSED]);
+
+ wrapper.destroy();
+ mouseEnter(toggle);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_CLOSED]);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
index 5d9a35fbf70..c85a6609e6f 100644
--- a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
@@ -16,13 +16,8 @@ const menuItems = [
describe('Sidebar Menu', () => {
let wrapper;
- let flyoutFlag = false;
-
const createWrapper = (extraProps = {}) => {
wrapper = shallowMountExtended(SidebarMenu, {
- provide: {
- glFeatures: { superSidebarFlyoutMenus: flyoutFlag },
- },
propsData: {
items: sidebarData.current_menu_items,
isLoggedIn: sidebarData.is_logged_in,
@@ -125,8 +120,11 @@ describe('Sidebar Menu', () => {
});
describe('flyout menus', () => {
- describe('when feature is disabled', () => {
+ describe('when screen width is smaller than "md" breakpoint', () => {
beforeEach(() => {
+ jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
+ return 767;
+ });
createWrapper({
items: menuItems,
});
@@ -140,59 +138,27 @@ describe('Sidebar Menu', () => {
});
});
- describe('when feature is enabled', () => {
+ describe('when screen width is equal or larger than "md" breakpoint', () => {
beforeEach(() => {
- flyoutFlag = true;
- });
-
- describe('when screen width is smaller than "md" breakpoint', () => {
- beforeEach(() => {
- jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
- return 767;
- });
- createWrapper({
- items: menuItems,
- });
+ jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
+ return 768;
});
-
- it('does not add flyout menus to sections', () => {
- expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
- false,
- false,
- ]);
+ createWrapper({
+ items: menuItems,
});
});
- describe('when screen width is equal or larger than "md" breakpoint', () => {
- beforeEach(() => {
- jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
- return 768;
- });
- createWrapper({
- items: menuItems,
- });
- });
-
- it('adds flyout menus to sections', () => {
- expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
- true,
- true,
- ]);
- });
+ it('adds flyout menus to sections', () => {
+ expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
+ true,
+ true,
+ ]);
});
});
});
});
describe('Separators', () => {
- it('should add the separator above pinned section', () => {
- createWrapper({
- items: menuItems,
- panelType: 'project',
- });
- expect(findPinnedSection().props('separated')).toBe(true);
- });
-
it('should add the separator above main menu items when there is a pinned section', () => {
createWrapper({
items: menuItems,
@@ -209,11 +175,4 @@ describe('Sidebar Menu', () => {
expect(findMainMenuSeparator().exists()).toBe(false);
});
});
-
- describe('ARIA attributes', () => {
- it('adds aria-label attribute to nav element', () => {
- createWrapper();
- expect(wrapper.find('nav').attributes('aria-label')).toBe('Main navigation');
- });
- });
});
diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
index 94ef072a951..90a950c5f35 100644
--- a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
@@ -2,14 +2,14 @@ import { mount } from '@vue/test-utils';
import {
SUPER_SIDEBAR_PEEK_OPEN_DELAY,
SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
} from '~/super_sidebar/constants';
-import SidebarPeek, {
- STATE_CLOSED,
- STATE_WILL_OPEN,
- STATE_OPEN,
- STATE_WILL_CLOSE,
-} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import SidebarPeek from '~/super_sidebar/components/sidebar_peek_behavior.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { moveMouse, moveMouseOutOfDocument } from '../mocks';
// These are measured at runtime in the browser, but statically defined here
// since Jest does not do layout/styling.
@@ -41,19 +41,6 @@ describe('SidebarPeek component', () => {
});
};
- const moveMouse = (clientX) => {
- const event = new MouseEvent('mousemove', {
- clientX,
- });
-
- document.dispatchEvent(event);
- };
-
- const moveMouseOutOfDocument = () => {
- const event = new MouseEvent('mouseleave');
- document.documentElement.dispatchEvent(event);
- };
-
const lastNChangeEvents = (n = 1) => wrapper.emitted('change').slice(-n).flat();
beforeEach(() => {
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 7b7b8a7be13..1371f8f00a7 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -4,29 +4,32 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
import HelpCenter from '~/super_sidebar/components/help_center.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
-import SidebarPeekBehavior, {
- STATE_CLOSED,
- STATE_WILL_OPEN,
- STATE_OPEN,
- STATE_WILL_CLOSE,
-} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import SidebarPeekBehavior from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import SidebarHoverPeekBehavior from '~/super_sidebar/components/sidebar_hover_peek_behavior.vue';
import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
-import ContextHeader from '~/super_sidebar/components/context_header.vue';
-import ContextSwitcher from '~/super_sidebar/components/context_switcher.vue';
import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
-import { sidebarState } from '~/super_sidebar/constants';
+import {
+ sidebarState,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
+} from '~/super_sidebar/constants';
import {
toggleSuperSidebarCollapsed,
isCollapsed,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
-import { stubComponent } from 'helpers/stub_component';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { trackContextAccess } from '~/super_sidebar/utils';
import { sidebarData as mockSidebarData, loggedOutSidebarData } from '../mock_data';
const initialSidebarState = { ...sidebarState };
jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager');
-const closeContextSwitcherMock = jest.fn();
+jest.mock('~/super_sidebar/utils', () => ({
+ ...jest.requireActual('~/super_sidebar/utils'),
+ trackContextAccess: jest.fn(),
+}));
const trialStatusWidgetStubTestId = 'trial-status-widget';
const TrialStatusWidgetStub = { template: `<div data-testid="${trialStatusWidgetStubTestId}" />` };
@@ -36,6 +39,7 @@ const TrialStatusPopoverStub = {
};
const peekClass = 'super-sidebar-peek';
+const hasPeekedClass = 'super-sidebar-has-peeked';
const peekHintClass = 'super-sidebar-peek-hint';
describe('SuperSidebar component', () => {
@@ -43,12 +47,11 @@ describe('SuperSidebar component', () => {
const findSidebar = () => wrapper.findByTestId('super-sidebar');
const findUserBar = () => wrapper.findComponent(UserBar);
- const findContextHeader = () => wrapper.findComponent(ContextHeader);
- const findContextSwitcher = () => wrapper.findComponent(ContextSwitcher);
const findNavContainer = () => wrapper.findByTestId('nav-container');
const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget);
const findPeekBehavior = () => wrapper.findComponent(SidebarPeekBehavior);
+ const findHoverPeekBehavior = () => wrapper.findComponent(SidebarHoverPeekBehavior);
const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId);
const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId);
const findSidebarMenu = () => wrapper.findComponent(SidebarMenu);
@@ -70,9 +73,6 @@ describe('SuperSidebar component', () => {
sidebarData,
},
stubs: {
- ContextSwitcher: stubComponent(ContextSwitcher, {
- methods: { close: closeContextSwitcherMock },
- }),
TrialStatusWidget: TrialStatusWidgetStub,
TrialStatusPopover: TrialStatusPopoverStub,
},
@@ -128,12 +128,6 @@ describe('SuperSidebar component', () => {
expect(findSidebarPortalTarget().exists()).toBe(true);
});
- it("does not call the context switcher's close method initially", () => {
- createWrapper();
-
- expect(closeContextSwitcherMock).not.toHaveBeenCalled();
- });
-
it('renders hidden shortcut links', () => {
createWrapper();
const [linkAttrs] = mockSidebarData.shortcut_links;
@@ -181,21 +175,43 @@ describe('SuperSidebar component', () => {
expect(findTrialStatusPopover().exists()).toBe(false);
});
- it('does not have peek behavior', () => {
+ it('does not have peek behaviors', () => {
createWrapper();
expect(findPeekBehavior().exists()).toBe(false);
+ expect(findHoverPeekBehavior().exists()).toBe(false);
});
- });
- describe('on collapse', () => {
- beforeEach(() => {
+ it('renders the context header', () => {
createWrapper();
- sidebarState.isCollapsed = true;
+
+ expect(wrapper.text()).toContain('Your work');
});
- it('closes the context switcher', () => {
- expect(closeContextSwitcherMock).toHaveBeenCalled();
+ describe('item access tracking', () => {
+ it('does not track anything if logged out', () => {
+ createWrapper({ sidebarData: loggedOutSidebarData });
+
+ expect(trackContextAccess).not.toHaveBeenCalled();
+ });
+
+ it('does not track anything if logged in and not within a trackable context', () => {
+ createWrapper();
+
+ expect(trackContextAccess).not.toHaveBeenCalled();
+ });
+
+ it('tracks item access if logged in within a trackable context', () => {
+ const currentContext = { namespace: 'groups' };
+ createWrapper({
+ sidebarData: {
+ ...mockSidebarData,
+ current_context: currentContext,
+ },
+ });
+
+ expect(trackContextAccess).toHaveBeenCalledWith('root', currentContext, '/-/track_visits');
+ });
});
});
@@ -205,6 +221,7 @@ describe('SuperSidebar component', () => {
expect(findSidebar().attributes('inert')).toBe('inert');
expect(findSidebar().classes()).not.toContain(peekHintClass);
+ expect(findSidebar().classes()).not.toContain(hasPeekedClass);
expect(findSidebar().classes()).not.toContain(peekClass);
});
@@ -216,6 +233,7 @@ describe('SuperSidebar component', () => {
expect(findSidebar().attributes('inert')).toBe('inert');
expect(findSidebar().classes()).toContain(peekHintClass);
+ expect(findSidebar().classes()).toContain(hasPeekedClass);
expect(findSidebar().classes()).not.toContain(peekClass);
});
@@ -230,9 +248,23 @@ describe('SuperSidebar component', () => {
expect(findSidebar().attributes('inert')).toBe(undefined);
expect(findSidebar().classes()).toContain(peekClass);
expect(findSidebar().classes()).not.toContain(peekHintClass);
+ expect(findHoverPeekBehavior().exists()).toBe(false);
},
);
+ it(`makes sidebar interactive and visible when hover peek state is ${STATE_OPEN}`, async () => {
+ createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
+
+ findHoverPeekBehavior().vm.$emit('change', STATE_OPEN);
+ await nextTick();
+
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().classes()).toContain(hasPeekedClass);
+ expect(findSidebar().classes()).not.toContain(peekHintClass);
+ expect(findPeekBehavior().exists()).toBe(false);
+ });
+
it('keeps track of if sidebar has mouseover or not', async () => {
createWrapper({ sidebarState: { isCollapsed: false, isPeekable: true } });
expect(findPeekBehavior().props('isMouseOverSidebar')).toBe(false);
@@ -248,16 +280,9 @@ describe('SuperSidebar component', () => {
createWrapper();
});
- it('allows overflow while the context switcher is closed', () => {
+ it('allows overflow', () => {
expect(findNavContainer().classes()).toContain('gl-overflow-auto');
});
-
- it('hides overflow when context switcher is opened', async () => {
- findContextSwitcher().vm.$emit('toggle', true);
- await nextTick();
-
- expect(findNavContainer().classes()).not.toContain('gl-overflow-auto');
- });
});
describe('when a trial is active', () => {
@@ -271,14 +296,10 @@ describe('SuperSidebar component', () => {
});
});
- describe('Logged out', () => {
- beforeEach(() => {
- createWrapper({ sidebarData: loggedOutSidebarData });
- });
-
- it('renders context header instead of context switcher', () => {
- expect(findContextHeader().exists()).toBe(true);
- expect(findContextSwitcher().exists()).toBe(false);
+ describe('ARIA attributes', () => {
+ it('adds aria-label attribute to nav element', () => {
+ createWrapper();
+ expect(wrapper.find('nav').attributes('aria-label')).toBe('Primary');
});
});
});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
index 23b735c2773..1f2e5602d10 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -1,6 +1,5 @@
import { nextTick } from 'vue';
import { GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -46,31 +45,29 @@ describe('SuperSidebarToggle component', () => {
expect(findButton().attributes('aria-expanded')).toBe('true');
});
- it('has aria-expanded as false when collapsed', () => {
- createWrapper({ sidebarState: { isCollapsed: true } });
- expect(findButton().attributes('aria-expanded')).toBe('false');
- });
+ it.each(['isCollapsed', 'isPeek', 'isHoverPeek'])(
+ 'has aria-expanded as false when %s is `true`',
+ (stateProp) => {
+ createWrapper({ sidebarState: { [stateProp]: true } });
+ expect(findButton().attributes('aria-expanded')).toBe('false');
+ },
+ );
it('has aria-label attribute', () => {
createWrapper();
- expect(findButton().attributes('aria-label')).toBe(__('Navigation sidebar'));
- });
-
- it('is disabled when isPeek is true', () => {
- createWrapper({ sidebarState: { isPeek: true } });
- expect(findButton().attributes('disabled')).toBeDefined();
+ expect(findButton().attributes('aria-label')).toBe('Primary navigation sidebar');
});
});
describe('tooltip', () => {
it('displays collapse when expanded', () => {
createWrapper();
- expect(getTooltip().title).toBe(__('Hide sidebar'));
+ expect(getTooltip().title).toBe('Hide sidebar');
});
it('displays expand when collapsed', () => {
createWrapper({ sidebarState: { isCollapsed: true } });
- expect(getTooltip().title).toBe(__('Show sidebar'));
+ expect(getTooltip().title).toBe('Keep sidebar visible');
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index c6dd8441094..b58b65f09f5 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -65,8 +65,20 @@ describe('UserBar component', () => {
createWrapper();
});
- it('passes the "Create new..." menu groups to the create-menu component', () => {
- expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups);
+ describe('"Create new..." menu', () => {
+ describe('when there are no menu items for it', () => {
+ // This scenario usually happens for an "External" user.
+ it('does not render it', () => {
+ createWrapper({ sidebarData: { ...mockSidebarData, create_new_menu_groups: [] } });
+ expect(findCreateMenu().exists()).toBe(false);
+ });
+ });
+
+ describe('when there are menu items for it', () => {
+ it('passes the "Create new..." menu groups to the create-menu component', () => {
+ expect(findCreateMenu().props('groups')).toBe(mockSidebarData.create_new_menu_groups);
+ });
+ });
});
it('passes the "Merge request" menu groups to the merge_request_menu component', () => {
@@ -165,7 +177,7 @@ describe('UserBar component', () => {
it('search button should have tooltip', () => {
const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
- expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
+ expect(tooltip.value).toBe(`Type <kbd>/</kbd> to search`);
});
it('should render search modal', () => {
@@ -184,7 +196,7 @@ describe('UserBar component', () => {
findSearchModal().vm.$emit('hidden');
await nextTick();
const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
- expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
+ expect(tooltip.value).toBe(`Type <kbd>/</kbd> to search`);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index 662677be40f..bcc3383bcd4 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -468,27 +468,6 @@ describe('UserMenu component', () => {
});
});
- describe('Feedback item', () => {
- let item;
-
- beforeEach(() => {
- createWrapper();
- item = wrapper.findByTestId('feedback-item');
- });
-
- it('should render feedback item with a link to a new GitLab issue', () => {
- expect(item.find('a').attributes('href')).toBe(UserMenu.feedbackUrl);
- });
-
- it('has Snowplow tracking attributes', () => {
- expect(item.find('a').attributes()).toMatchObject({
- 'data-track-property': 'nav_user_menu',
- 'data-track-action': 'click_link',
- 'data-track-label': 'provide_nav_feedback',
- });
- });
- });
-
describe('Sign out group', () => {
const findSignOutGroup = () => wrapper.findByTestId('sign-out-group');
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 6fb9715824f..d464ce372ed 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -79,10 +79,8 @@ export const contextSwitcherLinks = [
export const sidebarData = {
is_logged_in: true,
current_menu_items: [],
- current_context_header: {
- title: 'Your Work',
- icon: 'work',
- },
+ current_context: {},
+ current_context_header: 'Your work',
name: 'Administrator',
username: 'root',
avatar_url: 'path/to/img_administrator',
@@ -124,15 +122,14 @@ export const sidebarData = {
css_class: 'shortcut-link-class',
},
],
+ track_visits_path: '/-/track_visits',
};
export const loggedOutSidebarData = {
is_logged_in: false,
current_menu_items: [],
- current_context_header: {
- title: 'Your Work',
- icon: 'work',
- },
+ current_context: {},
+ current_context_header: 'Your work',
support_path: '/support',
display_whats_new: true,
whats_new_most_recent_release_items_count: 5,
@@ -285,36 +282,3 @@ export const cachedFrequentGroups = JSON.stringify([
frequency: 3,
},
]);
-
-export const searchUserProjectsAndGroupsResponseMock = {
- data: {
- projects: {
- nodes: [
- {
- id: 'gid://gitlab/Project/2',
- name: 'Gitlab Shell',
- namespace: 'Gitlab Org / Gitlab Shell',
- webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-shell',
- avatarUrl: null,
- __typename: 'Project',
- },
- ],
- },
-
- user: {
- id: 'gid://gitlab/User/1',
- groups: {
- nodes: [
- {
- id: 'gid://gitlab/Group/60',
- name: 'GitLab Instance',
- namespace: 'gitlab-instance-2e4abb29',
- webUrl: 'http://gdk.test:3000/groups/gitlab-instance-2e4abb29',
- avatarUrl: null,
- __typename: 'Group',
- },
- ],
- },
- },
- },
-};
diff --git a/spec/frontend/super_sidebar/mocks.js b/spec/frontend/super_sidebar/mocks.js
new file mode 100644
index 00000000000..d13e5f1f361
--- /dev/null
+++ b/spec/frontend/super_sidebar/mocks.js
@@ -0,0 +1,24 @@
+export const moveMouse = (clientX) => {
+ const event = new MouseEvent('mousemove', {
+ clientX,
+ });
+
+ document.dispatchEvent(event);
+};
+
+export const mouseEnter = (el) => {
+ const event = new MouseEvent('mouseenter');
+
+ el.dispatchEvent(event);
+};
+
+export const mouseLeave = (el) => {
+ const event = new MouseEvent('mouseleave');
+
+ el.dispatchEvent(event);
+};
+
+export const moveMouseOutOfDocument = () => {
+ const event = new MouseEvent('mouseleave');
+ document.documentElement.dispatchEvent(event);
+};
diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js
index 536599e6c12..85f45de06ba 100644
--- a/spec/frontend/super_sidebar/utils_spec.js
+++ b/spec/frontend/super_sidebar/utils_spec.js
@@ -1,17 +1,20 @@
import * as Sentry from '@sentry/browser';
+import MockAdapter from 'axios-mock-adapter';
import {
getTopFrequentItems,
trackContextAccess,
- formatContextSwitcherItems,
getItemsFromLocalStorage,
removeItemFromLocalStorage,
ariaCurrent,
} from '~/super_sidebar/utils';
+import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import waitForPromises from 'helpers/wait_for_promises';
import { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data';
-import { cachedFrequentProjects, searchUserProjectsAndGroupsResponseMock } from './mock_data';
+import { cachedFrequentProjects } from './mock_data';
jest.mock('@sentry/browser');
@@ -42,13 +45,29 @@ describe('Super sidebar utils spec', () => {
});
describe('trackContextAccess', () => {
+ useLocalStorageSpy();
+
+ let axiosMock;
+
const username = 'root';
+ const trackVisitsPath = '/-/track_visits';
const context = {
namespace: 'groups',
item: { id: 1 },
};
const storageKey = `${username}/frequent-${context.namespace}`;
+ beforeEach(() => {
+ gon.features = { serverSideFrecentNamespaces: true };
+ axiosMock = new MockAdapter(axios);
+ axiosMock.onPost(trackVisitsPath).reply(HTTP_STATUS_OK);
+ });
+
+ afterEach(() => {
+ gon.features = {};
+ axiosMock.restore();
+ });
+
it('returns `false` if local storage is not available', () => {
jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
@@ -56,7 +75,7 @@ describe('Super sidebar utils spec', () => {
});
it('creates a new item if it does not exist in the local storage', () => {
- trackContextAccess(username, context);
+ trackContextAccess(username, context, trackVisitsPath);
expect(window.localStorage.setItem).toHaveBeenCalledWith(
storageKey,
@@ -70,6 +89,24 @@ describe('Super sidebar utils spec', () => {
);
});
+ it('sends a POST request to persist the visit in the DB', async () => {
+ expect(axiosMock.history.post).toHaveLength(0);
+
+ trackContextAccess(username, context, trackVisitsPath);
+ await waitForPromises();
+
+ expect(axiosMock.history.post).toHaveLength(1);
+ expect(axiosMock.history.post[0].url).toBe(trackVisitsPath);
+ });
+
+ it('does not send a POST request when the serverSideFrecentNamespaces feature flag is disabled', async () => {
+ gon.features = { serverSideFrecentNamespaces: false };
+ trackContextAccess(username, context, trackVisitsPath);
+ await waitForPromises();
+
+ expect(axiosMock.history.post).toHaveLength(0);
+ });
+
it('updates existing item frequency/access time if it was persisted to the local storage over 15 minutes ago', () => {
window.localStorage.setItem(
storageKey,
@@ -81,7 +118,7 @@ describe('Super sidebar utils spec', () => {
},
]),
);
- trackContextAccess(username, context);
+ trackContextAccess(username, context, trackVisitsPath);
expect(window.localStorage.setItem).toHaveBeenCalledWith(
storageKey,
@@ -95,7 +132,7 @@ describe('Super sidebar utils spec', () => {
);
});
- it('leaves item frequency/access time as is if it was persisted to the local storage under 15 minutes ago', () => {
+ it('leaves item frequency/access time as is if it was persisted to the local storage under 15 minutes ago, and does not send a POST request', () => {
const jsonString = JSON.stringify([
{
id: 1,
@@ -108,10 +145,12 @@ describe('Super sidebar utils spec', () => {
expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
expect(window.localStorage.setItem).toHaveBeenCalledWith(storageKey, jsonString);
- trackContextAccess(username, context);
+ trackContextAccess(username, context, trackVisitsPath);
expect(window.localStorage.setItem).toHaveBeenCalledTimes(3);
expect(window.localStorage.setItem).toHaveBeenLastCalledWith(storageKey, jsonString);
+
+ expect(axiosMock.history.post).toHaveLength(0);
});
it('always updates stored item metadata', () => {
@@ -163,10 +202,14 @@ describe('Super sidebar utils spec', () => {
const newItem = {
id: FREQUENT_ITEMS.MAX_COUNT + 1,
};
- trackContextAccess(username, {
- namespace: 'groups',
- item: newItem,
- });
+ trackContextAccess(
+ username,
+ {
+ namespace: 'groups',
+ item: newItem,
+ },
+ trackVisitsPath,
+ );
// Finally, retrieve the final data from the local storage
const finallyStoredItems = JSON.parse(window.localStorage.getItem(storageKey));
@@ -182,21 +225,6 @@ describe('Super sidebar utils spec', () => {
});
});
- describe('formatContextSwitcherItems', () => {
- it('returns the formatted items', () => {
- const projects = searchUserProjectsAndGroupsResponseMock.data.projects.nodes;
- expect(formatContextSwitcherItems(projects)).toEqual([
- {
- id: projects[0].id,
- avatar: null,
- title: projects[0].name,
- subtitle: 'Gitlab Org',
- link: projects[0].webUrl,
- },
- ]);
- });
- });
-
describe('getItemsFromLocalStorage', () => {
const storageKey = 'mockStorageKey';
const maxItems = 5;
diff --git a/spec/frontend/time_tracking/components/timelogs_app_spec.js b/spec/frontend/time_tracking/components/timelogs_app_spec.js
index ca470ce63ac..13188f3b937 100644
--- a/spec/frontend/time_tracking/components/timelogs_app_spec.js
+++ b/spec/frontend/time_tracking/components/timelogs_app_spec.js
@@ -95,12 +95,12 @@ describe('Timelogs app', () => {
mountComponent();
const username = 'johnsmith';
- const fromDate = new Date('2023-02-28');
- const toDate = new Date('2023-03-28');
+ const fromDateTime = new Date('2023-02-28');
+ const toDateTime = new Date('2023-03-28');
findUsernameInput().vm.$emit('input', username);
- findFromDatepicker().vm.$emit('input', fromDate);
- findToDatepicker().vm.$emit('input', toDate);
+ findFromDatepicker().vm.$emit('input', fromDateTime);
+ findToDatepicker().vm.$emit('input', toDateTime);
resolvedEmptyListMock.mockClear();
@@ -110,8 +110,8 @@ describe('Timelogs app', () => {
expect(resolvedEmptyListMock).toHaveBeenCalledWith({
username,
- startDate: fromDate,
- endDate: toDate,
+ startTime: fromDateTime,
+ endTime: toDateTime,
groupId: null,
projectId: null,
first: 20,
@@ -119,6 +119,15 @@ describe('Timelogs app', () => {
after: null,
before: null,
});
+
+ expect(`${wrapper.vm.queryVariables.startTime}`).toEqual(
+ 'Tue Feb 28 2023 00:00:00 GMT+0000 (Greenwich Mean Time)',
+ );
+ // should be 1 day ahead of the initial To Date value
+ expect(`${wrapper.vm.queryVariables.endTime}`).toEqual(
+ 'Wed Mar 29 2023 00:00:00 GMT+0000 (Greenwich Mean Time)',
+ );
+
expect(createAlert).not.toHaveBeenCalled();
expect(Sentry.captureException).not.toHaveBeenCalled();
});
@@ -140,8 +149,8 @@ describe('Timelogs app', () => {
expect(resolvedEmptyListMock).toHaveBeenCalledWith({
username,
- startDate: null,
- endDate: null,
+ startTime: null,
+ endTime: null,
groupId: null,
projectId: null,
first: 20,
diff --git a/spec/frontend/tracing/components/tracing_details_spec.js b/spec/frontend/tracing/components/tracing_details_spec.js
deleted file mode 100644
index c5efa2a7eb5..00000000000
--- a/spec/frontend/tracing/components/tracing_details_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TracingDetails from '~/tracing/components/tracing_details.vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/alert';
-import { visitUrl, isSafeURL } from '~/lib/utils/url_utility';
-
-jest.mock('~/alert');
-jest.mock('~/lib/utils/url_utility');
-
-describe('TracingDetails', () => {
- let wrapper;
- let observabilityClientMock;
-
- const TRACE_ID = 'test-trace-id';
- const TRACING_INDEX_URL = 'https://www.gitlab.com/flightjs/Flight/-/tracing';
-
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findTraceDetails = () => wrapper.findComponentByTestId('trace-details');
-
- const props = {
- traceId: TRACE_ID,
- tracingIndexUrl: TRACING_INDEX_URL,
- };
-
- const mountComponent = async () => {
- wrapper = shallowMountExtended(TracingDetails, {
- propsData: {
- ...props,
- observabilityClient: observabilityClientMock,
- },
- });
- await waitForPromises();
- };
-
- beforeEach(() => {
- isSafeURL.mockReturnValue(true);
-
- observabilityClientMock = {
- isTracingEnabled: jest.fn(),
- fetchTrace: jest.fn(),
- };
- });
-
- it('renders the loading indicator while checking if tracing is enabled', () => {
- mountComponent();
-
- expect(findLoadingIcon().exists()).toBe(true);
- expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
- });
-
- describe('when tracing is enabled', () => {
- const mockTrace = { traceId: 'test-trace-id', foo: 'bar' };
- beforeEach(async () => {
- observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(true);
- observabilityClientMock.fetchTrace.mockResolvedValueOnce(mockTrace);
-
- await mountComponent();
- });
-
- it('fetches the trace and renders the trace details', () => {
- expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
- expect(observabilityClientMock.fetchTrace).toHaveBeenCalled();
- expect(findLoadingIcon().exists()).toBe(false);
- expect(findTraceDetails().exists()).toBe(true);
- });
- });
-
- describe('when tracing is not enabled', () => {
- beforeEach(async () => {
- observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(false);
-
- await mountComponent();
- });
-
- it('redirects to tracingIndexUrl', () => {
- expect(visitUrl).toHaveBeenCalledWith(props.tracingIndexUrl);
- });
- });
-
- describe('error handling', () => {
- it('if isTracingEnabled fails, it renders an alert and empty page', async () => {
- observabilityClientMock.isTracingEnabled.mockRejectedValueOnce('error');
-
- await mountComponent();
-
- expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load trace details.' });
- expect(findLoadingIcon().exists()).toBe(false);
- expect(findTraceDetails().exists()).toBe(false);
- });
-
- it('if fetchTrace fails, it renders an alert and empty page', async () => {
- observabilityClientMock.isTracingEnabled.mockReturnValueOnce(true);
- observabilityClientMock.fetchTrace.mockRejectedValueOnce('error');
-
- await mountComponent();
-
- expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load trace details.' });
- expect(findLoadingIcon().exists()).toBe(false);
- expect(findTraceDetails().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/tracing/components/tracing_empty_state_spec.js b/spec/frontend/tracing/components/tracing_empty_state_spec.js
deleted file mode 100644
index d91c62a1dad..00000000000
--- a/spec/frontend/tracing/components/tracing_empty_state_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { GlButton, GlEmptyState } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TracingEmptyState from '~/tracing/components/tracing_empty_state.vue';
-
-describe('TracingEmptyState', () => {
- let wrapper;
-
- const findEnableButton = () => wrapper.findComponent(GlButton);
-
- beforeEach(() => {
- wrapper = shallowMountExtended(TracingEmptyState);
- });
-
- it('renders the component properly', () => {
- expect(wrapper.exists()).toBe(true);
- });
-
- it('displays the correct title', () => {
- const { title } = wrapper.findComponent(GlEmptyState).props();
- expect(title).toBe('Get started with Tracing');
- });
-
- it('displays the correct description', () => {
- const description = wrapper.find('span').text();
- expect(description).toBe('Monitor your applications with GitLab Distributed Tracing.');
- });
-
- it('displays the enable button', () => {
- const enableButton = findEnableButton();
- expect(enableButton.exists()).toBe(true);
- expect(enableButton.text()).toBe('Enable');
- });
-
- it('emits enable-tracing when enable button is clicked', () => {
- findEnableButton().vm.$emit('click');
-
- expect(wrapper.emitted('enable-tracing')).toHaveLength(1);
- });
-});
diff --git a/spec/frontend/tracing/components/tracing_list_filtered_search_spec.js b/spec/frontend/tracing/components/tracing_list_filtered_search_spec.js
deleted file mode 100644
index ad15dd4a371..00000000000
--- a/spec/frontend/tracing/components/tracing_list_filtered_search_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { GlFilteredSearch } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-
-import TracingListFilteredSearch from '~/tracing/components/tracing_list_filtered_search.vue';
-
-describe('TracingListFilteredSearch', () => {
- let wrapper;
- const initialFilters = [
- { type: 'period', value: '1h' },
- { type: 'service_name', value: 'example-service' },
- ];
- beforeEach(() => {
- wrapper = shallowMountExtended(TracingListFilteredSearch, {
- propsData: {
- initialFilters,
- },
- });
- });
-
- it('renders the component', () => {
- expect(wrapper.exists()).toBe(true);
- });
-
- it('sets initialFilters prop correctly', () => {
- expect(wrapper.findComponent(GlFilteredSearch).props('value')).toEqual(initialFilters);
- });
-
- it('emits submit event on filtered search submit', () => {
- wrapper
- .findComponent(GlFilteredSearch)
- .vm.$emit('submit', { filters: [{ type: 'period', value: '1h' }] });
-
- expect(wrapper.emitted('submit')).toHaveLength(1);
- expect(wrapper.emitted('submit')[0][0]).toEqual({
- filters: [{ type: 'period', value: '1h' }],
- });
- });
-});
diff --git a/spec/frontend/tracing/components/tracing_list_spec.js b/spec/frontend/tracing/components/tracing_list_spec.js
deleted file mode 100644
index 9aa37ac9c9c..00000000000
--- a/spec/frontend/tracing/components/tracing_list_spec.js
+++ /dev/null
@@ -1,216 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import TracingList from '~/tracing/components/tracing_list.vue';
-import TracingEmptyState from '~/tracing/components/tracing_empty_state.vue';
-import TracingTableList from '~/tracing/components/tracing_table_list.vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/alert';
-import * as urlUtility from '~/lib/utils/url_utility';
-import {
- queryToFilterObj,
- filterObjToQuery,
- filterObjToFilterToken,
- filterTokensToFilterObj,
-} from '~/tracing/filters';
-import FilteredSearch from '~/tracing/components/tracing_list_filtered_search.vue';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-import setWindowLocation from 'helpers/set_window_location_helper';
-
-jest.mock('~/alert');
-jest.mock('~/tracing/filters');
-
-describe('TracingList', () => {
- let wrapper;
- let observabilityClientMock;
-
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findEmptyState = () => wrapper.findComponent(TracingEmptyState);
- const findTableList = () => wrapper.findComponent(TracingTableList);
- const findFilteredSearch = () => wrapper.findComponent(FilteredSearch);
- const findUrlSync = () => wrapper.findComponent(UrlSync);
-
- const mountComponent = async () => {
- wrapper = shallowMountExtended(TracingList, {
- propsData: {
- observabilityClient: observabilityClientMock,
- },
- });
- await waitForPromises();
- };
-
- beforeEach(() => {
- observabilityClientMock = {
- isTracingEnabled: jest.fn(),
- enableTraces: jest.fn(),
- fetchTraces: jest.fn(),
- };
- });
-
- it('renders the loading indicator while checking if tracing is enabled', () => {
- mountComponent();
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findEmptyState().exists()).toBe(false);
- expect(findTableList().exists()).toBe(false);
- expect(findFilteredSearch().exists()).toBe(false);
- expect(findUrlSync().exists()).toBe(false);
- expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
- });
-
- describe('when tracing is enabled', () => {
- const mockTraces = ['trace1', 'trace2'];
- beforeEach(async () => {
- observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(true);
- observabilityClientMock.fetchTraces.mockResolvedValueOnce(mockTraces);
-
- await mountComponent();
- });
-
- it('fetches the traces and renders the trace list with filtered search', () => {
- expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
- expect(observabilityClientMock.fetchTraces).toHaveBeenCalled();
- expect(findLoadingIcon().exists()).toBe(false);
- expect(findEmptyState().exists()).toBe(false);
- expect(findTableList().exists()).toBe(true);
- expect(findFilteredSearch().exists()).toBe(true);
- expect(findUrlSync().exists()).toBe(true);
- expect(findTableList().props('traces')).toBe(mockTraces);
- });
-
- it('calls fetchTraces method when TracingTableList emits reload event', () => {
- observabilityClientMock.fetchTraces.mockClear();
- observabilityClientMock.fetchTraces.mockResolvedValueOnce(['trace1']);
-
- findTableList().vm.$emit('reload');
-
- expect(observabilityClientMock.fetchTraces).toHaveBeenCalledTimes(1);
- });
-
- it('on trace selection it redirects to the details url', () => {
- setWindowLocation('base_path');
- const visitUrlMock = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
-
- findTableList().vm.$emit('trace-selected', { trace_id: 'test-trace-id' });
-
- expect(visitUrlMock).toHaveBeenCalledTimes(1);
- expect(visitUrlMock).toHaveBeenCalledWith('/base_path/test-trace-id');
- });
- });
-
- describe('filtered search', () => {
- let mockFilterObj;
- let mockFilterToken;
- let mockQuery;
- let mockUpdatedFilterObj;
-
- beforeEach(async () => {
- observabilityClientMock.isTracingEnabled.mockResolvedValue(true);
- observabilityClientMock.fetchTraces.mockResolvedValue([]);
-
- setWindowLocation('?trace-id=foo');
-
- mockFilterObj = { mock: 'filter-obj' };
- queryToFilterObj.mockReturnValue(mockFilterObj);
-
- mockFilterToken = ['mock-token'];
- filterObjToFilterToken.mockReturnValue(mockFilterToken);
-
- mockQuery = { mock: 'query' };
- filterObjToQuery.mockReturnValueOnce(mockQuery);
-
- mockUpdatedFilterObj = { mock: 'filter-obj-upd' };
- filterTokensToFilterObj.mockReturnValue(mockUpdatedFilterObj);
-
- await mountComponent();
- });
-
- it('renders FilteredSeach with initial filters parsed from window.location', () => {
- expect(queryToFilterObj).toHaveBeenCalledWith('?trace-id=foo');
- expect(filterObjToFilterToken).toHaveBeenCalledWith(mockFilterObj);
- expect(findFilteredSearch().props('initialFilters')).toBe(mockFilterToken);
- });
-
- it('renders UrlSync and sets query prop', () => {
- expect(filterObjToQuery).toHaveBeenCalledWith(mockFilterObj);
- expect(findUrlSync().props('query')).toBe(mockQuery);
- });
-
- it('process filters on search submit', async () => {
- const mockUpdatedQuery = { mock: 'updated-query' };
- filterObjToQuery.mockReturnValueOnce(mockUpdatedQuery);
- const mockFilters = { mock: 'some-filter' };
-
- findFilteredSearch().vm.$emit('submit', mockFilters);
- await waitForPromises();
-
- expect(filterTokensToFilterObj).toHaveBeenCalledWith(mockFilters);
- expect(filterObjToQuery).toHaveBeenCalledWith(mockUpdatedFilterObj);
- expect(findUrlSync().props('query')).toBe(mockUpdatedQuery);
- });
-
- it('fetches traces with filters', () => {
- expect(observabilityClientMock.fetchTraces).toHaveBeenCalledWith(mockFilterObj);
-
- findFilteredSearch().vm.$emit('submit', {});
-
- expect(observabilityClientMock.fetchTraces).toHaveBeenLastCalledWith(mockUpdatedFilterObj);
- });
- });
-
- describe('when tracing is not enabled', () => {
- beforeEach(async () => {
- observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(false);
- observabilityClientMock.fetchTraces.mockResolvedValueOnce([]);
-
- await mountComponent();
- });
-
- it('renders TracingEmptyState', () => {
- expect(findEmptyState().exists()).toBe(true);
- });
-
- it('calls enableTracing when TracingEmptyState emits enable-tracing', () => {
- findEmptyState().vm.$emit('enable-tracing');
-
- expect(observabilityClientMock.enableTraces).toHaveBeenCalled();
- });
- });
-
- describe('error handling', () => {
- it('if isTracingEnabled fails, it renders an alert and empty page', async () => {
- observabilityClientMock.isTracingEnabled.mockRejectedValueOnce('error');
-
- await mountComponent();
-
- expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load page.' });
- expect(findLoadingIcon().exists()).toBe(false);
- expect(findEmptyState().exists()).toBe(false);
- expect(findTableList().exists()).toBe(false);
- });
-
- it('if fetchTraces fails, it renders an alert and empty list', async () => {
- observabilityClientMock.fetchTraces.mockRejectedValueOnce('error');
- observabilityClientMock.isTracingEnabled.mockReturnValueOnce(true);
-
- await mountComponent();
-
- expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load traces.' });
- expect(findTableList().exists()).toBe(true);
- expect(findTableList().props('traces')).toEqual([]);
- });
-
- it('if enableTraces fails, it renders an alert and empty-state', async () => {
- observabilityClientMock.isTracingEnabled.mockReturnValueOnce(false);
- observabilityClientMock.enableTraces.mockRejectedValueOnce('error');
-
- await mountComponent();
-
- findEmptyState().vm.$emit('enable-tracing');
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to enable tracing.' });
- expect(findLoadingIcon().exists()).toBe(false);
- expect(findEmptyState().exists()).toBe(true);
- expect(findTableList().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/tracing/components/tracing_table_list_spec.js b/spec/frontend/tracing/components/tracing_table_list_spec.js
deleted file mode 100644
index aa96b9b370f..00000000000
--- a/spec/frontend/tracing/components/tracing_table_list_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { nextTick } from 'vue';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import TracingTableList from '~/tracing/components/tracing_table_list.vue';
-
-describe('TracingTableList', () => {
- let wrapper;
- const mockTraces = [
- {
- timestamp: '2023-07-10T15:02:30.677538Z',
- service_name: 'tracegen',
- operation: 'lets-go',
- duration: 150,
- },
- {
- timestamp: '2023-07-10T15:02:30.677538Z',
- service_name: 'tracegen',
- operation: 'lets-go',
- duration: 200,
- },
- ];
-
- const mountComponent = ({ traces = mockTraces } = {}) => {
- wrapper = mountExtended(TracingTableList, {
- propsData: {
- traces,
- },
- });
- };
-
- const getRows = () => wrapper.findComponent({ name: 'GlTable' }).find('tbody').findAll('tr');
- const getRow = (idx) => getRows().at(idx);
- const getCells = (trIdx) => getRows().at(trIdx).findAll('td');
-
- const getCell = (trIdx, tdIdx) => {
- return getCells(trIdx).at(tdIdx);
- };
-
- const selectRow = async (idx) => {
- getRow(idx).trigger('click');
- await nextTick();
- };
-
- it('renders traces as table', () => {
- mountComponent();
-
- const rows = wrapper.findAll('table tbody tr');
-
- expect(rows.length).toBe(mockTraces.length);
-
- mockTraces.forEach((trace, i) => {
- expect(getCells(i).length).toBe(4);
- expect(getCell(i, 0).text()).toBe(trace.timestamp);
- expect(getCell(i, 1).text()).toBe(trace.service_name);
- expect(getCell(i, 2).text()).toBe(trace.operation);
- expect(getCell(i, 3).text()).toBe(`${trace.duration} ms`);
- });
- });
-
- it('emits trace-selected on row selection', async () => {
- mountComponent();
-
- await selectRow(0);
- expect(wrapper.emitted('trace-selected')).toHaveLength(1);
- expect(wrapper.emitted('trace-selected')[0][0]).toBe(mockTraces[0]);
- });
-
- it('renders the empty state when no traces are provided', () => {
- mountComponent({ traces: [] });
-
- expect(getCell(0, 0).text()).toContain('No traces to display');
- const link = getCell(0, 0).findComponent({ name: 'GlLink' });
- expect(link.text()).toBe('Check again');
-
- link.trigger('click');
- expect(wrapper.emitted('reload')).toHaveLength(1);
- });
-});
diff --git a/spec/frontend/tracing/details_index_spec.js b/spec/frontend/tracing/details_index_spec.js
deleted file mode 100644
index e0d368b6cb7..00000000000
--- a/spec/frontend/tracing/details_index_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import DetailsIndex from '~/tracing/details_index.vue';
-import TracingDetails from '~/tracing/components/tracing_details.vue';
-import ObservabilityContainer from '~/observability/components/observability_container.vue';
-
-describe('DetailsIndex', () => {
- const props = {
- traceId: 'test-trace-id',
- tracingIndexUrl: 'https://example.com/tracing/index',
- oauthUrl: 'https://example.com/oauth',
- tracingUrl: 'https://example.com/tracing',
- provisioningUrl: 'https://example.com/provisioning',
- };
-
- let wrapper;
-
- const mountComponent = () => {
- wrapper = shallowMountExtended(DetailsIndex, {
- propsData: props,
- });
- };
-
- it('renders ObservabilityContainer component', () => {
- mountComponent();
-
- const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
- expect(observabilityContainer.exists()).toBe(true);
- expect(observabilityContainer.props('oauthUrl')).toBe(props.oauthUrl);
- expect(observabilityContainer.props('tracingUrl')).toBe(props.tracingUrl);
- expect(observabilityContainer.props('provisioningUrl')).toBe(props.provisioningUrl);
- });
-
- it('renders TracingList component inside ObservabilityContainer', () => {
- mountComponent();
-
- const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
- const detailsCmp = observabilityContainer.findComponent(TracingDetails);
- expect(detailsCmp.exists()).toBe(true);
- expect(detailsCmp.props('traceId')).toBe(props.traceId);
- expect(detailsCmp.props('tracingIndexUrl')).toBe(props.tracingIndexUrl);
- });
-});
diff --git a/spec/frontend/tracing/filters_spec.js b/spec/frontend/tracing/filters_spec.js
deleted file mode 100644
index ee396326f45..00000000000
--- a/spec/frontend/tracing/filters_spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import {
- filterToQueryObject,
- urlQueryToFilter,
- prepareTokens,
- processFilters,
-} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
-
-import {
- PERIOD_FILTER_TOKEN_TYPE,
- SERVICE_NAME_FILTER_TOKEN_TYPE,
- OPERATION_FILTER_TOKEN_TYPE,
- TRACE_ID_FILTER_TOKEN_TYPE,
- DURATION_MS_FILTER_TOKEN_TYPE,
- queryToFilterObj,
- filterObjToQuery,
- filterObjToFilterToken,
- filterTokensToFilterObj,
-} from '~/tracing/filters';
-
-jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils');
-
-describe('utils', () => {
- describe('queryToFilterObj', () => {
- it('should build a filter obj', () => {
- const url = 'http://example.com/';
- urlQueryToFilter.mockReturnValue({
- period: '7d',
- service: 'my_service',
- operation: 'my_operation',
- trace_id: 'my_trace_id',
- durationMs: '500',
- [FILTERED_SEARCH_TERM]: 'test',
- });
-
- const filterObj = queryToFilterObj(url);
-
- expect(urlQueryToFilter).toHaveBeenCalledWith(url, {
- customOperators: [
- { operator: '>', prefix: 'gt' },
- { operator: '<', prefix: 'lt' },
- ],
- filteredSearchTermKey: 'search',
- });
- expect(filterObj).toEqual({
- period: '7d',
- service: 'my_service',
- operation: 'my_operation',
- traceId: 'my_trace_id',
- durationMs: '500',
- search: 'test',
- });
- });
- });
-
- describe('filterObjToQuery', () => {
- it('should convert filter object to URL query', () => {
- filterToQueryObject.mockReturnValue('mockquery');
-
- const query = filterObjToQuery({
- period: '7d',
- serviceName: 'my_service',
- operation: 'my_operation',
- traceId: 'my_trace_id',
- durationMs: '500',
- search: 'test',
- });
-
- expect(filterToQueryObject).toHaveBeenCalledWith(
- {
- period: '7d',
- service: 'my_service',
- operation: 'my_operation',
- trace_id: 'my_trace_id',
- durationMs: '500',
- 'filtered-search-term': 'test',
- },
- {
- customOperators: [
- { applyOnlyToKey: 'durationMs', operator: '>', prefix: 'gt' },
- { applyOnlyToKey: 'durationMs', operator: '<', prefix: 'lt' },
- ],
- filteredSearchTermKey: 'search',
- },
- );
- expect(query).toBe('mockquery');
- });
- });
-
- describe('filterObjToFilterToken', () => {
- it('should convert filter object to filter tokens', () => {
- const mockTokens = [];
- prepareTokens.mockReturnValue(mockTokens);
-
- const tokens = filterObjToFilterToken({
- period: '7d',
- serviceName: 'my_service',
- operation: 'my_operation',
- traceId: 'my_trace_id',
- durationMs: '500',
- search: 'test',
- });
-
- expect(prepareTokens).toHaveBeenCalledWith({
- [PERIOD_FILTER_TOKEN_TYPE]: '7d',
- [SERVICE_NAME_FILTER_TOKEN_TYPE]: 'my_service',
- [OPERATION_FILTER_TOKEN_TYPE]: 'my_operation',
- [TRACE_ID_FILTER_TOKEN_TYPE]: 'my_trace_id',
- [DURATION_MS_FILTER_TOKEN_TYPE]: '500',
- [FILTERED_SEARCH_TERM]: 'test',
- });
- expect(tokens).toBe(mockTokens);
- });
- });
-
- describe('filterTokensToFilterObj', () => {
- it('should convert filter tokens to filter object', () => {
- const mockTokens = [];
- processFilters.mockReturnValue({
- [SERVICE_NAME_FILTER_TOKEN_TYPE]: 'my_service',
- [PERIOD_FILTER_TOKEN_TYPE]: '7d',
- [OPERATION_FILTER_TOKEN_TYPE]: 'my_operation',
- [TRACE_ID_FILTER_TOKEN_TYPE]: 'my_trace_id',
- [DURATION_MS_FILTER_TOKEN_TYPE]: '500',
- [FILTERED_SEARCH_TERM]: 'test',
- });
-
- const filterObj = filterTokensToFilterObj(mockTokens);
-
- expect(processFilters).toHaveBeenCalledWith(mockTokens);
- expect(filterObj).toEqual({
- serviceName: 'my_service',
- period: '7d',
- operation: 'my_operation',
- traceId: 'my_trace_id',
- durationMs: '500',
- search: 'test',
- });
- });
- });
-});
diff --git a/spec/frontend/tracing/list_index_spec.js b/spec/frontend/tracing/list_index_spec.js
deleted file mode 100644
index a5759035c2f..00000000000
--- a/spec/frontend/tracing/list_index_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ListIndex from '~/tracing/list_index.vue';
-import TracingList from '~/tracing/components/tracing_list.vue';
-import ObservabilityContainer from '~/observability/components/observability_container.vue';
-
-describe('ListIndex', () => {
- const props = {
- oauthUrl: 'https://example.com/oauth',
- tracingUrl: 'https://example.com/tracing',
- provisioningUrl: 'https://example.com/provisioning',
- };
-
- let wrapper;
-
- const mountComponent = () => {
- wrapper = shallowMountExtended(ListIndex, {
- propsData: props,
- });
- };
-
- it('renders ObservabilityContainer component', () => {
- mountComponent();
-
- const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
- expect(observabilityContainer.exists()).toBe(true);
- expect(observabilityContainer.props('oauthUrl')).toBe(props.oauthUrl);
- expect(observabilityContainer.props('tracingUrl')).toBe(props.tracingUrl);
- expect(observabilityContainer.props('provisioningUrl')).toBe(props.provisioningUrl);
- });
-
- it('renders TracingList component inside ObservabilityContainer', () => {
- mountComponent();
-
- const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
- expect(observabilityContainer.findComponent(TracingList).exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/tracking/dispatch_snowplow_event_spec.js b/spec/frontend/tracking/dispatch_snowplow_event_spec.js
new file mode 100644
index 00000000000..5f4d065d504
--- /dev/null
+++ b/spec/frontend/tracking/dispatch_snowplow_event_spec.js
@@ -0,0 +1,76 @@
+import * as Sentry from '@sentry/browser';
+
+import { dispatchSnowplowEvent } from '~/tracking/dispatch_snowplow_event';
+import getStandardContext from '~/tracking/get_standard_context';
+import { extraContext, servicePingContext } from './mock_data';
+
+jest.mock('@sentry/browser');
+jest.mock('~/tracking/get_standard_context');
+
+const category = 'Incident Management';
+const action = 'view_incident_details';
+
+describe('dispatchSnowplowEvent', () => {
+ const snowplowMock = jest.fn();
+ global.window.snowplow = snowplowMock;
+
+ const mockStandardContext = { some: 'context' };
+ getStandardContext.mockReturnValue(mockStandardContext);
+
+ beforeEach(() => {
+ snowplowMock.mockClear();
+ Sentry.captureException.mockClear();
+ });
+
+ it('calls snowplow trackStructEvent with correct arguments', () => {
+ const data = {
+ label: 'Show Incident',
+ property: 'click_event',
+ value: '12',
+ context: extraContext,
+ extra: { namespace: 'GitLab' },
+ };
+
+ dispatchSnowplowEvent(category, action, data);
+
+ expect(snowplowMock).toHaveBeenCalledWith('trackStructEvent', {
+ category,
+ action,
+ label: data.label,
+ property: data.property,
+ value: Number(data.value),
+ context: [mockStandardContext, data.context],
+ });
+ });
+
+ it('throws an error if no category is provided', () => {
+ expect(() => {
+ dispatchSnowplowEvent(undefined, 'some-action', {});
+ }).toThrow('Tracking: no category provided for tracking.');
+ });
+
+ it('handles an array of contexts', () => {
+ const data = {
+ context: [extraContext, servicePingContext],
+ extra: { namespace: 'GitLab' },
+ };
+
+ dispatchSnowplowEvent(category, action, data);
+
+ expect(snowplowMock).toHaveBeenCalledWith('trackStructEvent', {
+ category,
+ action,
+ context: [mockStandardContext, ...data.context],
+ });
+ });
+
+ it('handles Sentry error capturing', () => {
+ snowplowMock.mockImplementation(() => {
+ throw new Error('some error');
+ });
+
+ dispatchSnowplowEvent(category, action, {});
+
+ expect(Sentry.captureException).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/tracking/internal_events_spec.js b/spec/frontend/tracking/internal_events_spec.js
index ca244c25b06..6e773fde4db 100644
--- a/spec/frontend/tracking/internal_events_spec.js
+++ b/spec/frontend/tracking/internal_events_spec.js
@@ -6,9 +6,11 @@ import {
GITLAB_INTERNAL_EVENT_CATEGORY,
SERVICE_PING_SCHEMA,
LOAD_INTERNAL_EVENTS_SELECTOR,
+ USER_CONTEXT_SCHEMA,
} from '~/tracking/constants';
import * as utils from '~/tracking/utils';
import { Tracker } from '~/tracking/tracker';
+import { extraContext } from './mock_data';
jest.mock('~/api', () => ({
trackInternalEvent: jest.fn(),
@@ -21,11 +23,11 @@ jest.mock('~/tracking/utils', () => ({
Tracker.enabled = jest.fn();
+const event = 'TestEvent';
+
describe('InternalEvents', () => {
describe('track_event', () => {
it('track_event calls API.trackInternalEvent with correct arguments', () => {
- const event = 'TestEvent';
-
InternalEvents.track_event(event);
expect(API.trackInternalEvent).toHaveBeenCalledTimes(1);
@@ -35,42 +37,65 @@ describe('InternalEvents', () => {
it('track_event calls tracking.event functions with correct arguments', () => {
const trackingSpy = mockTracking(GITLAB_INTERNAL_EVENT_CATEGORY, undefined, jest.spyOn);
- const event = 'TestEvent';
-
- InternalEvents.track_event(event);
+ InternalEvents.track_event(event, { context: extraContext });
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(GITLAB_INTERNAL_EVENT_CATEGORY, event, {
- context: {
- schema: SERVICE_PING_SCHEMA,
- data: {
- event_name: event,
- data_source: 'redis_hll',
+ context: [
+ {
+ schema: SERVICE_PING_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: 'redis_hll',
+ },
},
- },
+ extraContext,
+ ],
});
});
});
describe('mixin', () => {
let wrapper;
+ const Component = {
+ template: `
+ <div>
+ <button data-testid="button1" @click="handleButton1Click">Button 1</button>
+ <button data-testid="button2" @click="handleButton2Click">Button 2</button>
+ </div>
+ `,
+ methods: {
+ handleButton1Click() {
+ this.track_event(event);
+ },
+ handleButton2Click() {
+ this.track_event(event, extraContext);
+ },
+ },
+ mixins: [InternalEvents.mixin()],
+ };
beforeEach(() => {
- const Component = {
- render() {},
- mixins: [InternalEvents.mixin()],
- };
wrapper = shallowMountExtended(Component);
});
- it('this.track_event function calls InternalEvent`s track function with an event', () => {
- const event = 'TestEvent';
+ it('this.track_event function calls InternalEvent`s track function with an event', async () => {
+ const trackEventSpy = jest.spyOn(InternalEvents, 'track_event');
+
+ await wrapper.findByTestId('button1').trigger('click');
+
+ expect(trackEventSpy).toHaveBeenCalledTimes(1);
+ expect(trackEventSpy).toHaveBeenCalledWith(event, {});
+ });
+
+ it("this.track_event function calls InternalEvent's track function with an event and data", async () => {
+ const data = extraContext;
const trackEventSpy = jest.spyOn(InternalEvents, 'track_event');
- wrapper.vm.track_event(event);
+ await wrapper.findByTestId('button2').trigger('click');
expect(trackEventSpy).toHaveBeenCalledTimes(1);
- expect(trackEventSpy).toHaveBeenCalledWith(event);
+ expect(trackEventSpy).toHaveBeenCalledWith(event, data);
});
});
@@ -145,4 +170,88 @@ describe('InternalEvents', () => {
});
});
});
+
+ describe('initBrowserSDK', () => {
+ beforeEach(() => {
+ window.glClient = {
+ setDocumentTitle: jest.fn(),
+ page: jest.fn(),
+ };
+ window.gl = {
+ environment: 'testing',
+ key: 'value',
+ };
+ window.gl.snowplowStandardContext = {
+ schema: 'iglu:com.gitlab/gitlab_standard',
+ data: {
+ environment: 'testing',
+ key: 'value',
+ google_analytics_id: '',
+ source: 'gitlab-javascript',
+ extra: {},
+ },
+ };
+ });
+
+ it('should not call setDocumentTitle or page methods when window.glClient is undefined', () => {
+ window.glClient = undefined;
+
+ InternalEvents.initBrowserSDK();
+
+ expect(window.glClient?.setDocumentTitle).toBeUndefined();
+ expect(window.glClient?.page).toBeUndefined();
+ });
+
+ it('should call setDocumentTitle and page methods on window.glClient when it is defined', () => {
+ const mockStandardContext = window.gl.snowplowStandardContext;
+ const userContext = {
+ schema: USER_CONTEXT_SCHEMA,
+ data: mockStandardContext?.data,
+ };
+
+ InternalEvents.initBrowserSDK();
+
+ expect(window.glClient.setDocumentTitle).toHaveBeenCalledWith('GitLab');
+ expect(window.glClient.page).toHaveBeenCalledWith({
+ title: 'GitLab',
+ context: [userContext],
+ });
+ });
+
+ it('should call page method with combined standard and experiment contexts', () => {
+ const mockStandardContext = window.gl.snowplowStandardContext;
+ const userContext = {
+ schema: USER_CONTEXT_SCHEMA,
+ data: mockStandardContext?.data,
+ };
+
+ InternalEvents.initBrowserSDK();
+
+ expect(window.glClient.page).toHaveBeenCalledWith({
+ title: 'GitLab',
+ context: [userContext],
+ });
+ });
+
+ it('should call setDocumentTitle and page methods with default data when window.gl is undefined', () => {
+ window.gl = undefined;
+
+ InternalEvents.initBrowserSDK();
+
+ expect(window.glClient.setDocumentTitle).toHaveBeenCalledWith('GitLab');
+ expect(window.glClient.page).toHaveBeenCalledWith({
+ title: 'GitLab',
+ context: [
+ {
+ schema: USER_CONTEXT_SCHEMA,
+ data: {
+ google_analytics_id: '',
+ source: 'gitlab-javascript',
+ extra: {},
+ },
+ },
+ ],
+ });
+ });
+ });
});
diff --git a/spec/frontend/tracking/mock_data.js b/spec/frontend/tracking/mock_data.js
new file mode 100644
index 00000000000..acde8676291
--- /dev/null
+++ b/spec/frontend/tracking/mock_data.js
@@ -0,0 +1,17 @@
+export const extraContext = {
+ schema: 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0',
+ data: {
+ 'design-version-number': '1.0.0',
+ 'design-is-current-version': '1.0.0',
+ 'internal-object-referrer': 'https://gitlab.com',
+ 'design-collection-owner': 'GitLab',
+ },
+};
+
+export const servicePingContext = {
+ schema: 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-1',
+ data: {
+ event_name: 'track_incident_event',
+ data_source: 'redis_hll',
+ },
+};
diff --git a/spec/frontend/tracking/tracking_initialization_spec.js b/spec/frontend/tracking/tracking_initialization_spec.js
index 3c512cf73a7..2dc3c6ab41c 100644
--- a/spec/frontend/tracking/tracking_initialization_spec.js
+++ b/spec/frontend/tracking/tracking_initialization_spec.js
@@ -1,6 +1,6 @@
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData, getAllExperimentContexts } from '~/experimentation/utils';
-import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking';
+import Tracking, { initUserTracking, initDefaultTrackers, InternalEvents } from '~/tracking';
import getStandardContext from '~/tracking/get_standard_context';
jest.mock('~/experimentation/utils', () => ({
@@ -15,6 +15,9 @@ describe('Tracking', () => {
let trackLoadEventsSpy;
let enableFormTracking;
let setAnonymousUrlsSpy;
+ let bindInternalEventDocumentSpy;
+ let trackInternalLoadEventsSpy;
+ let initBrowserSDKSpy;
beforeAll(() => {
window.gl = window.gl || {};
@@ -74,6 +77,15 @@ describe('Tracking', () => {
.spyOn(Tracking, 'enableFormTracking')
.mockImplementation(() => null);
setAnonymousUrlsSpy = jest.spyOn(Tracking, 'setAnonymousUrls').mockImplementation(() => null);
+ bindInternalEventDocumentSpy = jest
+ .spyOn(InternalEvents, 'bindInternalEventDocument')
+ .mockImplementation(() => null);
+ trackInternalLoadEventsSpy = jest
+ .spyOn(InternalEvents, 'trackInternalLoadEvents')
+ .mockImplementation(() => null);
+ initBrowserSDKSpy = jest
+ .spyOn(InternalEvents, 'initBrowserSDK')
+ .mockImplementation(() => null);
});
it('should activate features based on what has been enabled', () => {
@@ -117,6 +129,21 @@ describe('Tracking', () => {
expect(setAnonymousUrlsSpy).toHaveBeenCalled();
});
+ it('binds the document event handling for intenral events', () => {
+ initDefaultTrackers();
+ expect(bindInternalEventDocumentSpy).toHaveBeenCalled();
+ });
+
+ it('tracks page loaded events for internal events', () => {
+ initDefaultTrackers();
+ expect(trackInternalLoadEventsSpy).toHaveBeenCalled();
+ });
+
+ it('calls initBrowserSDKSpy', () => {
+ initDefaultTrackers();
+ expect(initBrowserSDKSpy).toHaveBeenCalled();
+ });
+
describe('when there are experiment contexts', () => {
const experimentContexts = [
{
diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js
index 88ab51cf135..0ae01083a09 100644
--- a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ProjectStorageApp from '~/usage_quotas/storage/components/project_storage_app.vue';
-import UsageGraph from '~/usage_quotas/storage/components/usage_graph.vue';
+import SectionedPercentageBar from '~/usage_quotas/components/sectioned_percentage_bar.vue';
import {
descendingStorageUsageSort,
getStorageTypesFromProjectStatistics,
@@ -56,7 +56,7 @@ describe('ProjectStorageApp', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findUsagePercentage = () => wrapper.findByTestId('total-usage');
- const findUsageGraph = () => wrapper.findComponent(UsageGraph);
+ const findSectionedPercentageBar = () => wrapper.findComponent(SectionedPercentageBar);
const findProjectDetailsTable = () => wrapper.findByTestId('usage-quotas-project-usage-details');
const findNamespaceDetailsTable = () =>
wrapper.findByTestId('usage-quotas-namespace-usage-details');
@@ -157,7 +157,7 @@ describe('ProjectStorageApp', () => {
});
});
- describe('rendering <usage-graph />', () => {
+ describe('rendering <sectioned-percentage-bar />', () => {
let mockApollo;
beforeEach(async () => {
@@ -168,16 +168,23 @@ describe('ProjectStorageApp', () => {
await waitForPromises();
});
- it('renders usage-graph component if project.statistics exists', () => {
- expect(findUsageGraph().exists()).toBe(true);
+ it('renders sectioned-percentage-bar component if project.statistics exists', () => {
+ expect(findSectionedPercentageBar().exists()).toBe(true);
});
- it('passes project.statistics to usage-graph component', () => {
- const {
- __typename,
- ...statistics
- } = mockGetProjectStorageStatisticsGraphQLResponse.data.project.statistics;
- expect(findUsageGraph().props('rootStorageStatistics')).toMatchObject(statistics);
+ it('passes processed project statistics to sectioned-percentage-bar component', () => {
+ expect(findSectionedPercentageBar().props('sections')).toMatchObject([
+ { formattedValue: '4.58 MiB', id: 'lfsObjects', label: 'LFS', value: 4800000 },
+ { formattedValue: '3.72 MiB', id: 'repository', label: 'Repository', value: 3900000 },
+ { formattedValue: '3.62 MiB', id: 'packages', label: 'Packages', value: 3800000 },
+ {
+ formattedValue: '390.63 KiB',
+ id: 'buildArtifacts',
+ label: 'Job artifacts',
+ value: 400000,
+ },
+ { formattedValue: '292.97 KiB', id: 'wiki', label: 'Wiki', value: 300000 },
+ ]);
});
});
});
diff --git a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
deleted file mode 100644
index fc116211bf0..00000000000
--- a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import UsageGraph from '~/usage_quotas/storage/components/usage_graph.vue';
-
-let data;
-let wrapper;
-
-function mountComponent({ rootStorageStatistics, limit }) {
- wrapper = shallowMount(UsageGraph, {
- propsData: {
- rootStorageStatistics,
- limit,
- },
- });
-}
-function findStorageTypeUsagesSerialized() {
- return wrapper
- .findAll('[data-testid="storage-type-usage"]')
- .wrappers.map((wp) => wp.element.style.flex);
-}
-
-describe('UsageGraph', () => {
- beforeEach(() => {
- data = {
- rootStorageStatistics: {
- wikiSize: 5000,
- repositorySize: 4000,
- packagesSize: 3000,
- containerRegistrySize: 2500,
- lfsObjectsSize: 2000,
- buildArtifactsSize: 700,
- snippetsSize: 2000,
- storageSize: 17000,
- },
- limit: 2000,
- };
- mountComponent(data);
- });
-
- it('renders the legend in order', () => {
- const types = wrapper.findAll('[data-testid="storage-type-legend"]');
-
- const {
- buildArtifactsSize,
- lfsObjectsSize,
- packagesSize,
- repositorySize,
- wikiSize,
- snippetsSize,
- } = data.rootStorageStatistics;
-
- expect(types.at(0).text()).toMatchInterpolatedText(`Wiki ${numberToHumanSize(wikiSize)}`);
- expect(types.at(1).text()).toMatchInterpolatedText(
- `Repository ${numberToHumanSize(repositorySize)}`,
- );
- expect(types.at(2).text()).toMatchInterpolatedText(
- `Packages ${numberToHumanSize(packagesSize)}`,
- );
- expect(types.at(3).text()).toMatchInterpolatedText(`LFS ${numberToHumanSize(lfsObjectsSize)}`);
- expect(types.at(4).text()).toMatchInterpolatedText(
- `Snippets ${numberToHumanSize(snippetsSize)}`,
- );
- expect(types.at(5).text()).toMatchInterpolatedText(
- `Job artifacts ${numberToHumanSize(buildArtifactsSize)}`,
- );
- });
-
- describe('when storage type is not used', () => {
- beforeEach(() => {
- data.rootStorageStatistics.wikiSize = 0;
- mountComponent(data);
- });
-
- it('filters the storage type', () => {
- expect(wrapper.text()).not.toContain('Wikis');
- });
- });
-
- describe('when there is no storage usage', () => {
- beforeEach(() => {
- data.rootStorageStatistics.storageSize = 0;
- mountComponent(data);
- });
-
- it('does not render', () => {
- expect(wrapper.html()).toEqual('');
- });
- });
-
- describe('when limit is 0', () => {
- beforeEach(() => {
- data.limit = 0;
- mountComponent(data);
- });
-
- it('sets correct flex values', () => {
- expect(findStorageTypeUsagesSerialized()).toStrictEqual([
- '0.29411764705882354',
- '0.23529411764705882',
- '0.17647058823529413',
- '0.11764705882352941',
- '0.11764705882352941',
- '0.041176470588235294',
- ]);
- });
- });
-
- describe('when storage exceeds limit', () => {
- beforeEach(() => {
- data.limit = data.rootStorageStatistics.storageSize - 1;
- mountComponent(data);
- });
-
- it('does render correclty', () => {
- expect(findStorageTypeUsagesSerialized()).toStrictEqual([
- '0.29411764705882354',
- '0.23529411764705882',
- '0.17647058823529413',
- '0.11764705882352941',
- '0.11764705882352941',
- '0.041176470588235294',
- ]);
- });
- });
-});
diff --git a/spec/frontend/user_lists/components/user_list_spec.js b/spec/frontend/user_lists/components/user_list_spec.js
index 286fb9fef5f..0ed21114778 100644
--- a/spec/frontend/user_lists/components/user_list_spec.js
+++ b/spec/frontend/user_lists/components/user_list_spec.js
@@ -169,7 +169,7 @@ describe('User List', () => {
it('displays the alert message', () => {
const alert = findAlert();
- expect(alert.text()).toBe('Something went wrong on our end. Please try again!');
+ expect(alert.text()).toBe('Unable to load user list. Reload the page and try again.');
});
it('can dismiss the alert', async () => {
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index b38400446a9..5aae922fec2 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -70,7 +70,8 @@ export const findDropdownItemsModel = () =>
return {
type: 'divider',
};
- } else if (el.classList.contains('dropdown-header')) {
+ }
+ if (el.classList.contains('dropdown-header')) {
return {
type: 'dropdown-header',
text: el.textContent,
diff --git a/spec/frontend/vue_merge_request_widget/components/action_buttons.js b/spec/frontend/vue_merge_request_widget/components/action_buttons_spec.js
index 7334f061dc9..02e23b81413 100644
--- a/spec/frontend/vue_merge_request_widget/components/action_buttons.js
+++ b/spec/frontend/vue_merge_request_widget/components/action_buttons_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlDropdownItem } from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Actions from '~/vue_merge_request_widget/components/action_buttons.vue';
@@ -6,7 +6,7 @@ let wrapper;
function factory(propsData = {}) {
wrapper = shallowMount(Actions, {
- propsData: { ...propsData, widget: 'test' },
+ propsData,
});
}
@@ -33,11 +33,29 @@ describe('MR widget extension actions', () => {
});
it('renders tertiary actions in dropdown', () => {
+ const action = { text: 'hello world', href: 'https://gitlab.com', target: '_blank' };
factory({
- tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
+ tertiaryButtons: [action, action],
});
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(1);
+ const component = wrapper.findComponent(GlDisclosureDropdown);
+ expect(component.exists()).toBe(true);
+ expect(component.props('items')).toMatchObject([
+ {
+ text: action.text,
+ href: action.href,
+ extraAttrs: {
+ target: action.target,
+ },
+ },
+ {
+ text: action.text,
+ href: action.href,
+ extraAttrs: {
+ target: action.target,
+ },
+ },
+ ]);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
index a0064224b46..35b4e222e01 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
@@ -6,7 +6,7 @@ import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MRWidgetPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import { SUCCESS } from '~/vue_merge_request_widget/constants';
import mockData from '../mock_data';
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
index f9936f22ea3..ecf4040cbda 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/new_ready_to_merge_spec.js.snap
@@ -2,36 +2,30 @@
exports[`New ready to merge state component renders permission text if canMerge (false) is false 1`] = `
<div
- class="mr-widget-body media"
+ class="media mr-widget-body"
>
<status-icon-stub
status="success"
/>
-
<p
- class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!"
+ class="gl-font-weight-bold gl-m-0! gl-text-gray-900! media-body"
>
-
- Ready to merge by members who can write to the target branch.
-
+ Ready to merge by members who can write to the target branch.
</p>
</div>
`;
exports[`New ready to merge state component renders permission text if canMerge (true) is false 1`] = `
<div
- class="mr-widget-body media"
+ class="media mr-widget-body"
>
<status-icon-stub
status="success"
/>
-
<p
- class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!"
+ class="gl-font-weight-bold gl-m-0! gl-text-gray-900! media-body"
>
-
- Ready to merge!
-
+ Ready to merge!
</p>
</div>
`;
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
index e44e2834a0e..5efb1dcce42 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js
@@ -1,4 +1,4 @@
-import { getByRole } from '@testing-library/dom';
+import { getAllByRole } from '@testing-library/dom';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
@@ -132,7 +132,7 @@ describe('MRWidgetMerged', () => {
createComponent();
const eventHubSpy = jest.spyOn(modalEventHub, '$emit');
- getByRole(wrapper.element, 'button', { name: /Revert/i }).click();
+ getAllByRole(wrapper.element, 'button', { name: /Revert/i })[0].click();
expect(eventHubSpy).toHaveBeenCalledWith(OPEN_REVERT_MODAL);
});
@@ -141,7 +141,7 @@ describe('MRWidgetMerged', () => {
createComponent();
const eventHubSpy = jest.spyOn(modalEventHub, '$emit');
- getByRole(wrapper.element, 'button', { name: /Cherry-pick/i }).click();
+ getAllByRole(wrapper.element, 'button', { name: /Cherry-pick/i })[0].click();
expect(eventHubSpy).toHaveBeenCalledWith(OPEN_CHERRY_PICK_MODAL);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
index ce4bf11f16b..d5d3f56e451 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
@@ -1,63 +1,133 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue renders given data 1`] = `
-"<div class=\\"gl-display-flex gl-border-t gl-py-3 gl-pl-7 gl-align-items-baseline\\">
- <!---->
- <div class=\\"gl-w-full gl-min-w-0\\">
- <div class=\\"gl-display-flex\\">
- <div class=\\"gl-mb-2\\"><strong class=\\"gl-display-block\\">This is a header</strong><span class=\\"gl-display-block\\">This is a subheader</span></div>
- <div class=\\"gl-ml-auto gl-display-flex gl-align-items-baseline\\">
- <help-popover-stub options=\\"[object Object]\\" icon=\\"information-o\\" triggerclass=\\"\\" class=\\"\\">
- <p class=\\"gl-mb-0\\">Widget help popover content</p>
- <!---->
+<div
+ class="gl-align-items-baseline gl-border-t gl-display-flex gl-pl-7 gl-py-3"
+>
+ <div
+ class="gl-min-w-0 gl-w-full"
+ >
+ <div
+ class="gl-display-flex"
+ >
+ <div
+ class="gl-mb-2"
+ >
+ <strong
+ class="gl-display-block"
+ >
+ This is a header
+ </strong>
+ <span
+ class="gl-display-block"
+ >
+ This is a subheader
+ </span>
+ </div>
+ <div
+ class="gl-align-items-baseline gl-display-flex gl-ml-auto"
+ >
+ <help-popover-stub
+ icon="information-o"
+ options="[object Object]"
+ triggerclass=""
+ >
+ <p
+ class="gl-mb-0"
+ >
+ Widget help popover content
+ </p>
</help-popover-stub>
- <!---->
</div>
</div>
- <div class=\\"gl-display-flex gl-align-items-baseline\\">
- <status-icon-stub level=\\"2\\" name=\\"MyWidget\\" iconname=\\"success\\"></status-icon-stub>
- <div class=\\"gl-w-full gl-display-flex\\">
- <div class=\\"gl-display-flex gl-flex-grow-1\\">
- <div class=\\"gl-display-flex gl-flex-grow-1 gl-align-items-baseline\\">
+ <div
+ class="gl-align-items-baseline gl-display-flex"
+ >
+ <status-icon-stub
+ iconname="success"
+ level="2"
+ name="MyWidget"
+ />
+ <div
+ class="gl-display-flex gl-w-full"
+ >
+ <div
+ class="gl-display-flex gl-flex-grow-1"
+ >
+ <div
+ class="gl-align-items-baseline gl-display-flex gl-flex-grow-1"
+ >
<div>
- <p class=\\"gl-mb-0 gl-mr-1\\">Main text for the row</p>
- <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
- <!---->
+ <p
+ class="gl-mb-0 gl-mr-1"
+ >
+ Main text for the row
+ </p>
+ <gl-link-stub
+ href="https://gitlab.com"
+ >
+ Optional link to display after text
+ </gl-link-stub>
</div>
- <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
+ <gl-badge-stub
+ iconsize="md"
+ size="md"
+ variant="info"
+ >
Badge is optional. Text to be displayed inside badge
</gl-badge-stub>
</div>
- <!---->
- <p class=\\"gl-m-0 gl-font-sm\\">Optional: Smaller sub-text to be displayed below the main text</p>
+ <p
+ class="gl-font-sm gl-m-0"
+ >
+ Optional: Smaller sub-text to be displayed below the main text
+ </p>
</div>
- <ul class=\\"gl-m-0 gl-p-0 gl-list-style-none\\">
+ <ul
+ class="gl-list-style-none gl-m-0 gl-p-0"
+ >
<li>
- <div class=\\"gl-display-flex gl-align-items-center\\" data-qa-selector=\\"child_content\\">
- <!---->
- <div class=\\"gl-w-full gl-min-w-0\\">
- <div class=\\"gl-display-flex\\">
- <div class=\\"gl-mb-2\\"><strong class=\\"gl-display-block\\">Child row header</strong>
- <!---->
+ <div
+ class="gl-align-items-center gl-display-flex"
+ data-qa-selector="child_content"
+ >
+ <div
+ class="gl-min-w-0 gl-w-full"
+ >
+ <div
+ class="gl-display-flex"
+ >
+ <div
+ class="gl-mb-2"
+ >
+ <strong
+ class="gl-display-block"
+ >
+ Child row header
+ </strong>
</div>
- <!---->
</div>
- <div class=\\"gl-display-flex gl-align-items-baseline\\">
- <!---->
- <div class=\\"gl-w-full gl-display-flex\\">
- <div class=\\"gl-display-flex gl-flex-grow-1\\">
- <div class=\\"gl-display-flex gl-flex-grow-1 gl-align-items-baseline\\">
+ <div
+ class="gl-align-items-baseline gl-display-flex"
+ >
+ <div
+ class="gl-display-flex gl-w-full"
+ >
+ <div
+ class="gl-display-flex gl-flex-grow-1"
+ >
+ <div
+ class="gl-align-items-baseline gl-display-flex gl-flex-grow-1"
+ >
<div>
- <p class=\\"gl-mb-0 gl-mr-1\\">This is recursive. It will be listed in level 3.</p>
- <!---->
- <!---->
+ <p
+ class="gl-mb-0 gl-mr-1"
+ >
+ This is recursive. It will be listed in level 3.
+ </p>
</div>
- <!---->
</div>
- <!---->
- <!---->
</div>
- <!---->
</div>
</div>
</div>
@@ -67,5 +137,5 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
</div>
</div>
</div>
-</div>"
+</div>
`;
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
index bf318cd6b88..205824c3edd 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js
@@ -1,13 +1,23 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import App from '~/vue_merge_request_widget/components/widget/app.vue';
+import MrSecurityWidgetCE from '~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue';
+import MrTestReportWidget from '~/vue_merge_request_widget/extensions/test_report/index.vue';
+import MrTerraformWidget from '~/vue_merge_request_widget/extensions/terraform/index.vue';
+import MrCodeQualityWidget from '~/vue_merge_request_widget/extensions/code_quality/index.vue';
describe('MR Widget App', () => {
let wrapper;
- const createComponent = () => {
+ const createComponent = ({ mr = {} } = {}) => {
wrapper = shallowMountExtended(App, {
propsData: {
- mr: {},
+ mr: {
+ pipeline: {
+ path: '/path/to/pipeline',
+ },
+ ...mr,
+ },
},
});
};
@@ -16,4 +26,35 @@ describe('MR Widget App', () => {
createComponent();
expect(wrapper.findByTestId('mr-widget-app').exists()).toBe(true);
});
+
+ describe('MRSecurityWidget', () => {
+ it('mounts MrSecurityWidgetCE', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(MrSecurityWidgetCE).exists()).toBe(true);
+ });
+ });
+
+ describe.each`
+ widgetName | widget | endpoint
+ ${'testReportWidget'} | ${MrTestReportWidget} | ${'testResultsPath'}
+ ${'terraformPlansWidget'} | ${MrTerraformWidget} | ${'terraformReportsPath'}
+ ${'codeQualityWidget'} | ${MrCodeQualityWidget} | ${'codequalityReportsPath'}
+ `('$widgetName', ({ widget, endpoint }) => {
+ it(`is mounted when ${endpoint} is defined`, async () => {
+ createComponent({ mr: { [endpoint]: `path/to/${endpoint}` } });
+ await waitForPromises();
+
+ expect(wrapper.findComponent(widget).exists()).toBe(true);
+ });
+
+ it(`is not mounted when ${endpoint} is not defined`, async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(widget).exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
index b901b80e8bf..6f5e08a0829 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js
@@ -2,7 +2,6 @@ import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import { visitUrl } from '~/lib/utils/url_utility';
import {
CREATED,
MANUAL_DEPLOY,
@@ -20,6 +19,7 @@ import {
deploymentMockData,
playDetails,
retryDetails,
+ mockRedeployProps,
} from './deployment_mock_data';
jest.mock('~/alert');
@@ -36,7 +36,6 @@ describe('DeploymentAction component', () => {
const findStopButton = () => wrapper.find('.js-stop-env');
const findDeployButton = () => wrapper.find('.js-manual-deploy-action');
- const findManualRedeployButton = () => wrapper.find('.js-manual-redeploy-action');
const findRedeployButton = () => wrapper.find('.js-redeploy-action');
beforeEach(() => {
@@ -78,23 +77,25 @@ describe('DeploymentAction component', () => {
expect(findDeployButton().exists()).toBe(false);
});
});
-
- describe('when there is no retry_path in details', () => {
- it('the manual redeploy button does not appear', () => {
- expect(findManualRedeployButton().exists()).toBe(false);
- });
- });
});
describe('when conditions are met', () => {
describe.each`
- configConst | computedDeploymentStatus | displayConditionChanges | finderFn | endpoint
- ${STOPPING} | ${CREATED} | ${{}} | ${findStopButton} | ${deploymentMockData.stop_url}
- ${DEPLOYING} | ${MANUAL_DEPLOY} | ${playDetails} | ${findDeployButton} | ${playDetails.playable_build.play_path}
- ${REDEPLOYING} | ${FAILED} | ${retryDetails} | ${findManualRedeployButton} | ${retryDetails.playable_build.retry_path}
+ configConst | computedDeploymentStatus | displayConditionChanges | finderFn | endpoint | props
+ ${STOPPING} | ${CREATED} | ${{}} | ${findStopButton} | ${deploymentMockData.stop_url} | ${{}}
+ ${DEPLOYING} | ${MANUAL_DEPLOY} | ${playDetails} | ${findDeployButton} | ${playDetails.playable_build.play_path} | ${{}}
+ ${REDEPLOYING} | ${FAILED} | ${{}} | ${findRedeployButton} | ${retryDetails.playable_build.retry_path} | ${mockRedeployProps}
+ ${REDEPLOYING} | ${SUCCESS} | ${{}} | ${findRedeployButton} | ${retryDetails.playable_build.retry_path} | ${mockRedeployProps}
`(
'$configConst action',
- ({ configConst, computedDeploymentStatus, displayConditionChanges, finderFn, endpoint }) => {
+ ({
+ configConst,
+ computedDeploymentStatus,
+ displayConditionChanges,
+ finderFn,
+ endpoint,
+ props,
+ }) => {
describe(`${configConst} action`, () => {
beforeEach(() => {
factory({
@@ -103,6 +104,7 @@ describe('DeploymentAction component', () => {
deployment: {
...deploymentMockData,
details: displayConditionChanges,
+ ...props,
},
},
});
@@ -163,25 +165,6 @@ describe('DeploymentAction component', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- describe('response includes redirect_url', () => {
- const url = '/root/example';
- beforeEach(async () => {
- executeActionSpy.mockResolvedValueOnce({
- data: { redirect_url: url },
- });
-
- await waitForPromises();
-
- confirmAction.mockResolvedValueOnce(true);
- finderFn().trigger('click');
- });
-
- it('calls visit url with the redirect_url', () => {
- expect(visitUrl).toHaveBeenCalled();
- expect(visitUrl).toHaveBeenCalledWith(url);
- });
- });
-
describe('it should call the executeAction method', () => {
beforeEach(async () => {
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
@@ -234,7 +217,7 @@ describe('DeploymentAction component', () => {
);
});
- describe('with the reviewAppsRedeployMrWidget feature flag turned on', () => {
+ describe('redeploy action', () => {
beforeEach(() => {
factory({
propsData: {
@@ -246,11 +229,6 @@ describe('DeploymentAction component', () => {
environment_available: false,
},
},
- provide: {
- glFeatures: {
- reviewAppsRedeployMrWidget: true,
- },
- },
});
});
@@ -304,24 +282,6 @@ describe('DeploymentAction component', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- describe('response includes redirect_url', () => {
- const url = '/root/example';
- beforeEach(async () => {
- executeActionSpy.mockResolvedValueOnce({
- data: { redirect_url: url },
- });
-
- await waitForPromises();
-
- confirmAction.mockResolvedValueOnce(true);
- findRedeployButton().trigger('click');
- });
-
- it('does not call visit url', () => {
- expect(visitUrl).not.toHaveBeenCalled();
- });
- });
-
describe('it should call the executeAction method', () => {
beforeEach(async () => {
jest.spyOn(wrapper.vm, 'executeAction').mockImplementation();
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js
index 374fe4e1b95..2c6a40c6e16 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_mock_data.js
@@ -74,4 +74,9 @@ const retryDetails = {
},
};
-export { actionButtonMocks, deploymentMockData, playDetails, retryDetails };
+const mockRedeployProps = {
+ retry_url: retryDetails.playable_build.retry_path,
+ environment_available: false,
+};
+
+export { actionButtonMocks, deploymentMockData, playDetails, retryDetails, mockRedeployProps };
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
index 234491c531a..0a96feb184f 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
@@ -10,7 +10,12 @@ import {
import DeploymentComponent from '~/vue_merge_request_widget/components/deployment/deployment.vue';
import DeploymentInfo from '~/vue_merge_request_widget/components/deployment/deployment_info.vue';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
-import { deploymentMockData, playDetails, retryDetails } from './deployment_mock_data';
+import {
+ deploymentMockData,
+ playDetails,
+ retryDetails,
+ mockRedeployProps,
+} from './deployment_mock_data';
describe('Deployment component', () => {
let wrapper;
@@ -46,7 +51,6 @@ describe('Deployment component', () => {
};
const defaultGroup = ['.js-deploy-url', '.js-stop-env'];
const manualDeployGroup = ['.js-manual-deploy-action', ...defaultGroup];
- const manualRedeployGroup = ['.js-manual-redeploy-action', ...defaultGroup];
describe.each`
status | previous | deploymentDetails | text | actionButtons
@@ -62,7 +66,7 @@ describe('Deployment component', () => {
${SUCCESS} | ${true} | ${noDetails} | ${'Deployed to'} | ${defaultGroup}
${SUCCESS} | ${false} | ${deployDetail} | ${'Deployed to'} | ${defaultGroup}
${SUCCESS} | ${false} | ${noDetails} | ${'Deployed to'} | ${defaultGroup}
- ${FAILED} | ${true} | ${retryDetail} | ${'Failed to deploy to'} | ${manualRedeployGroup}
+ ${FAILED} | ${true} | ${retryDetail} | ${'Failed to deploy to'} | ${defaultGroup}
${FAILED} | ${true} | ${noDetails} | ${'Failed to deploy to'} | ${defaultGroup}
${FAILED} | ${false} | ${retryDetail} | ${'Failed to deploy to'} | ${noActions}
${FAILED} | ${false} | ${noDetails} | ${'Failed to deploy to'} | ${noActions}
@@ -139,6 +143,27 @@ describe('Deployment component', () => {
}
},
);
+
+ describe('redeploy action', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ showMetrics: false,
+ deployment: {
+ ...deploymentMockData,
+ ...mockRedeployProps,
+ },
+ },
+ });
+ });
+
+ it('shows only the redeploy button', () => {
+ expect(wrapper.find('.js-redeploy-action').exists()).toBe(true);
+ expect(wrapper.find('.js-deploy-url').exists()).toBe(false);
+ expect(wrapper.find('.js-stop-env').exists()).toBe(false);
+ expect(wrapper.find('.js-manual-deploy-action').exists()).toBe(false);
+ });
+ });
});
describe('hasExternalUrls', () => {
diff --git a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
index d2d622d0534..88c348629cb 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
@@ -1,19 +1,17 @@
import { nextTick } from 'vue';
import MockAdapter from 'axios-mock-adapter';
-import testReportExtension from '~/vue_merge_request_widget/extensions/test_report';
+import testReportExtension from '~/vue_merge_request_widget/extensions/test_report/index.vue';
import { i18n } from '~/vue_merge_request_widget/extensions/test_report/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
-import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
-import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
+import TestCaseDetails from '~/ci/pipeline_details/test_reports/test_case_details.vue';
import { failedReport } from 'jest/ci/reports/mock_data/mock_data';
import mixedResultsTestReports from 'jest/ci/reports/mock_data/new_and_fixed_failures_report.json';
@@ -34,12 +32,10 @@ describe('Test report extension', () => {
let wrapper;
let mock;
- registerExtension(testReportExtension);
-
const endpoint = '/root/repo/-/merge_requests/4/test_reports.json';
const mockApi = (statusCode, data = mixedResultsTestReports) => {
- mock.onGet(endpoint).reply(statusCode, data);
+ mock.onGet(endpoint).reply(statusCode, data, {});
};
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
@@ -49,7 +45,7 @@ describe('Test report extension', () => {
const findModal = () => wrapper.findComponent(TestCaseDetails);
const createComponent = () => {
- wrapper = mountExtended(extensionsContainer, {
+ wrapper = mountExtended(testReportExtension, {
propsData: {
mr: {
testResultsPath: endpoint,
@@ -84,7 +80,7 @@ describe('Test report extension', () => {
expect(wrapper.text()).toContain(i18n.loading);
});
- it('with a 204 response, continues to display loading state', async () => {
+ it('with a "no content" response, continues to display loading state', async () => {
mockApi(HTTP_STATUS_NO_CONTENT, '');
createComponent();
@@ -269,7 +265,7 @@ describe('Test report extension', () => {
beforeEach(async () => {
await createExpandedWidgetWithData();
- wrapper.findByTestId('modal-link').trigger('click');
+ wrapper.findByTestId('extension-actions-button').trigger('click');
});
it('opens a modal to display test case details', () => {
diff --git a/spec/frontend/vue_merge_request_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js
index 5b3f533f34e..34f147307fc 100644
--- a/spec/frontend/vue_merge_request_widget/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/mock_data.js
@@ -352,7 +352,7 @@ export default {
merge_request_widget_path: '/root/acets-app/-/merge_requests/22/widget.json',
merge_request_cached_widget_path: '/cached.json',
merge_check_path: '/root/acets-app/-/merge_requests/22/merge_check',
- ci_environments_status_url: '/root/acets-app/-/merge_requests/22/ci_environments_status',
+ ci_environments_status_path: '/root/acets-app/-/merge_requests/22/ci_environments_status',
project_archived: false,
default_merge_commit_message_with_description:
"Merge branch 'daaaa' into 'main'\n\nUpdate README.md\n\nSee merge request !22",
@@ -452,3 +452,169 @@ export const mockStore = {
hasCI: true,
exposedArtifactsPath: 'exposed_artifacts.json',
};
+
+export const mockMergePipeline = {
+ id: 127,
+ user: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ status_tooltip_html: null,
+ path: '/root',
+ },
+ active: true,
+ coverage: null,
+ source: 'push',
+ created_at: '2018-10-22T11:41:35.186Z',
+ updated_at: '2018-10-22T11:41:35.433Z',
+ path: '/root/ci-web-terminal/pipelines/127',
+ flags: {
+ latest: true,
+ stuck: true,
+ auto_devops: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: true,
+ failure_reason: false,
+ },
+ details: {
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/root/ci-web-terminal/pipelines/127',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
+ },
+ duration: null,
+ finished_at: null,
+ stages: [
+ {
+ name: 'test',
+ title: 'test: pending',
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/root/ci-web-terminal/pipelines/127#test',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
+ },
+ path: '/root/ci-web-terminal/pipelines/127#test',
+ dropdown_path: '/root/ci-web-terminal/pipelines/127/stage.json?stage=test',
+ },
+ ],
+ artifacts: [],
+ manual_actions: [],
+ scheduled_actions: [],
+ },
+ ref: {
+ name: 'main',
+ path: '/root/ci-web-terminal/commits/main',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'aa1939133d373c94879becb79d91828a892ee319',
+ short_id: 'aa193913',
+ title: "Merge branch 'main-test' into 'main'",
+ created_at: '2018-10-22T11:41:33.000Z',
+ parent_ids: [
+ '4622f4dd792468993003caf2e3be978798cbe096',
+ '76598df914cdfe87132d0c3c40f80db9fa9396a4',
+ ],
+ message:
+ "Merge branch 'main-test' into 'main'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1",
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2018-10-22T11:41:33.000Z',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2018-10-22T11:41:33.000Z',
+ author: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ web_url: 'http://localhost:3000/root',
+ status_tooltip_html: null,
+ path: '/root',
+ },
+ author_gravatar_url: null,
+ commit_url:
+ 'http://localhost:3000/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
+ commit_path: '/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
+ },
+ cancel_path: '/root/ci-web-terminal/pipelines/127/cancel',
+};
+
+export const mockPostMergeDeployments = [
+ {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/acets-review-apps/environments/15',
+ stop_url: '/root/acets-review-apps/environments/15/stop',
+ metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
+ external_url: 'http://diplo.',
+ external_url_formatted: 'diplo.',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ changes: [
+ {
+ path: 'index.html',
+ external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/index.html',
+ },
+ {
+ path: 'imgs/gallery.html',
+ external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
+ },
+ {
+ path: 'about/',
+ external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/about/',
+ },
+ ],
+ status: 'success',
+ },
+];
+
+export const mockDeployment = {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/acets-review-apps/environments/15',
+ stop_url: '/root/acets-review-apps/environments/15/stop',
+ metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
+ external_url: 'http://diplo.',
+ external_url_formatted: 'diplo.',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ changes: [
+ {
+ path: 'index.html',
+ external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/index.html',
+ },
+ {
+ path: 'imgs/gallery.html',
+ external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
+ },
+ {
+ path: 'about/',
+ external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/about/',
+ },
+ ],
+ status: SUCCESS,
+ environment_available: true,
+};
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index ecb5a8448f9..09f58f17fd9 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -21,12 +21,15 @@ import {
registerExtension,
registeredExtensions,
} from '~/vue_merge_request_widget/components/extensions';
+import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import { STATE_QUERY_POLLING_INTERVAL_BACKOFF } from '~/vue_merge_request_widget/constants';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
+import ConflictsState from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
+import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
import WidgetContainer from '~/vue_merge_request_widget/components/widget/app.vue';
import WidgetSuggestPipeline from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
@@ -40,7 +43,7 @@ import approvedBySubscription from 'ee_else_ce/vue_merge_request_widget/componen
import userPermissionsQuery from '~/vue_merge_request_widget/queries/permissions.query.graphql';
import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
-import mockData from './mock_data';
+import mockData, { mockDeployment, mockMergePipeline, mockPostMergeDeployments } from './mock_data';
import {
workingExtension,
collapsedDataErrorExtension,
@@ -60,9 +63,7 @@ jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon');
jest.mock('@sentry/browser', () => ({
- setExtra: jest.fn(),
- setExtras: jest.fn(),
- captureMessage: jest.fn(),
+ ...jest.requireActual('@sentry/browser'),
captureException: jest.fn(),
}));
@@ -76,36 +77,25 @@ describe('MrWidgetOptions', () => {
let stateSubscription;
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
- const findApprovalsWidget = () => wrapper.findComponent(Approvals);
- const findPreparingWidget = () => wrapper.findComponent(Preparing);
- const findMergedPipelineContainer = () => wrapper.findByTestId('merged-pipeline-container');
- const findPipelineContainer = () => wrapper.findByTestId('pipeline-container');
- const findAlertMessage = () => wrapper.findComponent(MrWidgetAlertMessage);
-
- beforeEach(() => {
- gl.mrWidgetData = { ...mockData };
- gon.features = { asyncMrWidget: true };
- mock = new MockAdapter(axios);
- mock.onGet(mockData.merge_request_widget_path).reply(() => [HTTP_STATUS_OK, { ...mockData }]);
+ const setInitialData = (data) => {
+ gl.mrWidgetData = { ...mockData, ...data };
+ mock
+ .onGet(mockData.merge_request_widget_path)
+ .reply(() => [HTTP_STATUS_OK, { ...mockData, ...data }]);
mock
.onGet(mockData.merge_request_cached_widget_path)
- .reply(() => [HTTP_STATUS_OK, { ...mockData }]);
- });
-
- afterEach(() => {
- mock.restore();
- // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
- wrapper.destroy();
- gl.mrWidgetData = {};
- });
+ .reply(() => [HTTP_STATUS_OK, { ...mockData, ...data }]);
+ };
const createComponent = ({
- mrData = mockData,
+ updatedMrData = {},
options = {},
data = {},
mountFn = shallowMountExtended,
} = {}) => {
+ setInitialData(updatedMrData);
+ const mrData = { ...mockData, ...updatedMrData };
const mockedApprovalsSubscription = createMockApolloSubscription();
queryResponse = {
data: {
@@ -153,9 +143,7 @@ describe('MrWidgetOptions', () => {
});
wrapper = mountFn(MrWidgetOptions, {
- propsData: {
- mrData: { ...mrData },
- },
+ propsData: { mrData },
data() {
return {
loading: false,
@@ -170,6 +158,12 @@ describe('MrWidgetOptions', () => {
return axios.waitForAll();
};
+ const findApprovalsWidget = () => wrapper.findComponent(Approvals);
+ const findPreparingWidget = () => wrapper.findComponent(Preparing);
+ const findMergedPipelineContainer = () => wrapper.findByTestId('merged-pipeline-container');
+ const findPipelineContainer = () => wrapper.findByTestId('pipeline-container');
+ const findAlertMessage = () => wrapper.findComponent(MrWidgetAlertMessage);
+ const findMergePipelineForkAlert = () => wrapper.findByTestId('merge-pipeline-fork-warning');
const findExtensionToggleButton = () =>
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
const findExtensionLink = (linkHref) =>
@@ -177,23 +171,25 @@ describe('MrWidgetOptions', () => {
const findSuggestPipeline = () => wrapper.findComponent(WidgetSuggestPipeline);
const findWidgetContainer = () => wrapper.findComponent(WidgetContainer);
- describe('default', () => {
- beforeEach(() => {
- jest.spyOn(document, 'dispatchEvent');
- return createComponent();
- });
+ beforeEach(() => {
+ gon.features = { asyncMrWidget: true };
+ mock = new MockAdapter(axios);
+ });
- // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/385238
- // eslint-disable-next-line jest/no-disabled-tests
- describe.skip('data', () => {
- it('should instantiate Store and Service', () => {
- expect(wrapper.vm.mr).toBeDefined();
- expect(wrapper.vm.service).toBeDefined();
- });
- });
+ afterEach(() => {
+ mock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
+ gl.mrWidgetData = {};
+ });
+ describe('default', () => {
describe('computed', () => {
describe('componentName', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
// quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/409365
// eslint-disable-next-line jest/no-disabled-tests
it.skip.each`
@@ -205,137 +201,134 @@ describe('MrWidgetOptions', () => {
});
it.each`
- state | componentName
- ${'conflicts'} | ${'mr-widget-conflicts'}
- ${'shaMismatch'} | ${'sha-mismatch'}
- `('should translate $state into $componentName', ({ state, componentName }) => {
- wrapper.vm.mr.state = state;
-
- expect(wrapper.vm.componentName).toEqual(componentName);
+ state | componentName | component
+ ${'conflicts'} | ${'ConflictsState'} | ${ConflictsState}
+ ${'shaMismatch'} | ${'ShaMismatch'} | ${ShaMismatch}
+ `('should translate $state into $componentName component', async ({ state, component }) => {
+ Vue.set(wrapper.vm.mr, 'state', state);
+ await nextTick();
+ expect(wrapper.findComponent(component).exists()).toBe(true);
});
});
describe('MrWidgetPipelineContainer', () => {
- it('should return true when hasCI is true', async () => {
- wrapper.vm.mr.hasCI = true;
- await nextTick();
+ it('renders the pipeline container when it has CI', () => {
+ createComponent({ updatedMrData: { has_ci: true } });
expect(findPipelineContainer().exists()).toBe(true);
});
- it('should return false when hasCI is false', async () => {
- wrapper.vm.mr.hasCI = false;
- await nextTick();
-
+ it('does not render the pipeline container when it does not have CI', () => {
+ createComponent({ updatedMrData: { has_ci: false } });
expect(findPipelineContainer().exists()).toBe(false);
});
});
describe('shouldRenderCollaborationStatus', () => {
- describe('when collaboration is allowed', () => {
- beforeEach(() => {
- wrapper.vm.mr.allowCollaboration = true;
- });
-
- describe('when merge request is opened', () => {
- beforeEach(() => {
- wrapper.vm.mr.isOpen = true;
- return nextTick();
- });
-
- it('should render collaboration status', () => {
- expect(wrapper.text()).toContain(COLLABORATION_MESSAGE);
- });
+ it('renders collaboration message when collaboration is allowed and the MR is open', () => {
+ createComponent({
+ updatedMrData: { allow_collaboration: true, state: STATUS_OPEN, not: false },
});
-
- describe('when merge request is not opened', () => {
- beforeEach(() => {
- wrapper.vm.mr.isOpen = false;
- return nextTick();
- });
-
- it('should not render collaboration status', () => {
- expect(wrapper.text()).not.toContain(COLLABORATION_MESSAGE);
- });
+ expect(findPipelineContainer().props('mr')).toMatchObject({
+ allowCollaboration: true,
+ isOpen: true,
});
+ expect(wrapper.text()).toContain(COLLABORATION_MESSAGE);
});
- describe('when collaboration is not allowed', () => {
- beforeEach(() => {
- wrapper.vm.mr.allowCollaboration = false;
+ it('does not render collaboration message when collaboration is allowed and the MR is closed', () => {
+ createComponent({
+ updatedMrData: { allow_collaboration: true, state: STATUS_CLOSED, not: true },
});
-
- describe('when merge request is opened', () => {
- beforeEach(() => {
- wrapper.vm.mr.isOpen = true;
- return nextTick();
- });
-
- it('should not render collaboration status', () => {
- expect(wrapper.text()).not.toContain(COLLABORATION_MESSAGE);
- });
+ expect(findPipelineContainer().props('mr')).toMatchObject({
+ allowCollaboration: true,
+ isOpen: false,
});
+ expect(wrapper.text()).not.toContain(COLLABORATION_MESSAGE);
});
- });
- describe('showMergePipelineForkWarning', () => {
- describe('when the source project and target project are the same', () => {
- beforeEach(() => {
- Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', true);
- Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
- Vue.set(wrapper.vm.mr, 'targetProjectId', 1);
- return nextTick();
+ it('does not render collaboration message when collaboration is not allowed and the MR is closed', () => {
+ createComponent({
+ updatedMrData: { allow_collaboration: undefined, state: STATUS_CLOSED, not: true },
});
-
- it('should be false', () => {
- expect(findAlertMessage().exists()).toBe(false);
+ expect(findPipelineContainer().props('mr')).toMatchObject({
+ allowCollaboration: undefined,
+ isOpen: false,
});
+ expect(wrapper.text()).not.toContain(COLLABORATION_MESSAGE);
});
- describe('when merge pipelines are not enabled', () => {
- beforeEach(() => {
- Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', false);
- Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
- Vue.set(wrapper.vm.mr, 'targetProjectId', 2);
- return nextTick();
+ it('does not render collaboration message when collaboration is not allowed and the MR is open', () => {
+ createComponent({
+ updatedMrData: { allow_collaboration: undefined, state: STATUS_OPEN, not: true },
+ });
+ expect(findPipelineContainer().props('mr')).toMatchObject({
+ allowCollaboration: undefined,
+ isOpen: true,
});
+ expect(wrapper.text()).not.toContain(COLLABORATION_MESSAGE);
+ });
+ });
- it('should be false', () => {
- expect(findAlertMessage().exists()).toBe(false);
+ describe('showMergePipelineForkWarning', () => {
+ it('hides the alert when the source project and target project are the same', async () => {
+ createComponent({
+ updatedMrData: {
+ source_project_id: 1,
+ target_project_id: 1,
+ },
});
+ await nextTick();
+ Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', true);
+ await nextTick();
+ expect(findMergePipelineForkAlert().exists()).toBe(false);
});
- describe('when merge pipelines are enabled _and_ the source project and target project are different', () => {
- beforeEach(() => {
- Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', true);
- Vue.set(wrapper.vm.mr, 'sourceProjectId', 1);
- Vue.set(wrapper.vm.mr, 'targetProjectId', 2);
- return nextTick();
+ it('hides the alert when merge pipelines are not enabled', async () => {
+ createComponent({
+ updatedMrData: {
+ source_project_id: 1,
+ target_project_id: 2,
+ },
});
+ await nextTick();
+ expect(findMergePipelineForkAlert().exists()).toBe(false);
+ });
- it('should be true', () => {
- expect(findAlertMessage().exists()).toBe(true);
+ it('shows the alert when merge pipelines are enabled and the source project and target project are different', async () => {
+ createComponent({
+ updatedMrData: {
+ source_project_id: 1,
+ target_project_id: 2,
+ },
});
+ await nextTick();
+ Vue.set(wrapper.vm.mr, 'mergePipelinesEnabled', true);
+ await nextTick();
+ expect(findMergePipelineForkAlert().exists()).toBe(true);
});
});
describe('formattedHumanAccess', () => {
- it('when user is a tool admin but not a member of project', async () => {
- wrapper.vm.mr.humanAccess = null;
- wrapper.vm.mr.mergeRequestAddCiConfigPath = 'test';
- wrapper.vm.mr.hasCI = false;
- wrapper.vm.mr.isDismissedSuggestPipeline = false;
- await nextTick();
-
+ it('renders empty string when user is a tool admin but not a member of project', () => {
+ createComponent({
+ updatedMrData: {
+ human_access: null,
+ merge_request_add_ci_config_path: 'test',
+ has_ci: false,
+ is_dismissed_suggest_pipeline: false,
+ },
+ });
expect(findSuggestPipeline().props('humanAccess')).toBe('');
});
-
- it('when user a member of the project', async () => {
- wrapper.vm.mr.humanAccess = 'Owner';
- wrapper.vm.mr.mergeRequestAddCiConfigPath = 'test';
- wrapper.vm.mr.hasCI = false;
- wrapper.vm.mr.isDismissedSuggestPipeline = false;
- await nextTick();
-
+ it('renders human access when user is a member of the project', () => {
+ createComponent({
+ updatedMrData: {
+ human_access: 'Owner',
+ merge_request_add_ci_config_path: 'test',
+ has_ci: false,
+ is_dismissed_suggest_pipeline: false,
+ },
+ });
expect(findSuggestPipeline().props('humanAccess')).toBe('owner');
});
});
@@ -343,33 +336,50 @@ describe('MrWidgetOptions', () => {
describe('methods', () => {
describe('checkStatus', () => {
- let cb;
- let isCbExecuted;
-
- beforeEach(() => {
- jest.spyOn(wrapper.vm.service, 'checkStatus').mockResolvedValue({ data: mockData });
- jest.spyOn(wrapper.vm.mr, 'setData').mockImplementation(() => {});
- jest.spyOn(wrapper.vm, 'handleNotification').mockImplementation(() => {});
-
- isCbExecuted = false;
- cb = () => {
- isCbExecuted = true;
- };
+ it('checks the status of the pipelines', async () => {
+ const callback = jest.fn();
+ await createComponent({ updatedMrData: { foo: 1 } });
+ await waitForPromises();
+ eventHub.$emit('MRWidgetUpdateRequested', callback);
+ await waitForPromises();
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({ foo: 1 }));
});
- it('should tell service to check status if document is visible', () => {
- wrapper.vm.checkStatus(cb);
+ it('notifies the user of the pipeline status', async () => {
+ jest.spyOn(notify, 'notifyMe').mockImplementation(() => {});
+ const logoFilename = 'logo.png';
+ await createComponent({
+ updatedMrData: { gitlabLogo: logoFilename },
+ });
+ eventHub.$emit('MRWidgetUpdateRequested');
+ await waitForPromises();
+ expect(notify.notifyMe).toHaveBeenCalledWith(
+ `Pipeline passed`,
+ `Pipeline passed for "${mockData.title}"`,
+ logoFilename,
+ );
+ });
- return nextTick().then(() => {
- expect(wrapper.vm.service.checkStatus).toHaveBeenCalled();
- expect(wrapper.vm.mr.setData).toHaveBeenCalled();
- expect(wrapper.vm.handleNotification).toHaveBeenCalledWith(mockData);
- expect(isCbExecuted).toBe(true);
+ it('updates the stores data', async () => {
+ const mockSetData = jest.fn();
+ await createComponent({
+ data: {
+ mr: {
+ setData: mockSetData,
+ setGraphqlData: jest.fn(),
+ },
+ },
});
+ eventHub.$emit('MRWidgetUpdateRequested');
+ expect(mockSetData).toHaveBeenCalled();
});
});
describe('initDeploymentsPolling', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
it('should call SmartInterval', () => {
wrapper.vm.initDeploymentsPolling();
@@ -382,83 +392,98 @@ describe('MrWidgetOptions', () => {
});
describe('fetchDeployments', () => {
- it('should fetch deployments', () => {
- jest
- .spyOn(wrapper.vm.service, 'fetchDeployments')
- .mockResolvedValue({ data: [{ id: 1, status: SUCCESS }] });
-
- wrapper.vm.fetchPreMergeDeployments();
+ beforeEach(async () => {
+ mock
+ .onGet(mockData.ci_environments_status_path)
+ .reply(() => [HTTP_STATUS_OK, [{ id: 1, status: SUCCESS }]]);
+ await createComponent();
+ });
- return nextTick().then(() => {
- expect(wrapper.vm.service.fetchDeployments).toHaveBeenCalled();
- expect(wrapper.vm.mr.deployments.length).toEqual(1);
- expect(wrapper.vm.mr.deployments[0].id).toBe(1);
- });
+ it('should fetch deployments', async () => {
+ eventHub.$emit('FetchDeployments', {});
+ await waitForPromises();
+ expect(wrapper.vm.mr.deployments.length).toEqual(1);
+ expect(wrapper.vm.mr.deployments[0].id).toBe(1);
});
});
describe('fetchActionsContent', () => {
- it('should fetch content of Cherry Pick and Revert modals', () => {
- jest
- .spyOn(wrapper.vm.service, 'fetchMergeActionsContent')
- .mockResolvedValue({ data: 'hello world' });
-
- wrapper.vm.fetchActionsContent();
-
- return nextTick().then(() => {
- expect(wrapper.vm.service.fetchMergeActionsContent).toHaveBeenCalled();
- expect(document.body.textContent).toContain('hello world');
- expect(document.dispatchEvent).toHaveBeenCalledWith(
- new CustomEvent('merged:UpdateActions'),
- );
- });
+ const innerHTML = 'hello world';
+ beforeEach(async () => {
+ jest.spyOn(document, 'dispatchEvent');
+ mock.onGet(mockData.commit_change_content_path).reply(() => [HTTP_STATUS_OK, innerHTML]);
+ await createComponent();
+ });
+
+ it('should fetch content of Cherry Pick and Revert modals', async () => {
+ eventHub.$emit('FetchActionsContent');
+ await waitForPromises();
+ expect(document.body.textContent).toContain(innerHTML);
+ expect(document.dispatchEvent).toHaveBeenCalledWith(
+ new CustomEvent('merged:UpdateActions'),
+ );
});
});
describe('bindEventHubListeners', () => {
- it.each`
- event | method | methodArgs
- ${'MRWidgetUpdateRequested'} | ${'checkStatus'} | ${(x) => [x]}
- ${'MRWidgetRebaseSuccess'} | ${'checkStatus'} | ${(x) => [x, true]}
- ${'FetchActionsContent'} | ${'fetchActionsContent'} | ${() => []}
- ${'EnablePolling'} | ${'resumePolling'} | ${() => []}
- ${'DisablePolling'} | ${'stopPolling'} | ${() => []}
- ${'FetchDeployments'} | ${'fetchPreMergeDeployments'} | ${() => []}
- `('should bind to $event', ({ event, method, methodArgs }) => {
- jest.spyOn(wrapper.vm, method).mockImplementation();
-
- const eventArg = {};
- eventHub.$emit(event, eventArg);
-
- expect(wrapper.vm[method]).toHaveBeenCalledWith(...methodArgs(eventArg));
+ const mockSetData = jest.fn();
+ beforeEach(async () => {
+ await createComponent({
+ data: {
+ mr: {
+ setData: mockSetData,
+ setGraphqlData: jest.fn(),
+ },
+ },
+ });
});
- it('should bind to SetBranchRemoveFlag', () => {
- expect(wrapper.vm.mr.isRemovingSourceBranch).toBe(false);
-
- eventHub.$emit('SetBranchRemoveFlag', [true]);
+ it('refetches when "MRWidgetUpdateRequested" event is emitted', async () => {
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1);
+ eventHub.$emit('MRWidgetUpdateRequested', () => {});
+ await waitForPromises();
+ expect(stateQueryHandler).toHaveBeenCalledTimes(2);
+ });
- expect(wrapper.vm.mr.isRemovingSourceBranch).toBe(true);
+ it('refetches when "MRWidgetRebaseSuccess" event is emitted', async () => {
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1);
+ eventHub.$emit('MRWidgetRebaseSuccess', () => {});
+ await waitForPromises();
+ expect(stateQueryHandler).toHaveBeenCalledTimes(2);
});
- it('should bind to FailedToMerge', () => {
- wrapper.vm.mr.state = '';
- wrapper.vm.mr.mergeError = '';
+ it('should bind to SetBranchRemoveFlag', () => {
+ expect(findPipelineContainer().props('mr')).toMatchObject({
+ isRemovingSourceBranch: false,
+ });
+ eventHub.$emit('SetBranchRemoveFlag', [true]);
+ expect(findPipelineContainer().props('mr')).toMatchObject({
+ isRemovingSourceBranch: true,
+ });
+ });
+ it('should bind to FailedToMerge', async () => {
+ expect(findAlertMessage().exists()).toBe(false);
+ expect(findPipelineContainer().props('mr')).toMatchObject({
+ mergeError: undefined,
+ state: 'merged',
+ });
const mergeError = 'Something bad happened!';
- eventHub.$emit('FailedToMerge', mergeError);
+ await eventHub.$emit('FailedToMerge', mergeError);
- expect(wrapper.vm.mr.state).toBe('failedToMerge');
- expect(wrapper.vm.mr.mergeError).toBe(mergeError);
+ expect(findAlertMessage().exists()).toBe(true);
+ expect(findAlertMessage().text()).toBe(`${mergeError}. Try again.`);
+ expect(findPipelineContainer().props('mr')).toMatchObject({
+ mergeError,
+ state: 'failedToMerge',
+ });
});
it('should bind to UpdateWidgetData', () => {
- jest.spyOn(wrapper.vm.mr, 'setData').mockImplementation();
-
const data = { ...mockData };
eventHub.$emit('UpdateWidgetData', data);
- expect(wrapper.vm.mr.setData).toHaveBeenCalledWith(data);
+ expect(mockSetData).toHaveBeenCalledWith(data);
});
});
@@ -479,58 +504,39 @@ describe('MrWidgetOptions', () => {
});
it('should call setFavicon method', async () => {
- wrapper.vm.mr.faviconOverlayPath = overlayDataUrl;
-
- await wrapper.vm.setFaviconHelper();
-
+ await createComponent({ updatedMrData: { favicon_overlay_path: overlayDataUrl } });
expect(setFaviconOverlay).toHaveBeenCalledWith(overlayDataUrl);
});
it('should not call setFavicon when there is no faviconOverlayPath', async () => {
- wrapper.vm.mr.faviconOverlayPath = null;
- await wrapper.vm.setFaviconHelper();
+ await createComponent({ updatedMrData: { favicon_overlay_path: null } });
expect(faviconElement.getAttribute('href')).toEqual(null);
});
});
describe('handleNotification', () => {
- const data = {
- ci_status: 'running',
- title: 'title',
- pipeline: { details: { status: { label: 'running-label' } } },
- };
-
beforeEach(() => {
jest.spyOn(notify, 'notifyMe').mockImplementation(() => {});
-
- wrapper.vm.mr.ciStatus = 'failed';
- wrapper.vm.mr.gitlabLogo = 'logo.png';
});
- it('should call notifyMe', () => {
- wrapper.vm.handleNotification(data);
-
+ it('should call notifyMe', async () => {
+ const logoFilename = 'logo.png';
+ await createComponent({ updatedMrData: { gitlabLogo: logoFilename } });
expect(notify.notifyMe).toHaveBeenCalledWith(
- 'Pipeline running-label',
- 'Pipeline running-label for "title"',
- 'logo.png',
+ `Pipeline passed`,
+ `Pipeline passed for "${mockData.title}"`,
+ logoFilename,
);
});
- it('should not call notifyMe if the status has not changed', () => {
- wrapper.vm.mr.ciStatus = data.ci_status;
-
- wrapper.vm.handleNotification(data);
-
+ it('should not call notifyMe if the status has not changed', async () => {
+ await createComponent({ updatedMrData: { ci_status: undefined } });
+ await eventHub.$emit('MRWidgetUpdateRequested');
expect(notify.notifyMe).not.toHaveBeenCalled();
});
- it('should not notify if no pipeline provided', () => {
- wrapper.vm.handleNotification({
- ...data,
- pipeline: undefined,
- });
-
+ it('should not notify if no pipeline provided', async () => {
+ await createComponent({ updatedMrData: { pipeline: undefined } });
expect(notify.notifyMe).not.toHaveBeenCalled();
});
});
@@ -546,7 +552,6 @@ describe('MrWidgetOptions', () => {
wrapper.destroy();
return createComponent({
- mrData: mockData,
options: {},
data: {
pollInterval: interval,
@@ -597,47 +602,18 @@ describe('MrWidgetOptions', () => {
});
describe('rendering deployments', () => {
- const changes = [
- {
- path: 'index.html',
- external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/index.html',
- },
- {
- path: 'imgs/gallery.html',
- external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
- },
- {
- path: 'about/',
- external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/about/',
- },
- ];
- const deploymentMockData = {
- id: 15,
- name: 'review/diplo',
- url: '/root/acets-review-apps/environments/15',
- stop_url: '/root/acets-review-apps/environments/15/stop',
- metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
- metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
- external_url: 'http://diplo.',
- external_url_formatted: 'diplo.',
- deployed_at: '2017-03-22T22:44:42.258Z',
- deployed_at_formatted: 'Mar 22, 2017 10:44pm',
- changes,
- status: SUCCESS,
- environment_available: true,
- };
-
it('renders multiple deployments', async () => {
- wrapper.vm.mr.deployments.push(
- {
- ...deploymentMockData,
- },
- {
- ...deploymentMockData,
- id: deploymentMockData.id + 1,
+ await createComponent({
+ updatedMrData: {
+ deployments: [
+ mockDeployment,
+ {
+ ...mockDeployment,
+ id: mockDeployment.id + 1,
+ },
+ ],
},
- );
- await nextTick();
+ });
expect(findPipelineContainer().props('isPostMerge')).toBe(false);
expect(findPipelineContainer().props('mr').deployments).toHaveLength(2);
expect(findPipelineContainer().props('mr').postMergeDeployments).toHaveLength(0);
@@ -646,189 +622,44 @@ describe('MrWidgetOptions', () => {
describe('pipeline for target branch after merge', () => {
describe('with information for target branch pipeline', () => {
- beforeEach(() => {
- wrapper.vm.mr.state = 'merged';
- wrapper.vm.mr.mergePipeline = {
- id: 127,
- user: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url: null,
- web_url: 'http://localhost:3000/root',
- status_tooltip_html: null,
- path: '/root',
- },
- active: true,
- coverage: null,
- source: 'push',
- created_at: '2018-10-22T11:41:35.186Z',
- updated_at: '2018-10-22T11:41:35.433Z',
- path: '/root/ci-web-terminal/pipelines/127',
- flags: {
- latest: true,
- stuck: true,
- auto_devops: false,
- yaml_errors: false,
- retryable: false,
- cancelable: true,
- failure_reason: false,
- },
- details: {
- status: {
- icon: 'status_pending',
- text: 'pending',
- label: 'pending',
- group: 'pending',
- tooltip: 'pending',
- has_details: true,
- details_path: '/root/ci-web-terminal/pipelines/127',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
- },
- duration: null,
- finished_at: null,
- stages: [
- {
- name: 'test',
- title: 'test: pending',
- status: {
- icon: 'status_pending',
- text: 'pending',
- label: 'pending',
- group: 'pending',
- tooltip: 'pending',
- has_details: true,
- details_path: '/root/ci-web-terminal/pipelines/127#test',
- illustration: null,
- favicon:
- '/assets/ci_favicons/favicon_status_pending-5bdf338420e5221ca24353b6bff1c9367189588750632e9a871b7af09ff6a2ae.png',
- },
- path: '/root/ci-web-terminal/pipelines/127#test',
- dropdown_path: '/root/ci-web-terminal/pipelines/127/stage.json?stage=test',
- },
- ],
- artifacts: [],
- manual_actions: [],
- scheduled_actions: [],
- },
- ref: {
- name: 'main',
- path: '/root/ci-web-terminal/commits/main',
- tag: false,
- branch: true,
- },
- commit: {
- id: 'aa1939133d373c94879becb79d91828a892ee319',
- short_id: 'aa193913',
- title: "Merge branch 'main-test' into 'main'",
- created_at: '2018-10-22T11:41:33.000Z',
- parent_ids: [
- '4622f4dd792468993003caf2e3be978798cbe096',
- '76598df914cdfe87132d0c3c40f80db9fa9396a4',
- ],
- message:
- "Merge branch 'main-test' into 'main'\n\nUpdate .gitlab-ci.yml\n\nSee merge request root/ci-web-terminal!1",
- author_name: 'Administrator',
- author_email: 'admin@example.com',
- authored_date: '2018-10-22T11:41:33.000Z',
- committer_name: 'Administrator',
- committer_email: 'admin@example.com',
- committed_date: '2018-10-22T11:41:33.000Z',
- author: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- state: 'active',
- avatar_url: null,
- web_url: 'http://localhost:3000/root',
- status_tooltip_html: null,
- path: '/root',
- },
- author_gravatar_url: null,
- commit_url:
- 'http://localhost:3000/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
- commit_path: '/root/ci-web-terminal/commit/aa1939133d373c94879becb79d91828a892ee319',
- },
- cancel_path: '/root/ci-web-terminal/pipelines/127/cancel',
- };
- return nextTick();
- });
+ const state = 'merged';
- it('renders pipeline block', () => {
+ it('renders pipeline block', async () => {
+ await createComponent({ updatedMrData: { state, merge_pipeline: mockMergePipeline } });
expect(findMergedPipelineContainer().exists()).toBe(true);
});
describe('with post merge deployments', () => {
- beforeEach(() => {
- wrapper.vm.mr.postMergeDeployments = [
- {
- id: 15,
- name: 'review/diplo',
- url: '/root/acets-review-apps/environments/15',
- stop_url: '/root/acets-review-apps/environments/15/stop',
- metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
- metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
- external_url: 'http://diplo.',
- external_url_formatted: 'diplo.',
- deployed_at: '2017-03-22T22:44:42.258Z',
- deployed_at_formatted: 'Mar 22, 2017 10:44pm',
- changes: [
- {
- path: 'index.html',
- external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/index.html',
- },
- {
- path: 'imgs/gallery.html',
- external_url:
- 'http://root-main-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
- },
- {
- path: 'about/',
- external_url: 'http://root-main-patch-91341.volatile-watch.surge.sh/about/',
- },
- ],
- status: 'success',
+ it('renders post deployment information', async () => {
+ await createComponent({
+ updatedMrData: {
+ state,
+ merge_pipeline: mockMergePipeline,
+ post_merge_deployments: mockPostMergeDeployments,
},
- ];
-
- return nextTick();
- });
-
- it('renders post deployment information', () => {
+ });
expect(findMergedPipelineContainer().exists()).toBe(true);
});
});
});
describe('without information for target branch pipeline', () => {
- beforeEach(() => {
- wrapper.vm.mr.state = 'merged';
-
- return nextTick();
- });
-
- it('does not render pipeline block', () => {
+ it('does not render pipeline block', async () => {
+ await createComponent({ updatedMrData: { merge_pipeline: undefined } });
expect(findMergedPipelineContainer().exists()).toBe(false);
});
});
describe('when state is not merged', () => {
- beforeEach(() => {
- wrapper.vm.mr.state = 'archived';
-
- return nextTick();
- });
-
- it('does not render pipeline block', () => {
+ it('does not render pipeline block', async () => {
+ await createComponent({ updatedMrData: { state: 'archived' } });
expect(findMergedPipelineContainer().exists()).toBe(false);
});
});
});
it('should not suggest pipelines when feature flag is not present', () => {
+ createComponent();
expect(findSuggestPipeline().exists()).toBe(false);
});
});
@@ -839,28 +670,23 @@ describe('MrWidgetOptions', () => {
});
describe('given feature flag is enabled', () => {
- beforeEach(async () => {
- await createComponent();
- wrapper.vm.mr.hasCI = false;
- });
-
- it('should suggest pipelines when none exist', () => {
+ it('should suggest pipelines when none exist', async () => {
+ await createComponent({ updatedMrData: { has_ci: false } });
expect(findSuggestPipeline().exists()).toBe(true);
});
it.each([
- { isDismissedSuggestPipeline: true },
- { mergeRequestAddCiConfigPath: null },
- { hasCI: true },
+ { is_dismissed_suggest_pipeline: true },
+ { merge_request_add_ci_config_path: null },
+ { has_ci: true },
])('with %s, should not suggest pipeline', async (obj) => {
- Object.assign(wrapper.vm.mr, obj);
-
- await nextTick();
+ await createComponent({ updatedMrData: { has_ci: false, ...obj } });
expect(findSuggestPipeline().exists()).toBe(false);
});
it('should allow dismiss of the suggest pipeline message', async () => {
+ await createComponent({ updatedMrData: { has_ci: false } });
await findSuggestPipeline().vm.$emit('dismiss');
expect(findSuggestPipeline().exists()).toBe(false);
@@ -875,11 +701,11 @@ describe('MrWidgetOptions', () => {
${'merged'} | ${true} | ${'shows'}
${'open'} | ${true} | ${'shows'}
`('$showText merge error when state is $state', async ({ state, show }) => {
- createComponent({ mrData: { ...mockData, state, mergeError: 'Error!' } });
+ createComponent({ updatedMrData: { state, mergeError: 'Error!' } });
await waitForPromises();
- expect(wrapper.find('[data-testid="merge_error"]').exists()).toBe(show);
+ expect(wrapper.findByTestId('merge-error').exists()).toBe(show);
});
});
@@ -1111,6 +937,67 @@ describe('MrWidgetOptions', () => {
registeredExtensions.extensions = [];
});
+ describe('component name tier suffixes', () => {
+ let extension;
+
+ beforeEach(() => {
+ extension = workingExtension();
+ });
+
+ it('reports events without a CE suffix', () => {
+ extension.name = `${extension.name}CE`;
+
+ registerExtension(extension);
+ createComponent({ mountFn: mountExtended });
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_code_review_merge_request_widget_test_extension_view',
+ );
+ expect(api.trackRedisHllUserEvent).not.toHaveBeenCalledWith(
+ 'i_code_review_merge_request_widget_test_extension_c_e_view',
+ );
+ });
+
+ it('reports events without a EE suffix', () => {
+ extension.name = `${extension.name}EE`;
+
+ registerExtension(extension);
+ createComponent({ mountFn: mountExtended });
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_code_review_merge_request_widget_test_extension_view',
+ );
+ expect(api.trackRedisHllUserEvent).not.toHaveBeenCalledWith(
+ 'i_code_review_merge_request_widget_test_extension_e_e_view',
+ );
+ });
+
+ it('leaves non-CE & non-EE all caps suffixes intact', () => {
+ extension.name = `${extension.name}HI`;
+
+ registerExtension(extension);
+ createComponent({ mountFn: mountExtended });
+
+ expect(api.trackRedisHllUserEvent).not.toHaveBeenCalledWith(
+ 'i_code_review_merge_request_widget_test_extension_view',
+ );
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_code_review_merge_request_widget_test_extension_h_i_view',
+ );
+ });
+
+ it("doesn't remove CE or EE from the middle of a widget name", () => {
+ extension.name = 'TestCEExtensionEETest';
+
+ registerExtension(extension);
+ createComponent({ mountFn: mountExtended });
+
+ expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
+ 'i_code_review_merge_request_widget_test_c_e_extension_e_e_test_view',
+ );
+ });
+ });
+
it('triggers view events when mounted', () => {
registerExtension(workingExtension());
createComponent({ mountFn: mountExtended });
@@ -1217,11 +1104,7 @@ describe('MrWidgetOptions', () => {
it('renders the Preparing state component when the MR state is initially "preparing"', async () => {
await createComponent({
- mrData: {
- ...mockData,
- state: 'opened',
- detailedMergeStatus: 'PREPARING',
- },
+ updatedMrData: { state: 'opened', detailedMergeStatus: 'PREPARING' },
});
expect(findApprovalsWidget().exists()).toBe(false);
@@ -1235,11 +1118,7 @@ describe('MrWidgetOptions', () => {
it("shows the Preparing widget when the MR reports it's not ready yet", async () => {
await createComponent({
- mrData: {
- ...mockData,
- state: 'opened',
- detailedMergeStatus: 'PREPARING',
- },
+ updatedMrData: { state: 'opened', detailedMergeStatus: 'PREPARING' },
options: {},
data: {},
});
@@ -1249,11 +1128,7 @@ describe('MrWidgetOptions', () => {
it('removes the Preparing widget when the MR indicates it has been prepared', async () => {
await createComponent({
- mrData: {
- ...mockData,
- state: 'opened',
- detailedMergeStatus: 'PREPARING',
- },
+ updatedMrData: { state: 'opened', detailedMergeStatus: 'PREPARING' },
options: {},
data: {},
});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
index b93c64efbcb..da1a15b1b2b 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
@@ -4,12 +4,10 @@ exports[`Expand button on click when short text is provided renders button after
<span>
<button
aria-label="Click to expand text"
- class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
+ class="btn btn-blank btn-default btn-icon btn-md button-ellipsis-horizontal gl-button js-text-expander-prepend text-expander"
style="display: none;"
type="button"
>
- <!---->
-
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
@@ -20,26 +18,18 @@ exports[`Expand button on click when short text is provided renders button after
href="file-mock#ellipsis_h"
/>
</svg>
-
- <!---->
</button>
-
- <!---->
-
<span>
<p>
Expanded!
</p>
</span>
-
<button
aria-label="Click to expand text"
- class="btn js-text-expander-append text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
+ class="btn btn-blank btn-default btn-icon btn-md button-ellipsis-horizontal gl-button js-text-expander-append text-expander"
style=""
type="button"
>
- <!---->
-
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
@@ -50,8 +40,6 @@ exports[`Expand button on click when short text is provided renders button after
href="file-mock#ellipsis_h"
/>
</svg>
-
- <!---->
</button>
</span>
`;
@@ -60,11 +48,9 @@ exports[`Expand button when short text is provided renders button before text 1`
<span>
<button
aria-label="Click to expand text"
- class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
+ class="btn btn-blank btn-default btn-icon btn-md button-ellipsis-horizontal gl-button js-text-expander-prepend text-expander"
type="button"
>
- <!---->
-
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
@@ -75,26 +61,18 @@ exports[`Expand button when short text is provided renders button before text 1`
href="file-mock#ellipsis_h"
/>
</svg>
-
- <!---->
</button>
-
<span>
<p>
Short
</p>
</span>
-
- <!---->
-
<button
aria-label="Click to expand text"
- class="btn js-text-expander-append text-expander btn-blank btn-default btn-md gl-button btn-icon button-ellipsis-horizontal"
+ class="btn btn-blank btn-default btn-icon btn-md button-ellipsis-horizontal gl-button js-text-expander-append text-expander"
style="display: none;"
type="button"
>
- <!---->
-
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
@@ -105,8 +83,6 @@ exports[`Expand button when short text is provided renders button before text 1`
href="file-mock#ellipsis_h"
/>
</svg>
-
- <!---->
</button>
</span>
`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap
index df0fcf5da1c..d630d23873f 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/integration_help_text_spec.js.snap
@@ -8,14 +8,12 @@ exports[`IntegrationHelpText component should not render the link when start and
exports[`IntegrationHelpText component should render the help text 1`] = `
<span>
- Click
+ Click
<gl-link-stub
href="http://bar.com"
target="_blank"
>
-
- Bar
-
+ Bar
<gl-icon-stub
class="gl-vertical-align-middle"
name="external-link"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap
index f414359fef2..76deb4d0b36 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/source_editor_spec.js.snap
@@ -3,8 +3,8 @@
exports[`Source Editor component rendering matches the snapshot 1`] = `
<div
data-editor-loading=""
- data-qa-selector="source_editor_container"
- id="source-editor-snippet_777"
+ data-testid="source-editor-container"
+ id="reference-0"
>
<pre
class="editor-loading-content"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
deleted file mode 100644
index 62d75fbdc5f..00000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ /dev/null
@@ -1,59 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SplitButton renders actionItems 1`] = `
-<gl-dropdown-stub
- category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
- menu-class=""
- size="medium"
- split="true"
- splithref=""
- text="professor"
- variant="default"
->
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischecked="true"
- ischeckitem="true"
- secondarytext=""
- >
- <strong>
- professor
- </strong>
-
- <div>
- very symphonic
- </div>
- </gl-dropdown-item-stub>
-
- <gl-dropdown-divider-stub />
- <gl-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckitem="true"
- secondarytext=""
- >
- <strong>
- captain
- </strong>
-
- <div>
- warp drive
- </div>
- </gl-dropdown-item-stub>
-
- <!---->
-</gl-dropdown-stub>
-`;
diff --git a/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap b/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap
index 24b2c54f20b..359aaacde0b 100644
--- a/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap
+++ b/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap
@@ -11,7 +11,6 @@ exports[`Beta badge component renders the badge 1`] = `
>
Beta
</gl-badge-stub>
-
<gl-popover-stub
cssclasses=""
data-testid="beta-badge"
@@ -23,28 +22,23 @@ exports[`Beta badge component renders the badge 1`] = `
<p>
A Beta feature is not production-ready, but is unlikely to change drastically before it's released. We encourage users to try Beta features and provide feedback.
</p>
-
<p
class="gl-mb-0"
>
A Beta feature:
</p>
-
<ul
class="gl-pl-4"
>
<li>
May be unstable.
</li>
-
<li>
Should not cause data loss.
</li>
-
<li>
Is supported by a commercially reasonable effort.
</li>
-
<li>
Is complete or near completion.
</li>
diff --git a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
index fbf3d17fd64..1f3f1fef365 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
+++ b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
@@ -3,80 +3,69 @@
exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
<div>
<div
- class="file-content code js-syntax-highlight"
+ class="code file-content js-syntax-highlight"
>
<div
- class="line-numbers gl-pt-0!"
+ class="gl-pt-0! line-numbers"
>
<a
class="diff-line-num js-line-number"
data-line-number="1"
href="#LC1"
- id="L1"
+ id="reference-0"
>
<gl-icon-stub
name="link"
size="12"
/>
-
1
-
</a>
<a
class="diff-line-num js-line-number"
data-line-number="2"
href="#LC2"
- id="L2"
+ id="reference-1"
>
<gl-icon-stub
name="link"
size="12"
/>
-
2
-
</a>
<a
class="diff-line-num js-line-number"
data-line-number="3"
href="#LC3"
- id="L3"
+ id="reference-2"
>
<gl-icon-stub
name="link"
size="12"
/>
-
3
-
</a>
</div>
-
<div
class="blob-content"
>
<pre
- class="code highlight gl-p-0! gl-display-flex"
+ class="code gl-display-flex gl-p-0! highlight"
>
<code
data-blob-hash="foo-bar"
>
<span
- id="LC1"
+ id="reference-3"
>
First
</span>
-
-
<span
- id="LC2"
+ id="reference-4"
>
Second
</span>
-
-
<span
- id="LC3"
+ id="reference-5"
>
Third
</span>
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index fc8155bd381..eadcd452929 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -3,6 +3,10 @@ import { shallowMount } from '@vue/test-utils';
import { handleBlobRichViewer } from '~/blob/viewer';
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
+import {
+ MARKUP_FILE_TYPE,
+ CONTENT_LOADED_EVENT,
+} from '~/vue_shared/components/blob_viewers/constants';
import { handleLocationHash } from '~/lib/utils/common_utils';
jest.mock('~/blob/viewer');
@@ -10,10 +14,10 @@ jest.mock('~/lib/utils/common_utils');
describe('Blob Rich Viewer component', () => {
let wrapper;
- const content = '<h1 id="markdown">Foo Bar</h1>';
+ const dummyContent = '<h1 id="markdown">Foo Bar</h1>';
const defaultType = 'markdown';
- function createComponent(type = defaultType, richViewer) {
+ function createComponent(type = defaultType, richViewer, content = dummyContent) {
wrapper = shallowMount(RichViewer, {
propsData: {
richViewer,
@@ -23,26 +27,75 @@ describe('Blob Rich Viewer component', () => {
});
}
- beforeEach(() => {
- const execImmediately = (callback) => callback();
- jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+ beforeEach(() => createComponent());
- createComponent();
- });
+ const findMarkdownFieldView = () => wrapper.findComponent(MarkdownFieldView);
+
+ describe('Markdown content', () => {
+ const generateDummyContent = (contentLength) => {
+ let generatedContent = '';
+ for (let i = 0; i < contentLength; i += 1) {
+ generatedContent += `<span>Line: ${i + 1}</span>\n`;
+ }
+
+ generatedContent += '<img src="x" onerror="alert(`XSS`)">'; // for testing against XSS
+ return `<div class="js-markup-content">${generatedContent}</div>`;
+ };
+
+ describe('Large file', () => {
+ const content = generateDummyContent(50);
+ beforeEach(() => createComponent(MARKUP_FILE_TYPE, null, content));
+
+ it('renders the top of the file immediately and does not emit a content loaded event', () => {
+ expect(wrapper.text()).toContain('Line: 10');
+ expect(wrapper.text()).not.toContain('Line: 50');
+ expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toBeUndefined();
+ expect(findMarkdownFieldView().props('isLoading')).toBe(true);
+ });
+
+ it('renders the rest of the file later and emits a content loaded event', async () => {
+ jest.runAllTimers();
+ await nextTick();
+
+ expect(wrapper.text()).toContain('Line: 10');
+ expect(wrapper.text()).toContain('Line: 50');
+ expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toHaveLength(1);
+ expect(findMarkdownFieldView().props('isLoading')).toBe(false);
+ });
- it('listens to requestIdleCallback', () => {
- expect(window.requestIdleCallback).toHaveBeenCalled();
+ it('sanitizes the content', () => {
+ jest.runAllTimers();
+
+ expect(wrapper.html()).toContain('<img src="x">');
+ });
+ });
+
+ describe('Small file', () => {
+ const content = generateDummyContent(5);
+ beforeEach(() => createComponent(MARKUP_FILE_TYPE, null, content));
+
+ it('renders the entire file immediately and emits a content loaded event', () => {
+ expect(wrapper.text()).toContain('Line: 5');
+ expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toHaveLength(1);
+ expect(findMarkdownFieldView().props('isLoading')).toBe(false);
+ });
+
+ it('sanitizes the content', () => {
+ expect(wrapper.html()).toContain('<img src="x">');
+ });
+ });
});
it('renders the passed content without transformations', () => {
- expect(wrapper.html()).toContain(content);
+ expect(wrapper.html()).toContain(dummyContent);
});
- it('renders the richViewer if one is present', async () => {
+ it('renders the richViewer if one is present and emits a content loaded event', async () => {
const richViewer = '<div class="js-pdf-viewer"></div>';
createComponent('pdf', richViewer);
await nextTick();
expect(wrapper.html()).toContain(richViewer);
+ expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toHaveLength(1);
});
it('queries for advanced viewer', () => {
@@ -50,7 +103,7 @@ describe('Blob Rich Viewer component', () => {
});
it('is using Markdown View Field', () => {
- expect(wrapper.findComponent(MarkdownFieldView).exists()).toBe(true);
+ expect(findMarkdownFieldView().exists()).toBe(true);
});
it('scrolls to the hash location', () => {
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index 8c860c9b06f..c74964c13f5 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -145,7 +145,7 @@ describe('CI Badge Link Component', () => {
});
it('should render dynamic badge size', () => {
- createComponent({ status: statuses.success, badgeSize: 'lg' });
+ createComponent({ status: statuses.success, size: 'lg' });
expect(findBadge().props('size')).toBe('lg');
});
diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
index 5720f45f4dd..a8436af33f2 100644
--- a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
@@ -28,7 +28,7 @@ describe('Code Block Highlighted', () => {
>
const
</span>
- foo =
+ foo =
<span
class="hljs-number"
>
diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js
index 0fdfb96cb23..1bcf0c3938c 100644
--- a/spec/frontend/vue_shared/components/code_block_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_spec.js
@@ -17,12 +17,12 @@ describe('Code Block', () => {
createComponent({}, { default: 'DEFAULT SLOT' });
expect(wrapper.element).toMatchInlineSnapshot(`
- <pre
- class="code-block rounded code"
- >
- DEFAULT SLOT
- </pre>
- `);
+ <pre
+ class="code code-block rounded"
+ >
+ DEFAULT SLOT
+ </pre>
+ `);
});
it('renders with empty code prop', () => {
@@ -30,13 +30,11 @@ describe('Code Block', () => {
expect(wrapper.element).toMatchInlineSnapshot(`
<pre
- class="code-block rounded code"
+ class="code code-block rounded"
>
<code
class="d-block"
- >
-
- </code>
+ />
</pre>
`);
});
@@ -45,32 +43,32 @@ describe('Code Block', () => {
createComponent({ code });
expect(wrapper.element).toMatchInlineSnapshot(`
- <pre
- class="code-block rounded code"
+ <pre
+ class="code code-block rounded"
+ >
+ <code
+ class="d-block"
>
- <code
- class="d-block"
- >
- test-code
- </code>
- </pre>
- `);
+ test-code
+ </code>
+ </pre>
+ `);
});
it('sets maxHeight properly when provided', () => {
createComponent({ code, maxHeight: '200px' });
expect(wrapper.element).toMatchInlineSnapshot(`
- <pre
- class="code-block rounded code"
- style="max-height: 200px; overflow-y: auto;"
+ <pre
+ class="code code-block rounded"
+ style="max-height: 200px; overflow-y: auto;"
+ >
+ <code
+ class="d-block"
>
- <code
- class="d-block"
- >
- test-code
- </code>
- </pre>
- `);
+ test-code
+ </code>
+ </pre>
+ `);
});
});
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
index 92cd7597637..7f6d97e8e68 100644
--- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -1,15 +1,20 @@
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { TYPE_ISSUE, TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-const createComponent = ({ workspaceType = WORKSPACE_PROJECT, issuableType = TYPE_ISSUE } = {}) =>
+const createComponent = ({
+ workspaceType = WORKSPACE_PROJECT,
+ issuableType = TYPE_ISSUE,
+ hideTextInSmallScreens = false,
+} = {}) =>
shallowMount(ConfidentialityBadge, {
propsData: {
workspaceType,
issuableType,
+ hideTextInSmallScreens,
},
});
@@ -20,6 +25,11 @@ describe('ConfidentialityBadge', () => {
wrapper = createComponent();
});
+ const findConfidentialityBadgeText = () =>
+ wrapper.find('[data-testid="confidential-badge-text"]');
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findBadgeIcon = () => wrapper.findComponent(GlIcon);
+
it.each`
workspaceType | issuableType | expectedTooltip
${WORKSPACE_PROJECT} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
@@ -32,14 +42,30 @@ describe('ConfidentialityBadge', () => {
issuableType,
});
- const badgeEl = wrapper.findComponent(GlBadge);
-
- expect(badgeEl.props()).toMatchObject({
- icon: 'eye-slash',
+ expect(findBadgeIcon().props('name')).toBe('eye-slash');
+ expect(findBadge().props()).toMatchObject({
variant: 'warning',
});
- expect(badgeEl.attributes('title')).toBe(expectedTooltip);
- expect(badgeEl.text()).toBe('Confidential');
+ expect(findBadge().attributes('title')).toBe(expectedTooltip);
+ expect(findBadge().text()).toBe('Confidential');
},
);
+
+ it('does not have `gl-sm-display-block` and `gl-display-none` when `hideTextInSmallScreens` is false', () => {
+ wrapper = createComponent({ hideTextInSmallScreens: false });
+
+ expect(findConfidentialityBadgeText().classes()).not.toContain(
+ 'gl-display-none',
+ 'gl-sm-display-block',
+ );
+ });
+
+ it('has `gl-sm-display-block` and `gl-display-none` when `hideTextInSmallScreens` is true', () => {
+ wrapper = createComponent({ hideTextInSmallScreens: true });
+
+ expect(findConfidentialityBadgeText().classes()).toContain(
+ 'gl-display-none',
+ 'gl-sm-display-block',
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
index 0b5c8d9afc3..53218d794c7 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
@@ -31,6 +31,7 @@ describe('Confirm Danger Modal', () => {
propsData: {
modalId,
phrase,
+ visible: false,
},
provide,
stubs: { GlSprintf },
@@ -103,4 +104,16 @@ describe('Confirm Danger Modal', () => {
expect(wrapper.emitted('confirm')).not.toBeUndefined();
});
});
+
+ describe('v-model', () => {
+ it('emit `change` event', () => {
+ findModal().vm.$emit('change', true);
+
+ expect(wrapper.emitted('change')).toEqual([[true]]);
+ });
+
+ it('sets `visible` prop', () => {
+ expect(findModal().props('visible')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
deleted file mode 100644
index a3e5f187f9b..00000000000
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { mount } from '@vue/test-utils';
-import DateTimePickerInput from '~/vue_shared/components/date_time_picker/date_time_picker_input.vue';
-
-const inputLabel = 'This is a label';
-const inputValue = 'something';
-
-describe('DateTimePickerInput', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = mount(DateTimePickerInput, {
- propsData: {
- state: null,
- value: '',
- label: '',
- ...propsData,
- },
- });
- };
-
- it('renders label above the input', () => {
- createComponent({
- label: inputLabel,
- });
-
- expect(wrapper.find('.gl-form-group label').text()).toBe(inputLabel);
- });
-
- it('renders the same `ID` for input and `for` for label', () => {
- createComponent({ label: inputLabel });
-
- expect(wrapper.find('.gl-form-group label').attributes('for')).toBe(
- wrapper.find('input').attributes('id'),
- );
- });
-
- it('renders valid input in gray color instead of green', () => {
- createComponent({
- state: true,
- });
-
- expect(wrapper.find('input').classes('is-valid')).toBe(false);
- });
-
- it('renders invalid input in red color', () => {
- createComponent({
- state: false,
- });
-
- expect(wrapper.find('input').classes('is-invalid')).toBe(true);
- });
-
- it('input event is emitted when focus is lost', () => {
- createComponent();
-
- const input = wrapper.find('input');
- input.setValue(inputValue);
- input.trigger('blur');
-
- expect(wrapper.emitted('input')[0][0]).toEqual(inputValue);
- });
-});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
deleted file mode 100644
index 7a8f94b3746..00000000000
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
+++ /dev/null
@@ -1,190 +0,0 @@
-import timezoneMock from 'timezone-mock';
-
-import {
- isValidInputString,
- inputStringToIsoDate,
- isoDateToInputString,
-} from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
-
-describe('date time picker lib', () => {
- describe('isValidInputString', () => {
- [
- {
- input: '2019-09-09T00:00:00.000Z',
- output: true,
- },
- {
- input: '2019-09-09T000:00.000Z',
- output: false,
- },
- {
- input: 'a2019-09-09T000:00.000Z',
- output: false,
- },
- {
- input: '2019-09-09T',
- output: false,
- },
- {
- input: '2019-09-09',
- output: true,
- },
- {
- input: '2019-9-9',
- output: true,
- },
- {
- input: '2019-9-',
- output: true,
- },
- {
- input: '2019--',
- output: false,
- },
- {
- input: '2019',
- output: true,
- },
- {
- input: '',
- output: false,
- },
- {
- input: null,
- output: false,
- },
- ].forEach(({ input, output }) => {
- it(`isValidInputString return ${output} for ${input}`, () => {
- expect(isValidInputString(input)).toBe(output);
- });
- });
- });
-
- describe('inputStringToIsoDate', () => {
- [
- '',
- 'null',
- undefined,
- 'abc',
- 'xxxx-xx-xx',
- '9999-99-19',
- '2019-19-23',
- '2019-09-23 x',
- '2019-09-29 24:24:24',
- ].forEach((input) => {
- it(`throws error for invalid input like ${input}`, () => {
- expect(() => inputStringToIsoDate(input)).toThrow();
- });
- });
-
- [
- {
- input: '2019-09-08 01:01:01',
- output: '2019-09-08T01:01:01Z',
- },
- {
- input: '2019-09-08 00:00:00',
- output: '2019-09-08T00:00:00Z',
- },
- {
- input: '2019-09-08 23:59:59',
- output: '2019-09-08T23:59:59Z',
- },
- {
- input: '2019-09-08',
- output: '2019-09-08T00:00:00Z',
- },
- {
- input: '2019-09-08',
- output: '2019-09-08T00:00:00Z',
- },
- {
- input: '2019-09-08 00:00:00',
- output: '2019-09-08T00:00:00Z',
- },
- {
- input: '2019-09-08 23:24:24',
- output: '2019-09-08T23:24:24Z',
- },
- {
- input: '2019-09-08 0:0:0',
- output: '2019-09-08T00:00:00Z',
- },
- ].forEach(({ input, output }) => {
- it(`returns ${output} from ${input}`, () => {
- expect(inputStringToIsoDate(input)).toBe(output);
- });
- });
-
- describe('timezone formatting', () => {
- const value = '2019-09-08 01:01:01';
- const utcResult = '2019-09-08T01:01:01Z';
- const localResult = '2019-09-08T08:01:01Z';
-
- it.each`
- val | locatTimezone | utc | result
- ${value} | ${'UTC'} | ${undefined} | ${utcResult}
- ${value} | ${'UTC'} | ${false} | ${utcResult}
- ${value} | ${'UTC'} | ${true} | ${utcResult}
- ${value} | ${'US/Pacific'} | ${undefined} | ${localResult}
- ${value} | ${'US/Pacific'} | ${false} | ${localResult}
- ${value} | ${'US/Pacific'} | ${true} | ${utcResult}
- `(
- 'when timezone is $locatTimezone, formats $result for utc = $utc',
- ({ val, locatTimezone, utc, result }) => {
- timezoneMock.register(locatTimezone);
-
- expect(inputStringToIsoDate(val, utc)).toBe(result);
-
- timezoneMock.unregister();
- },
- );
- });
- });
-
- describe('isoDateToInputString', () => {
- [
- {
- input: '2019-09-08T01:01:01Z',
- output: '2019-09-08 01:01:01',
- },
- {
- input: '2019-09-08T01:01:01.999Z',
- output: '2019-09-08 01:01:01',
- },
- {
- input: '2019-09-08T00:00:00Z',
- output: '2019-09-08 00:00:00',
- },
- ].forEach(({ input, output }) => {
- it(`returns ${output} for ${input}`, () => {
- expect(isoDateToInputString(input)).toBe(output);
- });
- });
-
- describe('timezone formatting', () => {
- const value = '2019-09-08T08:01:01Z';
- const utcResult = '2019-09-08 08:01:01';
- const localResult = '2019-09-08 01:01:01';
-
- it.each`
- val | locatTimezone | utc | result
- ${value} | ${'UTC'} | ${undefined} | ${utcResult}
- ${value} | ${'UTC'} | ${false} | ${utcResult}
- ${value} | ${'UTC'} | ${true} | ${utcResult}
- ${value} | ${'US/Pacific'} | ${undefined} | ${localResult}
- ${value} | ${'US/Pacific'} | ${false} | ${localResult}
- ${value} | ${'US/Pacific'} | ${true} | ${utcResult}
- `(
- 'when timezone is $locatTimezone, formats $result for utc = $utc',
- ({ val, locatTimezone, utc, result }) => {
- timezoneMock.register(locatTimezone);
-
- expect(isoDateToInputString(val, utc)).toBe(result);
-
- timezoneMock.unregister();
- },
- );
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
deleted file mode 100644
index 5620b569409..00000000000
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ /dev/null
@@ -1,326 +0,0 @@
-import { mount } from '@vue/test-utils';
-import timezoneMock from 'timezone-mock';
-import { nextTick } from 'vue';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import {
- defaultTimeRanges,
- defaultTimeRange,
-} from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
-
-const optionsCount = defaultTimeRanges.length;
-
-describe('DateTimePicker', () => {
- let wrapper;
-
- const dropdownToggle = () => wrapper.find('.dropdown-toggle');
- const dropdownMenu = () => wrapper.find('.dropdown-menu');
- const cancelButton = () => wrapper.find('[data-testid="cancelButton"]');
- const applyButtonElement = () => wrapper.find('button.btn-confirm').element;
- const findQuickRangeItems = () => wrapper.findAll('.dropdown-item');
-
- const createComponent = (props) => {
- wrapper = mount(DateTimePicker, {
- propsData: {
- ...props,
- },
- });
- };
-
- it('renders dropdown toggle button with selected text', async () => {
- createComponent();
- await nextTick();
- expect(dropdownToggle().text()).toBe(defaultTimeRange.label);
- });
-
- it('renders dropdown toggle button with selected text and utc label', async () => {
- createComponent({ utc: true });
- await nextTick();
- expect(dropdownToggle().text()).toContain(defaultTimeRange.label);
- expect(dropdownToggle().text()).toContain('UTC');
- });
-
- it('renders dropdown with 2 custom time range inputs', async () => {
- createComponent();
- await nextTick();
- expect(wrapper.findAll('input').length).toBe(2);
- });
-
- describe('renders label with h/m/s truncated if possible', () => {
- [
- {
- start: '2019-10-10T00:00:00.000Z',
- end: '2019-10-10T00:00:00.000Z',
- label: '2019-10-10 to 2019-10-10',
- },
- {
- start: '2019-10-10T00:00:00.000Z',
- end: '2019-10-14T00:10:00.000Z',
- label: '2019-10-10 to 2019-10-14 00:10:00',
- },
- {
- start: '2019-10-10T00:00:00.000Z',
- end: '2019-10-10T00:00:01.000Z',
- label: '2019-10-10 to 2019-10-10 00:00:01',
- },
- {
- start: '2019-10-10T00:00:01.000Z',
- end: '2019-10-10T00:00:01.000Z',
- label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01',
- },
- {
- start: '2019-10-10T00:00:01.000Z',
- end: '2019-10-10T00:00:01.000Z',
- utc: true,
- label: '2019-10-10 00:00:01 to 2019-10-10 00:00:01 UTC',
- },
- ].forEach(({ start, end, utc, label }) => {
- it(`for start ${start}, end ${end}, and utc ${utc}, label is ${label}`, async () => {
- createComponent({
- value: { start, end },
- utc,
- });
- await nextTick();
- expect(dropdownToggle().text()).toBe(label);
- });
- });
- });
-
- it(`renders dropdown with ${optionsCount} (default) items in quick range`, async () => {
- createComponent();
- dropdownToggle().trigger('click');
- await nextTick();
- expect(findQuickRangeItems().length).toBe(optionsCount);
- });
-
- it('renders dropdown with a default quick range item selected', async () => {
- createComponent();
- dropdownToggle().trigger('click');
- await nextTick();
- expect(wrapper.find('.dropdown-item.active').exists()).toBe(true);
- expect(wrapper.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label);
- });
-
- it('renders a disabled apply button on wrong input', () => {
- createComponent({
- start: 'invalid-input-date',
- });
-
- expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
- });
-
- describe('user input', () => {
- const fillInputAndBlur = async (input, val) => {
- wrapper.find(input).setValue(val);
- await nextTick();
- wrapper.find(input).trigger('blur');
- await nextTick();
- };
-
- beforeEach(async () => {
- createComponent();
- await nextTick();
- });
-
- it('displays inline error message if custom time range inputs are invalid', async () => {
- await fillInputAndBlur('#custom-time-from', '2019-10-01abc');
- await fillInputAndBlur('#custom-time-to', '2019-10-10abc');
- expect(wrapper.findAll('.invalid-feedback').length).toBe(2);
- });
-
- it('keeps apply button disabled with invalid custom time range inputs', async () => {
- await fillInputAndBlur('#custom-time-from', '2019-10-01abc');
- await fillInputAndBlur('#custom-time-to', '2019-09-19');
- expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
- });
-
- it('enables apply button with valid custom time range inputs', async () => {
- await fillInputAndBlur('#custom-time-from', '2019-10-01');
- await fillInputAndBlur('#custom-time-to', '2019-10-19');
- expect(applyButtonElement().getAttribute('disabled')).toBeNull();
- });
-
- describe('when "apply" is clicked', () => {
- it('emits iso dates', async () => {
- await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00');
- await fillInputAndBlur('#custom-time-to', '2019-10-19 00:00:00');
- applyButtonElement().click();
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0]).toEqual([
- {
- end: '2019-10-19T00:00:00Z',
- start: '2019-10-01T00:00:00Z',
- },
- ]);
- });
-
- it('emits iso dates, for dates without time of day', async () => {
- await fillInputAndBlur('#custom-time-from', '2019-10-01');
- await fillInputAndBlur('#custom-time-to', '2019-10-19');
- applyButtonElement().click();
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0]).toEqual([
- {
- end: '2019-10-19T00:00:00Z',
- start: '2019-10-01T00:00:00Z',
- },
- ]);
- });
-
- describe('when timezone is different', () => {
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('emits iso dates', async () => {
- await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00');
- await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00');
- applyButtonElement().click();
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0]).toEqual([
- {
- start: '2019-10-01T07:00:00Z',
- end: '2019-10-19T19:00:00Z',
- },
- ]);
- });
-
- it('emits iso dates with utc format', async () => {
- wrapper.setProps({ utc: true });
- await nextTick();
- await fillInputAndBlur('#custom-time-from', '2019-10-01 00:00:00');
- await fillInputAndBlur('#custom-time-to', '2019-10-19 12:00:00');
- applyButtonElement().click();
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0]).toEqual([
- {
- start: '2019-10-01T00:00:00Z',
- end: '2019-10-19T12:00:00Z',
- },
- ]);
- });
- });
- });
-
- it('unchecks quick range when text is input is clicked', async () => {
- const findActiveItems = () =>
- findQuickRangeItems().filter((w) => w.classes().includes('active'));
-
- expect(findActiveItems().length).toBe(1);
-
- await fillInputAndBlur('#custom-time-from', '2019-10-01');
- expect(findActiveItems().length).toBe(0);
- });
-
- it('emits dates in an object when a is clicked', () => {
- findQuickRangeItems()
- .at(3) // any item
- .trigger('click');
-
- expect(wrapper.emitted().input).toHaveLength(1);
- expect(wrapper.emitted().input[0][0]).toMatchObject({
- duration: {
- seconds: expect.any(Number),
- },
- });
- });
-
- it('hides the popover with cancel button', async () => {
- dropdownToggle().trigger('click');
-
- await nextTick();
- cancelButton().trigger('click');
-
- await nextTick();
- expect(dropdownMenu().classes('show')).toBe(false);
- });
- });
-
- describe('when using non-default time windows', () => {
- const MOCK_NOW = Date.UTC(2020, 0, 23, 20);
-
- const otherTimeRanges = [
- {
- label: '1 minute',
- duration: { seconds: 60 },
- },
- {
- label: '2 minutes',
- duration: { seconds: 60 * 2 },
- default: true,
- },
- {
- label: '5 minutes',
- duration: { seconds: 60 * 5 },
- },
- ];
-
- beforeEach(() => {
- jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW);
- });
-
- it('renders dropdown with a label in the quick range', async () => {
- createComponent({
- value: {
- duration: { seconds: 60 * 5 },
- },
- options: otherTimeRanges,
- });
- dropdownToggle().trigger('click');
- await nextTick();
- expect(dropdownToggle().text()).toBe('5 minutes');
- });
-
- it('renders dropdown with a label in the quick range and utc label', async () => {
- createComponent({
- value: {
- duration: { seconds: 60 * 5 },
- },
- utc: true,
- options: otherTimeRanges,
- });
- dropdownToggle().trigger('click');
- await nextTick();
- expect(dropdownToggle().text()).toBe('5 minutes UTC');
- });
-
- it('renders dropdown with quick range items', async () => {
- createComponent({
- value: {
- duration: { seconds: 60 * 2 },
- },
- options: otherTimeRanges,
- });
- dropdownToggle().trigger('click');
- await nextTick();
- const items = findQuickRangeItems();
-
- expect(items.length).toBe(Object.keys(otherTimeRanges).length);
- expect(items.at(0).text()).toBe('1 minute');
- expect(items.at(0).classes()).not.toContain('active');
-
- expect(items.at(1).text()).toBe('2 minutes');
- expect(items.at(1).classes()).toContain('active');
-
- expect(items.at(2).text()).toBe('5 minutes');
- expect(items.at(2).classes()).not.toContain('active');
- });
-
- it('renders dropdown with a label not in the quick range', async () => {
- createComponent({
- value: {
- duration: { seconds: 60 * 4 },
- },
- });
- dropdownToggle().trigger('click');
- await nextTick();
- expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00');
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap
index eb0adb0bebd..a0b1bb7df09 100644
--- a/spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap
+++ b/spec/frontend/vue_shared/components/design_management/__snapshots__/design_note_pin_spec.js.snap
@@ -3,20 +3,18 @@
exports[`Design note pin component should match the snapshot of note with index 1`] = `
<button
aria-label="Comment '1' position"
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm js-image-badge design-note-pin gl-absolute"
+ class="design-note-pin gl-absolute gl-align-items-center gl-display-flex gl-font-sm gl-justify-content-center js-image-badge"
style="left: 10px; top: 10px;"
type="button"
>
-
- 1
-
+ 1
</button>
`;
exports[`Design note pin component should match the snapshot of note without index 1`] = `
<button
aria-label="Comment form position"
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm btn-transparent comment-indicator gl-absolute"
+ class="btn-transparent comment-indicator gl-absolute gl-align-items-center gl-display-flex gl-font-sm gl-justify-content-center"
style="left: 10px; top: 10px;"
type="button"
>
@@ -30,7 +28,7 @@ exports[`Design note pin component should match the snapshot of note without ind
exports[`Design note pin component should match the snapshot when pin is resolved 1`] = `
<button
aria-label="Comment form position"
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm btn-transparent comment-indicator resolved gl-absolute"
+ class="btn-transparent comment-indicator gl-absolute gl-align-items-center gl-display-flex gl-font-sm gl-justify-content-center resolved"
style="left: 10px; top: 10px;"
type="button"
>
@@ -44,7 +42,7 @@ exports[`Design note pin component should match the snapshot when pin is resolve
exports[`Design note pin component should match the snapshot when position is absent 1`] = `
<button
aria-label="Comment form position"
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-font-sm btn-transparent comment-indicator"
+ class="btn-transparent comment-indicator gl-align-items-center gl-display-flex gl-font-sm gl-justify-content-center"
type="button"
>
<gl-icon-stub
diff --git a/spec/frontend/vue_shared/components/entity_select/utils_spec.js b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
index 9aa1baf204e..1d73924aa58 100644
--- a/spec/frontend/vue_shared/components/entity_select/utils_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
@@ -2,12 +2,16 @@ import { groupsPath } from '~/vue_shared/components/entity_select/utils';
describe('entity_select utils', () => {
describe('groupsPath', () => {
+ beforeEach(() => {
+ window.gon = { api_version: 'v4' };
+ });
+
it.each`
groupsFilter | parentGroupID | expectedPath
- ${undefined} | ${undefined} | ${'/api/:version/groups.json'}
- ${undefined} | ${1} | ${'/api/:version/groups.json'}
- ${'descendant_groups'} | ${1} | ${'/api/:version/groups/1/descendant_groups'}
- ${'subgroups'} | ${1} | ${'/api/:version/groups/1/subgroups'}
+ ${undefined} | ${undefined} | ${'/api/v4/groups.json'}
+ ${undefined} | ${1} | ${'/api/v4/groups.json'}
+ ${'descendant_groups'} | ${1} | ${'/api/v4/groups/1/descendant_groups'}
+ ${'subgroups'} | ${1} | ${'/api/v4/groups/1/subgroups'}
`(
'returns $expectedPath with groupsFilter = $groupsFilter and parentGroupID = $parentGroupID',
({ groupsFilter, parentGroupID, expectedPath }) => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index b2f4c780f51..a22ad4c450e 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -84,6 +84,19 @@ export const mockMilestones = [
mockEscapedMilestone,
];
+export const projectMilestonesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ attributes: {
+ nodes: mockMilestones,
+ __typename: 'MilestoneConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
export const mockCrmContacts = [
{
__typename: 'CustomerRelationsContact',
@@ -257,7 +270,8 @@ export const mockMilestoneToken = {
symbol: '%',
token: MilestoneToken,
operators: OPERATORS_IS,
- fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
+ fullPath: 'gitlab-org',
+ isProject: true,
};
export const mockReleaseToken = {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index db51b4a05b1..36e82b39df4 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -6,17 +6,27 @@ import {
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { sortMilestonesByDueDate } from '~/milestones/utils';
+import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql';
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { mockMilestoneToken, mockMilestones, mockRegularMilestone } from '../mock_data';
+import {
+ mockMilestoneToken,
+ mockMilestones,
+ mockRegularMilestone,
+ projectMilestonesResponse,
+} from '../mock_data';
+
+Vue.use(VueApollo);
jest.mock('~/alert');
jest.mock('~/milestones/utils');
@@ -31,6 +41,9 @@ const defaultStubs = {
},
};
+const milestonesQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse);
+const mockApollo = createMockApollo([[searchMilestonesQuery, milestonesQueryHandler]]);
+
function createComponent(options = {}) {
const {
config = { ...mockMilestoneToken, shouldSkipSort: true },
@@ -39,6 +52,7 @@ function createComponent(options = {}) {
stubs = defaultStubs,
} = options;
return mount(MilestoneToken, {
+ apolloProvider: mockApollo,
propsData: {
config,
value,
@@ -102,6 +116,33 @@ describe('MilestoneToken', () => {
});
});
+ describe('default - when fetchMilestones function is not provided in config', () => {
+ beforeEach(() => {
+ wrapper = createComponent({});
+ return triggerFetchMilestones();
+ });
+
+ it('calls searchMilestonesQuery to fetch milestones', () => {
+ expect(milestonesQueryHandler).toHaveBeenCalledWith({
+ fullPath: mockMilestoneToken.fullPath,
+ isProject: mockMilestoneToken.isProject,
+ search: null,
+ });
+ });
+
+ it('calls searchMilestonesQuery with search parameter when provided', async () => {
+ const searchTerm = 'foo';
+
+ await triggerFetchMilestones(searchTerm);
+
+ expect(milestonesQueryHandler).toHaveBeenCalledWith({
+ fullPath: mockMilestoneToken.fullPath,
+ isProject: mockMilestoneToken.isProject,
+ search: searchTerm,
+ });
+ });
+ });
+
describe('when request is successful', () => {
const searchTerm = 'foo';
diff --git a/spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap b/spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap
index 6f98a74a82f..52684cf4259 100644
--- a/spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap
+++ b/spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Form Footer Actions renders content properly 1`] = `
<footer
- class="gl-mt-5 footer-block"
+ class="footer-block gl-mt-5"
>
Bar Foo Abrakadabra
</footer>
diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
index 271214907fc..36efcb9efa8 100644
--- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
@@ -1,5 +1,5 @@
import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
@@ -123,24 +123,24 @@ describe('GlModalVuex', () => {
state.isVisible = false;
factory();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+ const rootWrapper = createWrapper(wrapper.vm.$root);
state.isVisible = true;
await nextTick();
- expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID);
+ expect(rootWrapper.emitted(BV_SHOW_MODAL)[0]).toContain(TEST_MODAL_ID);
});
it('calls bootstrap hide when isVisible changes', async () => {
state.isVisible = true;
factory();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+ const rootWrapper = createWrapper(wrapper.vm.$root);
state.isVisible = false;
await nextTick();
- expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID);
+ expect(rootWrapper.emitted(BV_HIDE_MODAL)[0]).toContain(TEST_MODAL_ID);
});
it.each(['ok', 'cancel'])(
diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
index 877de4f4695..cba9f78790d 100644
--- a/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/groups_list/groups_list_item_spec.js
@@ -9,6 +9,9 @@ import {
} from '~/visibility_level/constants';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+import DangerConfirmModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
import { groups } from './mock_data';
describe('GroupsListItem', () => {
@@ -30,6 +33,8 @@ describe('GroupsListItem', () => {
const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
const findGroupDescription = () => wrapper.findByTestId('group-description');
const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
+ const findListActions = () => wrapper.findComponent(ListActions);
+ const findConfirmationModal = () => wrapper.findComponent(DangerConfirmModal);
it('renders group avatar', () => {
createComponent();
@@ -179,4 +184,68 @@ describe('GroupsListItem', () => {
expect(wrapper.findByTestId('group-icon').exists()).toBe(false);
});
});
+
+ describe('when group has actions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays actions dropdown', () => {
+ expect(findListActions().props()).toMatchObject({
+ actions: {
+ [ACTION_EDIT]: {
+ href: group.editPath,
+ },
+ [ACTION_DELETE]: {
+ action: expect.any(Function),
+ },
+ },
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
+ });
+ });
+
+ describe('when delete action is fired', () => {
+ beforeEach(() => {
+ findListActions().props('actions')[ACTION_DELETE].action();
+ });
+
+ it('displays confirmation modal with correct props', () => {
+ expect(findConfirmationModal().props()).toMatchObject({
+ visible: true,
+ phrase: group.fullName,
+ });
+ });
+
+ describe('when deletion is confirmed', () => {
+ beforeEach(() => {
+ findConfirmationModal().vm.$emit('confirm');
+ });
+
+ it('emits `delete` event', () => {
+ expect(wrapper.emitted('delete')).toMatchObject([[group]]);
+ });
+ });
+ });
+ });
+
+ describe('when group does not have actions', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ group: {
+ ...group,
+ availableActions: [],
+ },
+ },
+ });
+ });
+
+ it('does not display actions dropdown', () => {
+ expect(findListActions().exists()).toBe(false);
+ });
+
+ it('does not display confirmation modal', () => {
+ expect(findConfirmationModal().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js b/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js
index c65aa347bcf..ec6a1dc9576 100644
--- a/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js
+++ b/spec/frontend/vue_shared/components/groups_list/groups_list_spec.js
@@ -31,4 +31,18 @@ describe('GroupsList', () => {
})),
);
});
+
+ describe('when `GroupsListItem` emits `delete` event', () => {
+ const [firstGroup] = defaultPropsData.groups;
+
+ beforeEach(() => {
+ createComponent();
+
+ wrapper.findComponent(GroupsListItem).vm.$emit('delete', firstGroup);
+ });
+
+ it('emits `delete` event', () => {
+ expect(wrapper.emitted('delete')).toEqual([[firstGroup]]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/groups_list/mock_data.js b/spec/frontend/vue_shared/components/groups_list/mock_data.js
index 0dad27f8311..08ee962892c 100644
--- a/spec/frontend/vue_shared/components/groups_list/mock_data.js
+++ b/spec/frontend/vue_shared/components/groups_list/mock_data.js
@@ -1,3 +1,5 @@
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+
export const groups = [
{
id: 1,
@@ -14,6 +16,8 @@ export const groups = [
accessLevel: {
integerValue: 10,
},
+ editPath: 'http://127.0.0.1:3000/groups/gitlab-org/-/edit',
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
},
{
id: 2,
@@ -31,5 +35,7 @@ export const groups = [
accessLevel: {
integerValue: 20,
},
+ editPath: 'http://127.0.0.1:3000/groups/gitlab-org/test-subgroup/-/edit',
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
},
];
diff --git a/spec/frontend/vue_shared/components/list_actions/list_actions_spec.js b/spec/frontend/vue_shared/components/list_actions/list_actions_spec.js
new file mode 100644
index 00000000000..ae70cf091a5
--- /dev/null
+++ b/spec/frontend/vue_shared/components/list_actions/list_actions_spec.js
@@ -0,0 +1,135 @@
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+
+describe('ListActions', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ actions: {
+ [ACTION_EDIT]: {
+ href: '/-/edit',
+ },
+ [ACTION_DELETE]: {
+ action: () => {},
+ },
+ },
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMountExtended(ListActions, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const getDropdownItemsProp = () => findDropdown().props('items');
+
+ it('allows extending of base actions', () => {
+ createComponent();
+
+ expect(getDropdownItemsProp()).toEqual([
+ {
+ text: 'Edit',
+ href: '/-/edit',
+ },
+ {
+ text: 'Delete',
+ extraAttrs: {
+ class: 'gl-text-red-500!',
+ },
+ action: expect.any(Function),
+ },
+ ]);
+ });
+
+ it('allows adding custom actions', () => {
+ const ACTION_LEAVE = 'leave';
+
+ createComponent({
+ propsData: {
+ actions: {
+ ...defaultPropsData.actions,
+ [ACTION_LEAVE]: {
+ text: 'Leave project',
+ action: () => {},
+ },
+ },
+ availableActions: [ACTION_EDIT, ACTION_LEAVE, ACTION_DELETE],
+ },
+ });
+
+ expect(getDropdownItemsProp()).toEqual([
+ {
+ text: 'Edit',
+ href: '/-/edit',
+ },
+ {
+ text: 'Leave project',
+ action: expect.any(Function),
+ },
+ {
+ text: 'Delete',
+ extraAttrs: {
+ class: 'gl-text-red-500!',
+ },
+ action: expect.any(Function),
+ },
+ ]);
+ });
+
+ it('only shows available actions', () => {
+ createComponent({
+ propsData: {
+ availableActions: [ACTION_EDIT],
+ },
+ });
+
+ expect(getDropdownItemsProp()).toEqual([
+ {
+ text: 'Edit',
+ href: '/-/edit',
+ },
+ ]);
+ });
+
+ it('displays actions in the order set in `availableActions` prop', () => {
+ createComponent({
+ propsData: {
+ availableActions: [ACTION_DELETE, ACTION_EDIT],
+ },
+ });
+
+ expect(getDropdownItemsProp()).toEqual([
+ {
+ text: 'Delete',
+ extraAttrs: {
+ class: 'gl-text-red-500!',
+ },
+ action: expect.any(Function),
+ },
+ {
+ text: 'Edit',
+ href: '/-/edit',
+ },
+ ]);
+ });
+
+ it('renders `GlDisclosureDropdown` with expected appearance related props', () => {
+ createComponent();
+
+ expect(findDropdown().props()).toMatchObject({
+ icon: 'ellipsis_v',
+ noCaret: true,
+ toggleText: 'Actions',
+ textSrOnly: true,
+ placement: 'right',
+ category: 'tertiary',
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/__snapshots__/suggestion_diff_spec.js.snap b/spec/frontend/vue_shared/components/markdown/__snapshots__/suggestion_diff_spec.js.snap
index 3b49536799c..baf40115e7a 100644
--- a/spec/frontend/vue_shared/components/markdown/__snapshots__/suggestion_diff_spec.js.snap
+++ b/spec/frontend/vue_shared/components/markdown/__snapshots__/suggestion_diff_spec.js.snap
@@ -13,9 +13,8 @@ exports[`Suggestion Diff component matches snapshot 1`] = `
isbatched="true"
suggestionscount="0"
/>
-
<table
- class="mb-3 md-suggestion-diff js-syntax-highlight code"
+ class="code js-syntax-highlight mb-3 md-suggestion-diff"
>
<tbody>
<suggestion-diff-row-stub
diff --git a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
index 8aab867f32a..cdbdbfab9d1 100644
--- a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ApplySuggestionComponent from '~/vue_shared/components/markdown/apply_suggestion.vue';
@@ -10,10 +10,11 @@ describe('Apply Suggestion component', () => {
wrapper = shallowMount(ApplySuggestionComponent, { propsData: { ...propsData, ...props } });
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findTextArea = () => wrapper.findComponent(GlFormTextarea);
const findApplyButton = () => wrapper.findComponent(GlButton);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findHelpText = () => wrapper.find('span');
beforeEach(() => createWrapper());
@@ -22,7 +23,7 @@ describe('Apply Suggestion component', () => {
const dropdown = findDropdown();
expect(dropdown.exists()).toBe(true);
- expect(dropdown.props('text')).toBe('Apply suggestion');
+ expect(dropdown.props('toggleText')).toBe('Apply suggestion');
expect(dropdown.props('disabled')).toBe(false);
});
@@ -41,6 +42,22 @@ describe('Apply Suggestion component', () => {
});
});
+ describe('help text', () => {
+ describe('when applying a single suggestion', () => {
+ it('renders the correct help text', () => {
+ expect(findHelpText().text()).toEqual('This also resolves this thread');
+ });
+ });
+
+ describe('when applying in batch', () => {
+ it('renders the correct help text', () => {
+ createWrapper({ batchSuggestionsCount: 3 });
+
+ expect(findHelpText().text()).toEqual('This also resolves all related threads');
+ });
+ });
+ });
+
describe('disabled', () => {
it('disables the dropdown', () => {
createWrapper({ disabled: true });
diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
index cd9f27dccbd..11c57fc5768 100644
--- a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
@@ -12,6 +12,7 @@ import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.qu
import {
TRACKING_SAVED_REPLIES_USE,
TRACKING_SAVED_REPLIES_USE_IN_MR,
+ TRACKING_SAVED_REPLIES_USE_IN_OTHER,
} from '~/vue_shared/components/markdown/constants';
let wrapper;
@@ -87,6 +88,12 @@ describe('Comment templates dropdown', () => {
});
describe('tracking', () => {
+ it('always sends two tracking events', async () => {
+ await selectSavedReply();
+
+ expect(trackingSpy).toHaveBeenCalledTimes(2);
+ });
+
it('tracks overall usage', async () => {
await selectSavedReply();
@@ -108,7 +115,6 @@ describe('Comment templates dropdown', () => {
TRACKING_SAVED_REPLIES_USE_IN_MR,
expect.any(Object),
);
- expect(trackingSpy).toHaveBeenCalledTimes(2);
});
it('is not sent when not in an MR', async () => {
@@ -121,7 +127,32 @@ describe('Comment templates dropdown', () => {
TRACKING_SAVED_REPLIES_USE_IN_MR,
expect.any(Object),
);
- expect(trackingSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('non-MR usage event', () => {
+ it('is sent when not in an MR', async () => {
+ window.location.toString.mockReturnValue('this/looks/like/a/-/issues/1');
+
+ await selectSavedReply();
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ expect.any(String),
+ TRACKING_SAVED_REPLIES_USE_IN_OTHER,
+ expect.any(Object),
+ );
+ });
+
+ it('is not sent when in an MR', async () => {
+ window.location.toString.mockReturnValue('this/looks/like/a/-/merge_requests/1');
+
+ await selectSavedReply();
+
+ expect(trackingSpy).not.toHaveBeenCalledWith(
+ expect.any(String),
+ TRACKING_SAVED_REPLIES_USE_IN_OTHER,
+ expect.any(Object),
+ );
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
index 1bbbe0896f2..f61c67c4f9b 100644
--- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
@@ -6,15 +6,27 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
describe('Markdown Field View component', () => {
- function createComponent() {
- shallowMount(MarkdownFieldView);
+ function createComponent(isLoading = false) {
+ shallowMount(MarkdownFieldView, { propsData: { isLoading } });
}
- beforeEach(() => {
+ it('processes rendering with GFM', () => {
createComponent();
- });
- it('processes rendering with GFM', () => {
expect(renderGFM).toHaveBeenCalledTimes(1);
});
+
+ describe('watchers', () => {
+ it('does not process rendering with GFM if isLoading is true', () => {
+ createComponent(true);
+
+ expect(renderGFM).not.toHaveBeenCalled();
+ });
+
+ it('processes rendering with GFM when isLoading is updated to `false`', () => {
+ createComponent(false);
+
+ expect(renderGFM).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index 31c0fa6f699..c69b18bca88 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -489,6 +489,11 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue);
});
+ it('does not autofocus the content editor', () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+ expect(findContentEditor().props().autofocus).toBe(false);
+ });
+
it('bubbles up keydown event', () => {
const event = new Event('keydown');
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
index 9768bc7a6dd..bc82357cb81 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -219,12 +219,11 @@ describe('Suggestion Diff component', () => {
describe('tooltip message for apply button', () => {
const findTooltip = () => getBinding(findApplyButton().element, 'gl-tooltip');
- it('renders correct tooltip message when button is applicable', () => {
- createComponent({ batchSuggestionsCount: 0 });
+ it('renders no tooltip message when button is applicable', () => {
+ createComponent({ batchSuggestionsCount: 1, isBatched: true });
const tooltip = findTooltip();
- expect(tooltip.modifiers.viewport).toBe(true);
- expect(tooltip.value).toBe('This also resolves this thread');
+ expect(tooltip.value).toBe(false);
});
it('renders the inapplicable reason in the tooltip when button is not applicable', () => {
diff --git a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
index 015049795a1..2dd7149069f 100644
--- a/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
+++ b/spec/frontend/vue_shared/components/metric_images/__snapshots__/metric_images_table_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Metrics upload item render the metrics image component 1`] = `
<gl-card-stub
bodyclass="gl-border-1,gl-border-t-solid,gl-border-gray-100,[object Object]"
- class="collapsible-card border gl-p-0 gl-mb-5"
+ class="border collapsible-card gl-mb-5 gl-p-0"
footerclass=""
headerclass="gl-display-flex gl-align-items-center gl-border-b-0 gl-py-3"
>
@@ -18,12 +18,10 @@ exports[`Metrics upload item render the metrics image component 1`] = `
size="sm"
titletag="h4"
>
-
<p>
Are you sure you wish to delete this image?
</p>
</gl-modal-stub>
-
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
@@ -35,7 +33,6 @@ exports[`Metrics upload item render the metrics image component 1`] = `
size="sm"
titletag="h4"
>
-
<gl-form-group-stub
label="Text (optional)"
label-for="upload-text-input"
@@ -44,10 +41,9 @@ exports[`Metrics upload item render the metrics image component 1`] = `
>
<gl-form-input-stub
data-testid="metric-image-text-field"
- id="upload-text-input"
+ id="reference-0"
/>
</gl-form-group-stub>
-
<gl-form-group-stub
description="Must start with http or https"
label="Link (optional)"
@@ -57,17 +53,16 @@ exports[`Metrics upload item render the metrics image component 1`] = `
>
<gl-form-input-stub
data-testid="metric-image-url-field"
- id="upload-url-input"
+ id="reference-1"
/>
</gl-form-group-stub>
</gl-modal-stub>
-
<div
class="gl-display-flex gl-flex-direction-column"
data-testid="metric-image-body"
>
<img
- class="gl-max-w-full gl-align-self-center"
+ class="gl-align-self-center gl-max-w-full"
src="test_file_path"
/>
</div>
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
index 8a187f3cb1f..891b0c95f0e 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/noteable_warning_spec.js.snap
@@ -2,10 +2,7 @@
exports[`Issue Warning Component when issue is locked but not confidential renders information about locked issue 1`] = `
<span>
-
- This issue is locked.
- Only project members can comment.
-
+ This issue is locked. Only project members can comment.
<gl-link-stub
href="locked-path"
target="_blank"
@@ -17,10 +14,7 @@ exports[`Issue Warning Component when issue is locked but not confidential rende
exports[`Issue Warning Component when noteable is confidential but not locked renders information about confidential issue 1`] = `
<span>
-
- This is a confidential issue.
- People without permission will never get a notification.
-
+ This is a confidential issue. People without permission will never get a notification.
<gl-link-stub
href="confidential-path"
target="_blank"
@@ -33,14 +27,14 @@ exports[`Issue Warning Component when noteable is confidential but not locked re
exports[`Issue Warning Component when noteable is locked and confidential renders information about locked and confidential noteable 1`] = `
<span>
<span>
- This issue is
+ This issue is
<gl-link-stub
href=""
target="_blank"
>
confidential
</gl-link-stub>
- and
+ and
<gl-link-stub
href=""
target="_blank"
@@ -49,8 +43,6 @@ exports[`Issue Warning Component when noteable is locked and confidential render
</gl-link-stub>
.
</span>
-
- People without permission will never get a notification and won't be able to comment.
-
+ People without permission will never get a notification and won't be able to comment.
</span>
`;
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
index de53caa66c7..c489fb08be5 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap
@@ -2,10 +2,10 @@
exports[`Issue placeholder note component matches snapshot 1`] = `
<timeline-entry-item-stub
- class="note note-wrapper note-comment being-posted fade-in-half"
+ class="being-posted fade-in-half note note-comment note-wrapper"
>
<div
- class="timeline-avatar gl-float-left"
+ class="gl-float-left timeline-avatar"
>
<gl-avatar-link-stub
href="/root"
@@ -20,9 +20,8 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
/>
</gl-avatar-link-stub>
</div>
-
<div
- class="timeline-content discussion"
+ class="discussion timeline-content"
>
<div
class="note-header"
@@ -34,11 +33,10 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
href="/root"
>
<span
- class="d-none d-sm-inline-block bold"
+ class="bold d-none d-sm-inline-block"
>
Root
</span>
-
<span
class="note-headline-light"
>
@@ -47,7 +45,6 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
</a>
</div>
</div>
-
<div
class="timeline-discussion-body"
>
@@ -55,15 +52,13 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
class="note-body"
>
<div
- class="note-text md"
+ class="md note-text"
>
<p
dir="auto"
>
Foo
</p>
-
-
</div>
</div>
</div>
diff --git a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_system_note_spec.js.snap b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_system_note_spec.js.snap
index 10c33269107..a609df5e775 100644
--- a/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_system_note_spec.js.snap
+++ b/spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_system_note_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Placeholder system note component matches snapshot 1`] = `
<timeline-entry-item-stub
- class="note system-note being-posted fade-in-half"
+ class="being-posted fade-in-half note system-note"
>
<div
class="timeline-content"
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index e5b641c61fd..335c5bdfc46 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -291,7 +291,7 @@ describe('AlertManagementEmptyState', () => {
it('renders the search component for incidents', () => {
const filteredSearchBar = findFilteredSearchBar();
- expect(filteredSearchBar.props('searchInputPlaceholder')).toBe('Search or filter results…');
+
expect(filteredSearchBar.props('tokens')).toEqual([
{
type: TOKEN_TYPE_AUTHOR,
diff --git a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
index 2a1a6342c38..4cb1c1f3616 100644
--- a/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js
@@ -1,4 +1,4 @@
-import { GlPagination, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlPagination, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
@@ -42,8 +42,8 @@ describe('Pagination bar', () => {
});
it('emits set-page-size event when page size is selected', () => {
- const firstItemInPageSizeDropdown = wrapper.findComponent(GlDropdownItem);
- firstItemInPageSizeDropdown.vm.$emit('click');
+ const firstItemInPageSizeDropdown = wrapper.findComponent(GlDisclosureDropdownItem);
+ firstItemInPageSizeDropdown.vm.$emit('action');
const [emittedPageSizeChange] = wrapper.emitted('set-page-size')[0];
expect(firstItemInPageSizeDropdown.text()).toMatchInterpolatedText(
@@ -62,9 +62,9 @@ describe('Pagination bar', () => {
},
});
- expect(wrapper.findComponent(GlDropdown).find('button').text()).toMatchInterpolatedText(
- `${CURRENT_PAGE_SIZE} items per page`,
- );
+ expect(
+ wrapper.findComponent(GlDisclosureDropdown).find('button').text(),
+ ).toMatchInterpolatedText(`${CURRENT_PAGE_SIZE} items per page`);
});
it('renders current page information', () => {
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
index 2490422e4e8..7cf560745b6 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -1,10 +1,10 @@
-import { GlAvatarLabeled, GlBadge, GlIcon, GlPopover, GlDisclosureDropdown } from '@gitlab/ui';
+import { GlAvatarLabeled, GlBadge, GlIcon, GlPopover } from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
import projects from 'test_fixtures/api/users/projects/get.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { __ } from '~/locale';
import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue';
-import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/projects_list/constants';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import {
@@ -43,6 +43,7 @@ describe('ProjectsListItem', () => {
const findPopover = () => findProjectTopics().findComponent(GlPopover);
const findProjectDescription = () => wrapper.findByTestId('project-description');
const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
+ const findListActions = () => wrapper.findComponent(ListActions);
beforeEach(() => {
uniqueId.mockImplementation(jest.requireActual('lodash/uniqueId'));
@@ -327,7 +328,7 @@ describe('ProjectsListItem', () => {
propsData: {
project: {
...project,
- actions: [ACTION_EDIT, ACTION_DELETE],
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
isForked: true,
editPath,
},
@@ -336,32 +337,22 @@ describe('ProjectsListItem', () => {
});
it('displays actions dropdown', () => {
- expect(wrapper.findComponent(GlDisclosureDropdown).props()).toMatchObject({
- items: [
- {
- id: ACTION_EDIT,
- text: __('Edit'),
+ expect(findListActions().props()).toMatchObject({
+ actions: {
+ [ACTION_EDIT]: {
href: editPath,
},
- {
- id: ACTION_DELETE,
- text: __('Delete'),
- extraAttrs: {
- class: 'gl-text-red-500!',
- },
+ [ACTION_DELETE]: {
action: expect.any(Function),
},
- ],
+ },
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
});
});
describe('when delete action is fired', () => {
beforeEach(() => {
- wrapper
- .findComponent(GlDisclosureDropdown)
- .props('items')
- .find((item) => item.id === ACTION_DELETE)
- .action();
+ findListActions().props('actions')[ACTION_DELETE].action();
});
it('displays confirmation modal with correct props', () => {
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
index eadcb6ceeb7..64bab2de3b7 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap
@@ -7,14 +7,12 @@ exports[`Package code instruction multiline to match the snapshot 1`] = `
>
foo_label
</label>
-
<div>
<pre
class="gl-font-monospace"
data-testid="multiline-instruction"
>
- this is some
-multiline text
+ this is somemultiline text
</pre>
</div>
</div>
@@ -23,25 +21,23 @@ multiline text
exports[`Package code instruction single line to match the default snapshot 1`] = `
<div>
<label
- for="instruction-input_1"
+ for="reference-0"
>
foo_label
</label>
-
<div
class="gl-mb-3"
>
<div
- class="input-group gl-mb-3"
+ class="gl-mb-3 input-group"
>
<input
class="form-control gl-font-monospace"
data-testid="instruction-input"
- id="instruction-input_1"
- readonly="readonly"
+ id="reference-0"
+ readonly=""
type="text"
/>
-
<span
class="input-group-append"
data-testid="instruction-button"
diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
index 5c487754b87..8eb0e08908b 100644
--- a/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
+++ b/spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap
@@ -2,20 +2,19 @@
exports[`History Item renders the correct markup 1`] = `
<li
- class="timeline-entry system-note note-wrapper"
+ class="note-wrapper system-note timeline-entry"
>
<div
class="timeline-entry-inner"
>
<div
- class="gl--flex-center gl-rounded-full gl-mt-n1 gl-ml-2 gl-w-6 gl-h-6 gl-bg-gray-50 gl-text-gray-600 gl-float-left"
+ class="gl--flex-center gl-bg-gray-50 gl-float-left gl-h-6 gl-ml-2 gl-mt-n1 gl-rounded-full gl-text-gray-600 gl-w-6"
>
<gl-icon-stub
name="pencil"
size="16"
/>
</div>
-
<div
class="timeline-content"
>
@@ -30,7 +29,6 @@ exports[`History Item renders the correct markup 1`] = `
/>
</div>
</div>
-
<div
class="note-body"
>
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
index 65427374e1b..22cfe8a5fc7 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
+++ b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap
@@ -23,7 +23,7 @@ exports[`Resizable Skeleton Loader default setup renders the bars, labels, and g
/>
<defs>
<clippath
- id="null-idClip"
+ id="reference-0"
>
<rect
data-testid="skeleton-chart-grid"
@@ -46,7 +46,6 @@ exports[`Resizable Skeleton Loader default setup renders the bars, labels, and g
x="0"
y="90%"
/>
-
<rect
data-testid="skeleton-chart-bar"
height="5%"
@@ -111,7 +110,6 @@ exports[`Resizable Skeleton Loader default setup renders the bars, labels, and g
x="90%"
y="10%"
/>
-
<rect
data-testid="skeleton-chart-label"
height="3%"
@@ -178,7 +176,7 @@ exports[`Resizable Skeleton Loader default setup renders the bars, labels, and g
/>
</clippath>
<lineargradient
- id="null-idGradient"
+ id="reference-1"
>
<stop
class="primary-stop"
@@ -242,7 +240,7 @@ exports[`Resizable Skeleton Loader with custom settings renders the correct posi
/>
<defs>
<clippath
- id="-idClip"
+ id="reference-0"
>
<rect
data-testid="skeleton-chart-grid"
@@ -265,7 +263,6 @@ exports[`Resizable Skeleton Loader with custom settings renders the correct posi
x="0"
y="90%"
/>
-
<rect
data-testid="skeleton-chart-bar"
height="5%"
@@ -330,7 +327,6 @@ exports[`Resizable Skeleton Loader with custom settings renders the correct posi
x="90.9375%"
y="10%"
/>
-
<rect
data-testid="skeleton-chart-label"
height="2%"
@@ -397,7 +393,7 @@ exports[`Resizable Skeleton Loader with custom settings renders the correct posi
/>
</clippath>
<lineargradient
- id="-idGradient"
+ id="reference-1"
>
<stop
class="primary-stop"
diff --git a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
index a0f46f07d6a..91d1b0accf1 100644
--- a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
+++ b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
@@ -9,11 +9,11 @@ exports[`Settings Block renders the correct markup 1`] = `
>
<h4>
<span
- aria-controls="settings_content_3"
+ aria-controls="reference-1"
aria-expanded="false"
class="gl-cursor-pointer"
data-testid="section-title-button"
- id="settings_label_2"
+ id="reference-0"
role="button"
tabindex="0"
>
@@ -22,9 +22,8 @@ exports[`Settings Block renders the correct markup 1`] = `
/>
</span>
</h4>
-
<gl-button-stub
- aria-controls="settings_content_3"
+ aria-controls="reference-1"
aria-expanded="false"
aria-label="Expand settings section"
buttontextclasses=""
@@ -33,22 +32,18 @@ exports[`Settings Block renders the correct markup 1`] = `
size="medium"
variant="default"
>
-
Expand
-
</gl-button-stub>
-
<p>
<div
data-testid="description-slot"
/>
</p>
</div>
-
<div
- aria-labelledby="settings_label_2"
+ aria-labelledby="reference-0"
class="settings-content"
- id="settings_content_3"
+ id="reference-1"
role="region"
style="display: none;"
tabindex="-1"
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap
index 26c9a6f8d5a..bab1920fd3a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap
+++ b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_new_spec.js.snap
@@ -2,23 +2,20 @@
exports[`Chunk component rendering isHighlighted is true renders line numbers 1`] = `
<div
- class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ class="diff-line-num gl-border-r gl-display-flex gl-p-0! gl-z-index-3 line-links line-numbers"
data-testid="line-numbers"
>
<a
- class="gl-user-select-none gl-shadow-none! file-line-blame"
+ class="file-line-blame gl-shadow-none! gl-user-select-none"
href="some/blame/path.js#L71"
/>
-
<a
- class="gl-user-select-none gl-shadow-none! file-line-num"
+ class="file-line-num gl-shadow-none! gl-user-select-none"
data-line-number="71"
href="#L71"
- id="L71"
+ id="reference-0"
>
-
- 71
-
+ 71
</a>
</div>
`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
index 1154c930e5d..852598b13dc 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
@@ -35,6 +35,7 @@ describe('Chunk component', () => {
await nextTick();
expect(findContent().exists()).toBe(true);
+ expect(wrapper.emitted('appear')).toHaveLength(1);
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
index 431ede17954..1a498d0c5b1 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
@@ -58,7 +58,8 @@ describe('Source Viewer component', () => {
describe('hash highlighting', () => {
it('calls highlightHash with expected parameter', () => {
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ const scrollEnabled = false;
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash, scrollEnabled);
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index a486d13a856..2043f36443d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -1,10 +1,9 @@
import hljs from 'highlight.js/lib/core';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import CodeownersValidation from 'ee_component/blob/components/codeowners_validation.vue';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
import {
@@ -24,11 +23,10 @@ import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
-jest.mock('~/blob/line_highlighter');
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () => jest.fn().mockReturnValue({ highlightHash: jest.fn() }));
jest.mock('highlight.js/lib/core');
jest.mock('~/vue_shared/components/source_viewer/plugins/index');
-Vue.use(VueRouter);
-const router = new VueRouter();
const mockAxios = new MockAdapter(axios);
const generateContent = (content, totalLines = 1, delimiter = '\n') => {
@@ -44,6 +42,7 @@ const execImmediately = (callback) => callback();
describe('Source Viewer component', () => {
let wrapper;
const language = 'docker';
+ const selectedRangeHash = '#L1-2';
const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
const chunk1 = generateContent('// Some source code 1', 70);
const chunk2 = generateContent('// Some source code 2', 70);
@@ -55,11 +54,13 @@ describe('Source Viewer component', () => {
const fileType = 'javascript';
const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
+ const currentRef = 'main';
+ const projectPath = 'test/project';
const createComponent = async (blob = {}) => {
wrapper = shallowMountExtended(SourceViewer, {
- router,
- propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
+ propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob }, currentRef, projectPath },
+ mocks: { $route: { hash: selectedRangeHash } },
});
await waitForPromises();
};
@@ -268,5 +269,25 @@ describe('Source Viewer component', () => {
it('instantiates the lineHighlighter class', () => {
expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
});
+
+ it('highlights the range when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ const scrollEnabled = false;
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(selectedRangeHash, scrollEnabled);
+ });
+ });
+
+ describe('Codeowners validation', () => {
+ const findCodeownersValidation = () => wrapper.findComponent(CodeownersValidation);
+
+ it('does not render codeowners validation when file is not CODEOWNERS', async () => {
+ await createComponent();
+ expect(findCodeownersValidation().exists()).toBe(false);
+ });
+
+ it('renders codeowners validation when file is CODEOWNERS', async () => {
+ await createComponent({ name: CODEOWNERS_FILE_NAME });
+ expect(findCodeownersValidation().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
deleted file mode 100644
index ffa25ae8448..00000000000
--- a/spec/frontend/vue_shared/components/split_button_spec.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-
-import { nextTick } from 'vue';
-import { assertProps } from 'helpers/assert_props';
-import SplitButton from '~/vue_shared/components/split_button.vue';
-
-const mockActionItems = [
- {
- eventName: 'concert',
- title: 'professor',
- description: 'very symphonic',
- },
- {
- eventName: 'apocalypse',
- title: 'captain',
- description: 'warp drive',
- },
-];
-
-describe('SplitButton', () => {
- let wrapper;
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(SplitButton, {
- propsData,
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItem = (index = 0) =>
- findDropdown().findAllComponents(GlDropdownItem).at(index);
- const selectItem = async (index) => {
- findDropdownItem(index).vm.$emit('click');
-
- await nextTick();
- };
- const clickToggleButton = async () => {
- findDropdown().vm.$emit('click');
-
- await nextTick();
- };
-
- it('fails for empty actionItems', () => {
- const actionItems = [];
- expect(() => assertProps(SplitButton, { actionItems })).toThrow();
- });
-
- it('fails for single actionItems', () => {
- const actionItems = [mockActionItems[0]];
- expect(() => assertProps(SplitButton, { actionItems })).toThrow();
- });
-
- it('renders actionItems', () => {
- createComponent({ actionItems: mockActionItems });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('toggle button text', () => {
- beforeEach(() => {
- createComponent({ actionItems: mockActionItems });
- });
-
- it('defaults to first actionItems title', () => {
- expect(findDropdown().props().text).toBe(mockActionItems[0].title);
- });
-
- it('changes to selected actionItems title', () =>
- selectItem(1).then(() => {
- expect(findDropdown().props().text).toBe(mockActionItems[1].title);
- }));
- });
-
- describe('emitted event', () => {
- let eventHandler;
- let changeEventHandler;
-
- beforeEach(() => {
- createComponent({ actionItems: mockActionItems });
- });
-
- const addEventHandler = ({ eventName }) => {
- eventHandler = jest.fn();
- wrapper.vm.$once(eventName, () => eventHandler());
- };
-
- const addChangeEventHandler = () => {
- changeEventHandler = jest.fn();
- wrapper.vm.$once('change', (item) => changeEventHandler(item));
- };
-
- it('defaults to first actionItems event', () => {
- addEventHandler(mockActionItems[0]);
-
- return clickToggleButton().then(() => {
- expect(eventHandler).toHaveBeenCalled();
- });
- });
-
- it('changes to selected actionItems event', () =>
- selectItem(1)
- .then(() => addEventHandler(mockActionItems[1]))
- .then(clickToggleButton)
- .then(() => {
- expect(eventHandler).toHaveBeenCalled();
- }));
-
- it('change to selected actionItem emits change event', () => {
- addChangeEventHandler();
-
- return selectItem(1).then(() => {
- expect(changeEventHandler).toHaveBeenCalledWith(mockActionItems[1]);
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
index c816fe790a8..cffe26f8175 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
+++ b/spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap
@@ -2,14 +2,14 @@
exports[`Upload dropzone component correctly overrides description and drop messages 1`] = `
<div
- class="gl-w-full gl-relative"
+ class="gl-relative gl-w-full"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0"
+ class="card gl-align-items-center gl-h-full gl-justify-content-center gl-mb-0 gl-px-5 gl-py-4 gl-w-full upload-dropzone-border upload-dropzone-card"
type="button"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ class="gl-align-items-center gl-display-flex gl-flex-direction-column gl-justify-content-center gl-text-center"
data-testid="dropzone-area"
>
<gl-icon-stub
@@ -17,7 +17,6 @@ exports[`Upload dropzone component correctly overrides description and drop mess
name="upload"
size="24"
/>
-
<p
class="gl-mb-0"
data-testid="upload-text"
@@ -28,50 +27,37 @@ exports[`Upload dropzone component correctly overrides description and drop mess
</p>
</div>
</button>
-
<input
accept="image/jpg,image/jpeg"
class="hide"
- multiple="multiple"
+ multiple=""
name="upload_file"
type="file"
/>
-
<transition-stub
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card gl-absolute gl-align-items-center gl-display-flex gl-h-full gl-justify-content-center gl-p-4 gl-w-full upload-dropzone-border upload-dropzone-overlay"
style="display: none;"
>
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style="display: none;"
>
- <h3
- class=""
- >
-
- Oh no!
-
+ <h3>
+ Oh no!
</h3>
-
<span>
You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
</span>
</div>
-
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
>
- <h3
- class=""
- >
-
- Incoming!
-
+ <h3>
+ Incoming!
</h3>
-
<span>
Test drop-to-start message.
</span>
@@ -83,14 +69,14 @@ exports[`Upload dropzone component correctly overrides description and drop mess
exports[`Upload dropzone component when dragging renders correct template when drag event contains files 1`] = `
<div
- class="gl-w-full gl-relative"
+ class="gl-relative gl-w-full"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0"
+ class="card gl-align-items-center gl-h-full gl-justify-content-center gl-mb-0 gl-px-5 gl-py-4 gl-w-full upload-dropzone-border upload-dropzone-card"
type="button"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ class="gl-align-items-center gl-display-flex gl-flex-direction-column gl-justify-content-center gl-text-center"
data-testid="dropzone-area"
>
<gl-icon-stub
@@ -98,65 +84,49 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload"
size="24"
/>
-
<p
class="gl-mb-0"
data-testid="upload-text"
>
- Drop or
+ Drop or
<gl-link-stub>
-
- upload
-
+ upload
</gl-link-stub>
- files to attach
+ files to attach
</p>
</div>
</button>
-
<input
accept="image/*"
class="hide"
- multiple="multiple"
+ multiple=""
name="upload_file"
type="file"
/>
-
<transition-stub
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card gl-absolute gl-align-items-center gl-display-flex gl-h-full gl-justify-content-center gl-p-4 gl-w-full upload-dropzone-border upload-dropzone-overlay"
style=""
>
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style="display: none;"
>
- <h3
- class=""
- >
-
- Oh no!
-
+ <h3>
+ Oh no!
</h3>
-
<span>
You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
</span>
</div>
-
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
>
- <h3
- class=""
- >
-
- Incoming!
-
+ <h3>
+ Incoming!
</h3>
-
<span>
Drop your files to start your upload.
</span>
@@ -168,14 +138,14 @@ exports[`Upload dropzone component when dragging renders correct template when d
exports[`Upload dropzone component when dragging renders correct template when drag event contains files and text 1`] = `
<div
- class="gl-w-full gl-relative"
+ class="gl-relative gl-w-full"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0"
+ class="card gl-align-items-center gl-h-full gl-justify-content-center gl-mb-0 gl-px-5 gl-py-4 gl-w-full upload-dropzone-border upload-dropzone-card"
type="button"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ class="gl-align-items-center gl-display-flex gl-flex-direction-column gl-justify-content-center gl-text-center"
data-testid="dropzone-area"
>
<gl-icon-stub
@@ -183,65 +153,49 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload"
size="24"
/>
-
<p
class="gl-mb-0"
data-testid="upload-text"
>
- Drop or
+ Drop or
<gl-link-stub>
-
- upload
-
+ upload
</gl-link-stub>
- files to attach
+ files to attach
</p>
</div>
</button>
-
<input
accept="image/*"
class="hide"
- multiple="multiple"
+ multiple=""
name="upload_file"
type="file"
/>
-
<transition-stub
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card gl-absolute gl-align-items-center gl-display-flex gl-h-full gl-justify-content-center gl-p-4 gl-w-full upload-dropzone-border upload-dropzone-overlay"
style=""
>
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style="display: none;"
>
- <h3
- class=""
- >
-
- Oh no!
-
+ <h3>
+ Oh no!
</h3>
-
<span>
You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
</span>
</div>
-
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
>
- <h3
- class=""
- >
-
- Incoming!
-
+ <h3>
+ Incoming!
</h3>
-
<span>
Drop your files to start your upload.
</span>
@@ -253,14 +207,14 @@ exports[`Upload dropzone component when dragging renders correct template when d
exports[`Upload dropzone component when dragging renders correct template when drag event contains text 1`] = `
<div
- class="gl-w-full gl-relative"
+ class="gl-relative gl-w-full"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0"
+ class="card gl-align-items-center gl-h-full gl-justify-content-center gl-mb-0 gl-px-5 gl-py-4 gl-w-full upload-dropzone-border upload-dropzone-card"
type="button"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ class="gl-align-items-center gl-display-flex gl-flex-direction-column gl-justify-content-center gl-text-center"
data-testid="dropzone-area"
>
<gl-icon-stub
@@ -268,66 +222,50 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload"
size="24"
/>
-
<p
class="gl-mb-0"
data-testid="upload-text"
>
- Drop or
+ Drop or
<gl-link-stub>
-
- upload
-
+ upload
</gl-link-stub>
- files to attach
+ files to attach
</p>
</div>
</button>
-
<input
accept="image/*"
class="hide"
- multiple="multiple"
+ multiple=""
name="upload_file"
type="file"
/>
-
<transition-stub
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card gl-absolute gl-align-items-center gl-display-flex gl-h-full gl-justify-content-center gl-p-4 gl-w-full upload-dropzone-border upload-dropzone-overlay"
style=""
>
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style=""
>
- <h3
- class=""
- >
-
- Oh no!
-
+ <h3>
+ Oh no!
</h3>
-
<span>
You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
</span>
</div>
-
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style="display: none;"
>
- <h3
- class=""
- >
-
- Incoming!
-
+ <h3>
+ Incoming!
</h3>
-
<span>
Drop your files to start your upload.
</span>
@@ -339,14 +277,14 @@ exports[`Upload dropzone component when dragging renders correct template when d
exports[`Upload dropzone component when dragging renders correct template when drag event is empty 1`] = `
<div
- class="gl-w-full gl-relative"
+ class="gl-relative gl-w-full"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0"
+ class="card gl-align-items-center gl-h-full gl-justify-content-center gl-mb-0 gl-px-5 gl-py-4 gl-w-full upload-dropzone-border upload-dropzone-card"
type="button"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ class="gl-align-items-center gl-display-flex gl-flex-direction-column gl-justify-content-center gl-text-center"
data-testid="dropzone-area"
>
<gl-icon-stub
@@ -354,66 +292,50 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload"
size="24"
/>
-
<p
class="gl-mb-0"
data-testid="upload-text"
>
- Drop or
+ Drop or
<gl-link-stub>
-
- upload
-
+ upload
</gl-link-stub>
- files to attach
+ files to attach
</p>
</div>
</button>
-
<input
accept="image/*"
class="hide"
- multiple="multiple"
+ multiple=""
name="upload_file"
type="file"
/>
-
<transition-stub
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card gl-absolute gl-align-items-center gl-display-flex gl-h-full gl-justify-content-center gl-p-4 gl-w-full upload-dropzone-border upload-dropzone-overlay"
style=""
>
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style=""
>
- <h3
- class=""
- >
-
- Oh no!
-
+ <h3>
+ Oh no!
</h3>
-
<span>
You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
</span>
</div>
-
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style="display: none;"
>
- <h3
- class=""
- >
-
- Incoming!
-
+ <h3>
+ Incoming!
</h3>
-
<span>
Drop your files to start your upload.
</span>
@@ -425,14 +347,14 @@ exports[`Upload dropzone component when dragging renders correct template when d
exports[`Upload dropzone component when dragging renders correct template when dragging stops 1`] = `
<div
- class="gl-w-full gl-relative"
+ class="gl-relative gl-w-full"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0"
+ class="card gl-align-items-center gl-h-full gl-justify-content-center gl-mb-0 gl-px-5 gl-py-4 gl-w-full upload-dropzone-border upload-dropzone-card"
type="button"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ class="gl-align-items-center gl-display-flex gl-flex-direction-column gl-justify-content-center gl-text-center"
data-testid="dropzone-area"
>
<gl-icon-stub
@@ -440,66 +362,50 @@ exports[`Upload dropzone component when dragging renders correct template when d
name="upload"
size="24"
/>
-
<p
class="gl-mb-0"
data-testid="upload-text"
>
- Drop or
+ Drop or
<gl-link-stub>
-
- upload
-
+ upload
</gl-link-stub>
- files to attach
+ files to attach
</p>
</div>
</button>
-
<input
accept="image/*"
class="hide"
- multiple="multiple"
+ multiple=""
name="upload_file"
type="file"
/>
-
<transition-stub
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card gl-absolute gl-align-items-center gl-display-flex gl-h-full gl-justify-content-center gl-p-4 gl-w-full upload-dropzone-border upload-dropzone-overlay"
style="display: none;"
>
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style=""
>
- <h3
- class=""
- >
-
- Oh no!
-
+ <h3>
+ Oh no!
</h3>
-
<span>
You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
</span>
</div>
-
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style="display: none;"
>
- <h3
- class=""
- >
-
- Incoming!
-
+ <h3>
+ Incoming!
</h3>
-
<span>
Drop your files to start your upload.
</span>
@@ -511,14 +417,14 @@ exports[`Upload dropzone component when dragging renders correct template when d
exports[`Upload dropzone component when no slot provided renders default dropzone card 1`] = `
<div
- class="gl-w-full gl-relative"
+ class="gl-relative gl-w-full"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0"
+ class="card gl-align-items-center gl-h-full gl-justify-content-center gl-mb-0 gl-px-5 gl-py-4 gl-w-full upload-dropzone-border upload-dropzone-card"
type="button"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ class="gl-align-items-center gl-display-flex gl-flex-direction-column gl-justify-content-center gl-text-center"
data-testid="dropzone-area"
>
<gl-icon-stub
@@ -526,65 +432,49 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
name="upload"
size="24"
/>
-
<p
class="gl-mb-0"
data-testid="upload-text"
>
- Drop or
+ Drop or
<gl-link-stub>
-
- upload
-
+ upload
</gl-link-stub>
- files to attach
+ files to attach
</p>
</div>
</button>
-
<input
accept="image/*"
class="hide"
- multiple="multiple"
+ multiple=""
name="upload_file"
type="file"
/>
-
<transition-stub
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card gl-absolute gl-align-items-center gl-display-flex gl-h-full gl-justify-content-center gl-p-4 gl-w-full upload-dropzone-border upload-dropzone-overlay"
style="display: none;"
>
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style="display: none;"
>
- <h3
- class=""
- >
-
- Oh no!
-
+ <h3>
+ Oh no!
</h3>
-
<span>
You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
</span>
</div>
-
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
>
- <h3
- class=""
- >
-
- Incoming!
-
+ <h3>
+ Incoming!
</h3>
-
<span>
Drop your files to start your upload.
</span>
@@ -596,47 +486,35 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
exports[`Upload dropzone component when slot provided renders dropzone with slot content 1`] = `
<div
- class="gl-w-full gl-relative"
+ class="gl-relative gl-w-full"
>
<div>
dropzone slot
</div>
-
<transition-stub
name="upload-dropzone-fade"
>
<div
- class="card upload-dropzone-border upload-dropzone-overlay gl-w-full gl-h-full gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-4"
+ class="card gl-absolute gl-align-items-center gl-display-flex gl-h-full gl-justify-content-center gl-p-4 gl-w-full upload-dropzone-border upload-dropzone-overlay"
style="display: none;"
>
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
style="display: none;"
>
- <h3
- class=""
- >
-
- Oh no!
-
+ <h3>
+ Oh no!
</h3>
-
<span>
You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
</span>
</div>
-
<div
- class="mw-50 gl-text-center"
+ class="gl-text-center mw-50"
>
- <h3
- class=""
- >
-
- Incoming!
-
+ <h3>
+ Incoming!
</h3>
-
<span>
Drop your files to start your upload.
</span>
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 8c7657da8bc..119b892392f 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -5,17 +5,17 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
-import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
+import searchUsersQuery from '~/graphql_shared/queries/project_autocomplete_users.query.graphql';
+import searchUsersQueryOnMR from '~/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import getIssueParticipantsQuery from '~/sidebar/queries/get_issue_participants.query.graphql';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import {
- searchResponse,
- searchResponseOnMR,
- projectMembersResponse,
+ projectAutocompleteMembersResponse,
+ searchAutocompleteQueryResponse,
+ searchAutocompleteResponseOnMR,
participantsQueryResponse,
mockUser1,
mockUser2,
@@ -59,7 +59,7 @@ describe('User select dropdown', () => {
const findUnassignLink = () => wrapper.findByTestId('unassign');
const findEmptySearchResults = () => wrapper.findAllByTestId('empty-results');
- const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
+ const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectAutocompleteMembersResponse);
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
const createComponent = ({
@@ -69,7 +69,7 @@ describe('User select dropdown', () => {
} = {}) => {
fakeApollo = createMockApollo([
[searchUsersQuery, searchQueryHandler],
- [searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
+ [searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchAutocompleteResponseOnMR)],
[getIssueParticipantsQuery, participantsQueryHandler],
]);
wrapper = shallowMountExtended(UserSelect, {
@@ -200,7 +200,7 @@ describe('User select dropdown', () => {
});
await waitForPromises();
- expect(findUnselectedParticipantByIndex(0).props('user')).toEqual(mockUser2);
+ expect(findUnselectedParticipantByIndex(0).props('user')).toMatchObject(mockUser2);
});
it('moves issuable author on top of unassigned list after current user, if author and current user are unassigned project members', async () => {
@@ -372,7 +372,9 @@ describe('User select dropdown', () => {
});
it('renders a list of found users and external participants matching search term', async () => {
- createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) });
+ createComponent({
+ searchQueryHandler: jest.fn().mockResolvedValue(searchAutocompleteQueryResponse),
+ });
await waitForPromises();
findSearchField().vm.$emit('input', 'ro');
@@ -382,7 +384,9 @@ describe('User select dropdown', () => {
});
it('renders a list of found users only if no external participants match search term', async () => {
- createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) });
+ createComponent({
+ searchQueryHandler: jest.fn().mockResolvedValue(searchAutocompleteQueryResponse),
+ });
await waitForPromises();
findSearchField().vm.$emit('input', 'roo');
@@ -392,8 +396,8 @@ describe('User select dropdown', () => {
});
it('shows a message about no matches if search returned an empty list', async () => {
- const responseCopy = cloneDeep(searchResponse);
- responseCopy.data.workspace.users.nodes = [];
+ const responseCopy = cloneDeep(searchAutocompleteQueryResponse);
+ responseCopy.data.workspace.users = [];
createComponent({
searchQueryHandler: jest.fn().mockResolvedValue(responseCopy),
diff --git a/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap b/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
deleted file mode 100644
index 1d4aa1afeaf..00000000000
--- a/spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap
+++ /dev/null
@@ -1,30 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`IssuableBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = `
-"<div class=\\"gl-display-inline\\"><svg data-testid=\\"issuable-blocked-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"issuable-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500 gl-icon s16\\" id=\\"blocked-icon-uniqueId\\">
- <use href=\\"file-mock#issue-block\\"></use>
- </svg>
- <div class=\\"gl-popover\\">
- <ul class=\\"gl-list-style-none gl-p-0 gl-mb-0\\">
- <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/6\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#6</a>
- <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-3\\">
- blocking issue title 1
- </p>
- </li>
- <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/5\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#5</a>
- <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-3\\">
- blocking issue title 2 + blocking issue title 2 + blocking issue title 2 + bloc…
- </p>
- </li>
- <li><a href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/4\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">my-project-1#4</a>
- <p data-testid=\\"issuable-title\\" class=\\"gl-display-block! gl-mb-0\\">
- blocking issue title 3
- </p>
- </li>
- </ul>
- <div class=\\"gl-mt-4\\">
- <p data-testid=\\"hidden-blocking-count\\" class=\\"gl-mb-3\\">+ 1 more issue</p> <a data-testid=\\"view-all-issues\\" href=\\"http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0#related-issues\\" class=\\"gl-link gl-text-blue-500! gl-font-sm\\">View all blocking issues</a>
- </div><span data-testid=\\"popover-title\\">Blocked by 4 issues</span>
- </div>
-</div>"
-`;
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
index 338dc80b43e..62361705843 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
@@ -81,7 +81,7 @@ describe('IssuableForm', () => {
ariaLabel: __('Description'),
class: 'rspec-issuable-form-description',
placeholder: __('Write a comment or drag your files here…'),
- dataQaSelector: 'issuable_form_description_field',
+ dataTestid: 'issuable-form-description-field',
id: 'issuable-description',
name: 'issuable-description',
},
diff --git a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
index d5603d4ba4b..6512da07125 100644
--- a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
+++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
@@ -237,10 +237,6 @@ describe('IssuableBlockedIcon', () => {
await mouseenter();
});
- it('matches the snapshot', () => {
- expect(wrapper.html()).toMatchSnapshot();
- });
-
it('should render popover title with correct blocking issuable count', () => {
expect(findPopoverTitle().text()).toBe('Blocked by 4 issues');
});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 77333a878d1..9f7254ba0e6 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -1,5 +1,6 @@
import { GlLink, GlLabel, GlIcon, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { escape } from 'lodash';
import { useFakeDate } from 'helpers/fake_date';
import { shallowMountExtended as shallowMount } from 'helpers/vue_test_utils_helper';
import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue';
@@ -63,6 +64,14 @@ describe('IssuableItem', () => {
});
});
+ describe('externalAuthor', () => {
+ it('returns `externalAuthor` reference', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.vm.externalAuthor).toEqual(mockIssuable.externalAuthor);
+ });
+ });
+
describe('authorId', () => {
it.each`
authorId | returnValue
@@ -279,10 +288,23 @@ describe('IssuableItem', () => {
expect(titleEl.exists()).toBe(true);
expect(titleEl.findComponent(GlLink).attributes('href')).toBe(expectedHref);
expect(titleEl.findComponent(GlLink).attributes('target')).toBe(expectedTarget);
- expect(titleEl.findComponent(GlLink).text()).toBe(mockIssuable.title);
+ expect(titleEl.findComponent(GlLink).html()).toContain(mockIssuable.titleHtml);
},
);
+ it('renders issuable title with escaped markup when issue tracker is external', () => {
+ const mockTitle = '<script>foobar</script>';
+ wrapper = createComponent({
+ issuable: {
+ ...mockIssuable,
+ title: mockTitle,
+ externalTracker: 'jira',
+ },
+ });
+
+ expect(wrapper.findByTestId('issuable-title').html()).toContain(escape(mockTitle));
+ });
+
it('renders checkbox when `showCheckbox` prop is true', async () => {
wrapper = createComponent({
showCheckbox: true,
@@ -437,6 +459,15 @@ describe('IssuableItem', () => {
expect(authorEl.text()).toBe(mockAuthor.name);
});
+ it('renders issuable external author info via author slot', () => {
+ wrapper = createComponent({
+ issuableSymbol: '#',
+ issuable: { ...mockIssuable, externalAuthor: 'client@example.com' },
+ });
+
+ expect(wrapper.findByTestId('external-author').text()).toBe('client@example.com via');
+ });
+
it('renders timeframe via slot', () => {
wrapper = createComponent({
issuableSymbol: '#',
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index f8cf3ba5271..b39d177f292 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -42,7 +42,7 @@ export const mockCurrentUserTodo = {
export const mockIssuable = {
iid: '30',
title: 'Dismiss Cipher with no integrity',
- titleHtml: 'Dismiss Cipher with no integrity',
+ titleHtml: '<gl-emoji title="party-parrot"></gl-emoji>Dismiss Cipher with no integrity',
description: 'fortitudinis _fomentis_ dolor mitigari solet.',
descriptionHtml: 'fortitudinis <i>fomentis</i> dolor mitigari solet.',
state: 'opened',
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index 4d08ad54e58..3b6f06d835b 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -2,13 +2,7 @@ import { GlBadge, GlButton, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import {
- STATUS_CLOSED,
- STATUS_OPEN,
- STATUS_REOPENED,
- TYPE_ISSUE,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { STATUS_CLOSED, STATUS_OPEN, STATUS_REOPENED, TYPE_ISSUE } from '~/issues/constants';
import { __ } from '~/locale';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -45,7 +39,6 @@ describe('IssuableHeader component', () => {
...mockIssuableShowProps,
issuableState: STATUS_OPEN,
issuableType: TYPE_ISSUE,
- workspaceType: WORKSPACE_PROJECT,
...props,
},
slots: {
@@ -107,6 +100,7 @@ describe('IssuableHeader component', () => {
expect(findConfidentialityBadge().props()).toEqual({
issuableType: 'issue',
workspaceType: 'project',
+ hideTextInSmallScreens: false,
});
});
@@ -169,7 +163,7 @@ describe('IssuableHeader component', () => {
expect(findWorkItemTypeIcon().props()).toMatchObject({
showText: true,
- workItemType: 'ISSUE',
+ workItemType: 'issue',
});
});
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
index 39316dfa249..eefc9142064 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js
@@ -66,11 +66,11 @@ describe('IssuableTitle', () => {
});
await nextTick();
- const titleEl = wrapperWithTitle.find('[data-testid="title"]');
+ const titleEl = wrapperWithTitle.find('[data-testid="issuable-title"]');
expect(titleEl.exists()).toBe(true);
expect(titleEl.html()).toBe(
- '<h1 dir="auto" data-qa-selector="title_content" data-testid="title" class="title gl-font-size-h-display"><b>Sample</b> title</h1>',
+ '<h1 dir="auto" data-testid="issuable-title" class="title gl-font-size-h-display"><b>Sample</b> title</h1>',
);
wrapperWithTitle.destroy();
diff --git a/spec/frontend/vue_shared/issuable/show/mock_data.js b/spec/frontend/vue_shared/issuable/show/mock_data.js
index 5ec205a2d5c..946ad33555d 100644
--- a/spec/frontend/vue_shared/issuable/show/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/show/mock_data.js
@@ -38,6 +38,7 @@ export const mockIssuableShowProps = {
showFieldTitle: false,
statusIcon: 'issues',
statusIconClass: 'gl-sm-display-none',
+ workspaceType: 'project',
taskCompletionStatus: {
completedCount: 0,
count: 5,
diff --git a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
index a7ddcbdd8bc..109b7732539 100644
--- a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
@@ -1,6 +1,6 @@
import { GlBreadcrumb } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import LegacyContainer from '~/vue_shared/new_namespace/components/legacy_container.vue';
import WelcomePage from '~/vue_shared/new_namespace/components/welcome.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
@@ -14,6 +14,7 @@ describe('Experimental new namespace creation app', () => {
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
+ const findTopBar = () => wrapper.findByTestId('top-bar');
const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
const findImage = () => wrapper.find('img');
const findNewTopLevelGroupAlert = () => wrapper.findComponent(NewTopLevelGroupAlert);
@@ -30,7 +31,7 @@ describe('Experimental new namespace creation app', () => {
};
const createComponent = ({ slots, propsData } = {}) => {
- wrapper = shallowMount(NewNamespacePage, {
+ wrapper = shallowMountExtended(NewNamespacePage, {
slots,
propsData: {
...DEFAULT_PROPS,
@@ -167,4 +168,19 @@ describe('Experimental new namespace creation app', () => {
});
});
});
+
+ describe('top bar', () => {
+ it('adds "top-bar-fixed" and "container-fluid" classes when new navigation enabled', () => {
+ gon.use_new_navigation = true;
+ createComponent();
+
+ expect(findTopBar().classes()).toEqual(['top-bar-fixed', 'container-fluid']);
+ });
+
+ it('does not add classes when new navigation is not enabled', () => {
+ createComponent();
+
+ expect(findTopBar().classes()).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
index 299a3d62421..f5bc23a91fd 100644
--- a/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
@@ -12,8 +12,7 @@ describe('SecurityReportDownloadDropdown component', () => {
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
describe('given report artifacts', () => {
beforeEach(() => {
@@ -28,21 +27,36 @@ describe('SecurityReportDownloadDropdown component', () => {
},
];
- createComponent({ artifacts });
+ createComponent({ artifacts, text: 'test' });
});
it('renders a dropdown', () => {
expect(findDropdown().props('loading')).toBe(false);
+ expect(findDropdown().props('toggleText')).toBe('test');
+ expect(findDropdown().attributes()).toMatchObject({
+ placement: 'right',
+ size: 'small',
+ icon: 'download',
+ });
});
- it('renders a dropdown item for each artifact', () => {
- artifacts.forEach((artifact, i) => {
- const item = findDropdownItems().at(i);
- expect(item.text()).toContain(artifact.name);
-
- expect(item.element.getAttribute('href')).toBe(artifact.path);
- expect(item.element.getAttribute('download')).toBeDefined();
- });
+ it('passes artifacts as items', () => {
+ expect(findDropdown().props('items')).toMatchObject([
+ {
+ text: 'Download foo',
+ href: '/foo.json',
+ extraAttrs: {
+ download: '',
+ },
+ },
+ {
+ text: 'Download bar',
+ href: '/bar.json',
+ extraAttrs: {
+ download: '',
+ },
+ },
+ ]);
});
});
@@ -56,31 +70,13 @@ describe('SecurityReportDownloadDropdown component', () => {
});
});
- describe('given title props', () => {
+ describe('given it is not loading and no artifacts', () => {
beforeEach(() => {
- createComponent({ artifacts: [], loading: true, title: 'test title' });
- });
-
- it('should render title', () => {
- expect(findDropdown().attributes('title')).toBe('test title');
- });
-
- it('should not render text', () => {
- expect(findDropdown().text().trim()).toBe('');
- });
- });
-
- describe('given text props', () => {
- beforeEach(() => {
- createComponent({ artifacts: [], loading: true, text: 'test text' });
- });
-
- it('should not render title', () => {
- expect(findDropdown().props().title).not.toBeDefined();
+ createComponent({ artifacts: [], loading: false });
});
- it('should render text', () => {
- expect(findDropdown().props().text).toContain('test text');
+ it('does not render dropdown', () => {
+ expect(findDropdown().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
index aec0f84cb82..4150bd75c16 100644
--- a/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
+++ b/spec/frontend/webhooks/components/__snapshots__/push_events_spec.js.snap
@@ -11,7 +11,7 @@ exports[`Webhook push events form editor component Different push events rules w
valuefield="value"
>
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_all_branches"
value="all_branches"
>
@@ -21,50 +21,34 @@ exports[`Webhook push events form editor component Different push events rules w
All branches
</div>
</gl-form-radio-stub>
-
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_wildcard"
value="wildcard"
>
<div
data-qa-selector="strategy_radio_wildcard"
>
-
- Wildcard pattern
-
+ Wildcard pattern
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
- >
- <!---->
- </div>
-
- <!---->
-
+ />
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_regex"
value="regex"
>
<div
data-qa-selector="strategy_radio_regex"
>
-
- Regular expression
-
+ Regular expression
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
- >
- <!---->
- </div>
-
- <!---->
+ />
</gl-form-radio-group-stub>
`;
@@ -79,7 +63,7 @@ exports[`Webhook push events form editor component Different push events rules w
valuefield="value"
>
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_all_branches"
value="all_branches"
>
@@ -89,43 +73,31 @@ exports[`Webhook push events form editor component Different push events rules w
All branches
</div>
</gl-form-radio-stub>
-
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_wildcard"
value="wildcard"
>
<div
data-qa-selector="strategy_radio_wildcard"
>
-
- Wildcard pattern
-
+ Wildcard pattern
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
- >
- <!---->
- </div>
-
- <!---->
-
+ />
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_regex"
value="regex"
>
<div
data-qa-selector="strategy_radio_regex"
>
-
- Regular expression
-
+ Regular expression
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
>
@@ -136,9 +108,8 @@ exports[`Webhook push events form editor component Different push events rules w
value="foo"
/>
</div>
-
<p
- class="form-text text-muted custom-control"
+ class="custom-control form-text text-muted"
>
<gl-sprintf-stub
message="Regular expressions such as %{REGEX_CODE} are supported."
@@ -158,7 +129,7 @@ exports[`Webhook push events form editor component Different push events rules w
valuefield="value"
>
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_all_branches"
value="all_branches"
>
@@ -168,21 +139,17 @@ exports[`Webhook push events form editor component Different push events rules w
All branches
</div>
</gl-form-radio-stub>
-
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_wildcard"
value="wildcard"
>
<div
data-qa-selector="strategy_radio_wildcard"
>
-
- Wildcard pattern
-
+ Wildcard pattern
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
>
@@ -193,36 +160,27 @@ exports[`Webhook push events form editor component Different push events rules w
value="foo"
/>
</div>
-
<p
- class="form-text text-muted custom-control"
+ class="custom-control form-text text-muted"
>
<gl-sprintf-stub
message="Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported."
/>
</p>
-
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_regex"
value="regex"
>
<div
data-qa-selector="strategy_radio_regex"
>
-
- Regular expression
-
+ Regular expression
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
- >
- <!---->
- </div>
-
- <!---->
+ />
</gl-form-radio-group-stub>
`;
@@ -237,7 +195,7 @@ exports[`Webhook push events form editor component Different push events rules w
valuefield="value"
>
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_all_branches"
value="all_branches"
>
@@ -247,50 +205,34 @@ exports[`Webhook push events form editor component Different push events rules w
All branches
</div>
</gl-form-radio-stub>
-
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_wildcard"
value="wildcard"
>
<div
data-qa-selector="strategy_radio_wildcard"
>
-
- Wildcard pattern
-
+ Wildcard pattern
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
- >
- <!---->
- </div>
-
- <!---->
-
+ />
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_regex"
value="regex"
>
<div
data-qa-selector="strategy_radio_regex"
>
-
- Regular expression
-
+ Regular expression
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
- >
- <!---->
- </div>
-
- <!---->
+ />
</gl-form-radio-group-stub>
`;
@@ -305,7 +247,7 @@ exports[`Webhook push events form editor component Different push events rules w
valuefield="value"
>
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_all_branches"
value="all_branches"
>
@@ -315,43 +257,31 @@ exports[`Webhook push events form editor component Different push events rules w
All branches
</div>
</gl-form-radio-stub>
-
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_wildcard"
value="wildcard"
>
<div
data-qa-selector="strategy_radio_wildcard"
>
-
- Wildcard pattern
-
+ Wildcard pattern
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
- >
- <!---->
- </div>
-
- <!---->
-
+ />
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_regex"
value="regex"
>
<div
data-qa-selector="strategy_radio_regex"
>
-
- Regular expression
-
+ Regular expression
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
>
@@ -362,9 +292,8 @@ exports[`Webhook push events form editor component Different push events rules w
value=""
/>
</div>
-
<p
- class="form-text text-muted custom-control"
+ class="custom-control form-text text-muted"
>
<gl-sprintf-stub
message="Regular expressions such as %{REGEX_CODE} are supported."
@@ -384,7 +313,7 @@ exports[`Webhook push events form editor component Different push events rules w
valuefield="value"
>
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_all_branches"
value="all_branches"
>
@@ -394,21 +323,17 @@ exports[`Webhook push events form editor component Different push events rules w
All branches
</div>
</gl-form-radio-stub>
-
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_wildcard"
value="wildcard"
>
<div
data-qa-selector="strategy_radio_wildcard"
>
-
- Wildcard pattern
-
+ Wildcard pattern
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
>
@@ -419,35 +344,26 @@ exports[`Webhook push events form editor component Different push events rules w
value=""
/>
</div>
-
<p
- class="form-text text-muted custom-control"
+ class="custom-control form-text text-muted"
>
<gl-sprintf-stub
message="Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported."
/>
</p>
-
<gl-form-radio-stub
- class="gl-mt-2 branch-filter-strategy-radio"
+ class="branch-filter-strategy-radio gl-mt-2"
data-testid="rule_regex"
value="regex"
>
<div
data-qa-selector="strategy_radio_regex"
>
-
- Regular expression
-
+ Regular expression
</div>
</gl-form-radio-stub>
-
<div
class="gl-ml-6"
- >
- <!---->
- </div>
-
- <!---->
+ />
</gl-form-radio-group-stub>
`;
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap
index 52838dcd0bc..841b8b57f88 100644
--- a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap
+++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap
@@ -1,9 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Work Item Note Body should have the wrapper to show the note body 1`] = `
-"<div data-testid=\\"work-item-note-body\\" class=\\"note-text md\\">
- <p dir=\\"auto\\" data-sourcepos=\\"1:1-1:76\\">
- <gl-emoji data-unicode-version=\\"6.0\\" data-name=\\"wave\\" title=\\"waving hand sign\\">👋</gl-emoji> Hi <a title=\\"Sherie Nitzsche\\" class=\\"gfm gfm-project_member js-user-link\\" data-placement=\\"top\\" data-container=\\"body\\" data-user=\\"3\\" data-reference-type=\\"user\\" href=\\"/fredda.brekke\\">@fredda.brekke</a> How are you ? what do you think about this ? <gl-emoji data-unicode-version=\\"6.0\\" data-name=\\"pray\\" title=\\"person with folded hands\\">🙏</gl-emoji>
+<div
+ class="md note-text"
+ data-testid="work-item-note-body"
+>
+ <p
+ data-sourcepos="1:1-1:76"
+ dir="auto"
+ >
+ <gl-emoji
+ data-name="wave"
+ data-unicode-version="6.0"
+ title="waving hand sign"
+ >
+ 👋
+ </gl-emoji>
+ Hi
+ <a
+ class="gfm gfm-project_member js-user-link"
+ data-container="body"
+ data-placement="top"
+ data-reference-type="user"
+ data-user="3"
+ href="/fredda.brekke"
+ title="Sherie Nitzsche"
+ >
+ @fredda.brekke
+ </a>
+ How are you ? what do you think about this ?
+ <gl-emoji
+ data-name="pray"
+ data-unicode-version="6.0"
+ title="person with folded hands"
+ >
+ 🙏
+ </gl-emoji>
</p>
-</div>"
+</div>
`;
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
index 30577dc60cf..af930f56509 100644
--- a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
+++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
@@ -1,3 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Work Item Note Replying should have the note body and header 1`] = `"<note-header-stub author=\\"[object Object]\\" actiontext=\\"\\" noteabletype=\\"\\" expanded=\\"true\\" showspinner=\\"true\\" noteurl=\\"\\" emailparticipant=\\"\\"></note-header-stub>"`;
+exports[`Work Item Note Replying should have the note body and header 1`] = `
+<note-header-stub
+ actiontext=""
+ author="[object Object]"
+ emailparticipant=""
+ expanded="true"
+ noteabletype=""
+ noteurl=""
+ showspinner="true"
+/>
+`;
diff --git a/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js
index 5ed9d581446..0d0235f4b20 100644
--- a/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemActivitySortFilter from '~/work_items/components/notes/work_item_activity_sort_filter.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@@ -19,15 +19,13 @@ describe('Work Item Activity/Discussions Filtering', () => {
let wrapper;
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findByDataTestId = (dataTestId) => wrapper.findByTestId(dataTestId);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const createComponent = ({
loading = false,
workItemType = 'Task',
sortFilterProp = ASC,
- filterOptions = WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ items = WORK_ITEM_ACTIVITY_SORT_OPTIONS,
trackingLabel = 'item_track_notes_sorting',
trackingAction = 'work_item_notes_sort_order_changed',
filterEvent = 'changeSort',
@@ -39,7 +37,7 @@ describe('Work Item Activity/Discussions Filtering', () => {
loading,
workItemType,
sortFilterProp,
- filterOptions,
+ items,
trackingLabel,
trackingAction,
filterEvent,
@@ -50,13 +48,13 @@ describe('Work Item Activity/Discussions Filtering', () => {
};
describe.each`
- usedFor | filterOptions | storageKey | filterEvent | newInputOption | trackingLabel | trackingAction | defaultSortFilterProp | sortFilterProp | nonDefaultDataTestId
- ${'Sorting'} | ${WORK_ITEM_ACTIVITY_SORT_OPTIONS} | ${WORK_ITEM_NOTES_SORT_ORDER_KEY} | ${'changeSort'} | ${DESC} | ${'item_track_notes_sorting'} | ${'work_item_notes_sort_order_changed'} | ${ASC} | ${ASC} | ${'newest-first'}
- ${'Filtering'} | ${WORK_ITEM_ACTIVITY_FILTER_OPTIONS} | ${WORK_ITEM_NOTES_FILTER_KEY} | ${'changeFilter'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${'item_track_notes_sorting'} | ${'work_item_notes_filter_changed'} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${'comments-activity'}
+ usedFor | items | storageKey | filterEvent | newInputOption | trackingLabel | trackingAction | defaultSortFilterProp | sortFilterProp | nonDefaultValue
+ ${'Sorting'} | ${WORK_ITEM_ACTIVITY_SORT_OPTIONS} | ${WORK_ITEM_NOTES_SORT_ORDER_KEY} | ${'changeSort'} | ${DESC} | ${'item_track_notes_sorting'} | ${'work_item_notes_sort_order_changed'} | ${ASC} | ${ASC} | ${DESC}
+ ${'Filtering'} | ${WORK_ITEM_ACTIVITY_FILTER_OPTIONS} | ${WORK_ITEM_NOTES_FILTER_KEY} | ${'changeFilter'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${'item_track_notes_sorting'} | ${'work_item_notes_filter_changed'} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS}
`(
'When used for $usedFor',
({
- filterOptions,
+ items,
storageKey,
filterEvent,
trackingLabel,
@@ -64,12 +62,12 @@ describe('Work Item Activity/Discussions Filtering', () => {
newInputOption,
defaultSortFilterProp,
sortFilterProp,
- nonDefaultDataTestId,
+ nonDefaultValue,
}) => {
beforeEach(() => {
createComponent({
sortFilterProp,
- filterOptions,
+ items,
trackingLabel,
trackingAction,
filterEvent,
@@ -79,8 +77,7 @@ describe('Work Item Activity/Discussions Filtering', () => {
});
it('has a dropdown with options equal to the length of `filterOptions`', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findAllDropdownItems()).toHaveLength(filterOptions.length);
+ expect(findListbox().props('items')).toEqual(items);
});
it('has local storage sync with the correct props', () => {
@@ -96,7 +93,7 @@ describe('Work Item Activity/Discussions Filtering', () => {
it('emits tracking event when the a non default dropdown item is clicked', () => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- findByDataTestId(nonDefaultDataTestId).vm.$emit('click');
+ findListbox().vm.$emit('select', nonDefaultValue);
expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, trackingAction, {
category: TRACKING_CATEGORY_SHOW,
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 4b1b7b27ad9..826fc2b2230 100644
--- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -255,6 +255,20 @@ describe('Work item add note', () => {
expect(wrapper.emitted('error')).toEqual([[error]]);
});
+
+ it('sends confidential prop to work item comment form', async () => {
+ await createComponent({ isEditing: true, signedIn: true });
+
+ const {
+ data: {
+ workspace: {
+ workItems: { nodes },
+ },
+ },
+ } = workItemByIidResponseFactory({ canUpdate: true, canCreateNote: true });
+
+ expect(findCommentForm().props('isWorkItemConfidential')).toBe(nodes[0].confidential);
+ });
});
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index c5d1decfb42..9049a69656a 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -388,6 +388,13 @@ describe('Work Item Note', () => {
});
});
+ it('confidential information on note', async () => {
+ createComponent();
+ await findNoteActions().vm.$emit('startEditing');
+ const { confidential } = workItemByIidResponseFactory().data.workspace.workItems.nodes[0];
+ expect(findCommentForm().props('isWorkItemConfidential')).toBe(confidential);
+ });
+
describe('author and user role badges', () => {
describe('author badge props', () => {
it.each`
diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
index 9a20e2ec98f..b86f9ff34ae 100644
--- a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
@@ -10,7 +10,7 @@ import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip
import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue';
import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
-import { TASK_TYPE_NAME, WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '~/work_items/constants';
+import { WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '~/work_items/constants';
import {
workItemTask,
@@ -26,11 +26,9 @@ jest.mock('~/alert');
describe('WorkItemLinkChildContents', () => {
Vue.use(VueApollo);
- const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
let wrapper;
const { LABELS } = workItemObjectiveMetadataWidgets;
const mockLabels = LABELS.labels.nodes;
- const mockFullPath = 'gitlab-org/gitlab-test';
const findStatusIconComponent = () =>
wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
@@ -43,19 +41,11 @@ describe('WorkItemLinkChildContents', () => {
const findScopedLabel = () => findAllLabels().at(1);
const findLinksMenuComponent = () => wrapper.findComponent(WorkItemLinksMenu);
- const createComponent = ({
- canUpdate = true,
- parentWorkItemId = WORK_ITEM_ID,
- childItem = workItemTask,
- workItemType = TASK_TYPE_NAME,
- } = {}) => {
+ const createComponent = ({ canUpdate = true, childItem = workItemTask } = {}) => {
wrapper = shallowMountExtended(WorkItemLinkChildContents, {
propsData: {
canUpdate,
- parentWorkItemId,
childItem,
- workItemType,
- fullPath: mockFullPath,
childPath: '/gitlab-org/gitlab-test/-/work_items/4',
},
});
diff --git a/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js b/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
index 721db6c3315..338a70feae4 100644
--- a/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
@@ -10,8 +10,8 @@ describe('WorkItemLinksMenu', () => {
wrapper = shallowMountExtended(WorkItemLinksMenu);
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findRemoveDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findRemoveDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
beforeEach(() => {
createComponent();
@@ -23,7 +23,7 @@ describe('WorkItemLinksMenu', () => {
});
it('emits removeChild event on click Remove', () => {
- findRemoveDropdownItem().vm.$emit('click');
+ findRemoveDropdownItem().vm.$emit('action');
expect(wrapper.emitted('removeChild')).toHaveLength(1);
});
diff --git a/spec/frontend/work_items/components/shared/work_item_token_input_spec.js b/spec/frontend/work_items/components/shared/work_item_token_input_spec.js
new file mode 100644
index 00000000000..075b69415cf
--- /dev/null
+++ b/spec/frontend/work_items/components/shared/work_item_token_input_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+import { GlTokenSelector } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_input.vue';
+import { WORK_ITEM_TYPE_ENUM_TASK } from '~/work_items/constants';
+import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
+import { availableWorkItemsResponse, searchedWorkItemsResponse } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+describe('WorkItemTokenInput', () => {
+ let wrapper;
+
+ const availableWorkItemsResolver = jest.fn().mockResolvedValue(availableWorkItemsResponse);
+ const searchedWorkItemResolver = jest.fn().mockResolvedValue(searchedWorkItemsResponse);
+
+ const createComponent = async ({
+ workItemsToAdd = [],
+ parentConfidential = false,
+ childrenType = WORK_ITEM_TYPE_ENUM_TASK,
+ areWorkItemsToAddValid = true,
+ workItemsResolver = searchedWorkItemResolver,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemTokenInput, {
+ apolloProvider: createMockApollo([[projectWorkItemsQuery, workItemsResolver]]),
+ propsData: {
+ value: workItemsToAdd,
+ childrenType,
+ childrenIds: [],
+ fullPath: 'test-project-path',
+ parentWorkItemId: 'gid://gitlab/WorkItem/1',
+ parentConfidential,
+ areWorkItemsToAddValid,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+
+ it('searches for available work items on focus', async () => {
+ createComponent({ workItemsResolver: availableWorkItemsResolver });
+ findTokenSelector().vm.$emit('focus');
+ await waitForPromises();
+
+ expect(availableWorkItemsResolver).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ searchTerm: '',
+ types: [WORK_ITEM_TYPE_ENUM_TASK],
+ in: undefined,
+ });
+ expect(findTokenSelector().props('dropdownItems')).toHaveLength(3);
+ });
+
+ it('searches for available work items when typing in input', async () => {
+ createComponent({ workItemsResolver: searchedWorkItemResolver });
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'Task 2');
+ await waitForPromises();
+
+ expect(searchedWorkItemResolver).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ searchTerm: 'Task 2',
+ types: [WORK_ITEM_TYPE_ENUM_TASK],
+ in: 'TITLE',
+ });
+ expect(findTokenSelector().props('dropdownItems')).toHaveLength(1);
+ });
+
+ it('renders red border around token selector input when work item is not valid', () => {
+ createComponent({
+ areWorkItemsToAddValid: false,
+ });
+
+ expect(findTokenSelector().props('containerClass')).toBe('gl-inset-border-1-red-500!');
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index 0fe517d7d74..0098a2e0864 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -140,7 +140,12 @@ describe('WorkItemActions component', () => {
stubs: {
GlModal: stubComponent(GlModal, {
methods: {
- show: modalShowSpy,
+ show: jest.fn(),
+ },
+ }),
+ GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
+ methods: {
+ close: modalShowSpy,
},
}),
},
@@ -208,7 +213,7 @@ describe('WorkItemActions component', () => {
it('emits `toggleWorkItemConfidentiality` event when clicked', () => {
createComponent();
- findConfidentialityToggleButton().vm.$emit('click');
+ findConfidentialityToggleButton().vm.$emit('action');
expect(wrapper.emitted('toggleWorkItemConfidentiality')[0]).toEqual([true]);
});
@@ -228,7 +233,7 @@ describe('WorkItemActions component', () => {
it('shows confirm modal when clicked', () => {
createComponent();
- findDeleteButton().vm.$emit('click');
+ findDeleteButton().vm.$emit('action');
expect(modalShowSpy).toHaveBeenCalled();
});
@@ -359,7 +364,7 @@ describe('WorkItemActions component', () => {
await waitForPromises();
expect(findPromoteButton().exists()).toBe(true);
- findPromoteButton().vm.$emit('click');
+ findPromoteButton().vm.$emit('action');
await waitForPromises();
@@ -378,7 +383,7 @@ describe('WorkItemActions component', () => {
await waitForPromises();
expect(findPromoteButton().exists()).toBe(true);
- findPromoteButton().vm.$emit('click');
+ findPromoteButton().vm.$emit('action');
await waitForPromises();
@@ -394,7 +399,7 @@ describe('WorkItemActions component', () => {
createComponent();
expect(findCopyReferenceButton().exists()).toBe(true);
- findCopyReferenceButton().vm.$emit('click');
+ findCopyReferenceButton().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Reference copied');
});
@@ -416,7 +421,7 @@ describe('WorkItemActions component', () => {
createComponent();
expect(findCopyCreateNoteEmailButton().exists()).toBe(true);
- findCopyCreateNoteEmailButton().vm.$emit('click');
+ findCopyCreateNoteEmailButton().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Email address copied');
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index d3c7c9e2074..fec6d0673c6 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -20,6 +20,7 @@ import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_up
import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
+import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -37,6 +38,7 @@ import {
workItemByIidResponseFactory,
objectiveType,
mockWorkItemCommentNote,
+ mockBlockingLinkedItem,
} from '../mock_data';
jest.mock('~/lib/utils/common_utils');
@@ -76,6 +78,7 @@ describe('WorkItemDetail component', () => {
const findCloseButton = () => wrapper.findByTestId('work-item-close');
const findWorkItemType = () => wrapper.findByTestId('work-item-type');
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
+ const findWorkItemRelationships = () => wrapper.findComponent(WorkItemRelationships);
const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
@@ -96,6 +99,7 @@ describe('WorkItemDetail component', () => {
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
workItemsMvc2Enabled = false,
+ linkedWorkItemsEnabled = false,
} = {}) => {
const handlers = [
[workItemByIidQuery, handler],
@@ -119,6 +123,7 @@ describe('WorkItemDetail component', () => {
provide: {
glFeatures: {
workItemsMvc2: workItemsMvc2Enabled,
+ linkedWorkItems: linkedWorkItemsEnabled,
},
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
@@ -581,12 +586,91 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('relationship widget', () => {
+ it('does not render linked items by default', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findWorkItemRelationships().exists()).toBe(false);
+ });
+
+ describe('work item has children', () => {
+ const mockWorkItemLinkedItem = workItemByIidResponseFactory({
+ linkedItems: mockBlockingLinkedItem,
+ });
+ const handler = jest.fn().mockResolvedValue(mockWorkItemLinkedItem);
+
+ it('renders relationship widget when work item has linked items', async () => {
+ createComponent({ handler, linkedWorkItemsEnabled: true });
+ await waitForPromises();
+
+ expect(findWorkItemRelationships().exists()).toBe(true);
+ });
+
+ it('opens the modal with the linked item when `showModal` is emitted', async () => {
+ createComponent({
+ handler,
+ linkedWorkItemsEnabled: true,
+ workItemsMvc2Enabled: true,
+ });
+ await waitForPromises();
+
+ const event = {
+ preventDefault: jest.fn(),
+ };
+
+ findWorkItemRelationships().vm.$emit('showModal', {
+ event,
+ modalWorkItem: { id: 'childWorkItemId' },
+ });
+ await waitForPromises();
+
+ expect(findModal().props().workItemId).toBe('childWorkItemId');
+ expect(showModalHandler).toHaveBeenCalled();
+ });
+
+ describe('linked work item is rendered in a modal and has linked items', () => {
+ beforeEach(async () => {
+ createComponent({
+ isModal: true,
+ handler,
+ workItemsMvc2Enabled: true,
+ linkedWorkItemsEnabled: true,
+ });
+
+ await waitForPromises();
+ });
+
+ it('does not render a new modal', () => {
+ expect(findModal().exists()).toBe(false);
+ });
+
+ it('emits `update-modal` when `show-modal` is emitted', async () => {
+ const event = {
+ preventDefault: jest.fn(),
+ };
+
+ findWorkItemRelationships().vm.$emit('showModal', {
+ event,
+ modalWorkItem: { id: 'childWorkItemId' },
+ });
+ await waitForPromises();
+
+ expect(wrapper.emitted('update-modal')).toBeDefined();
+ });
+ });
+ });
+ });
+
describe('notes widget', () => {
it('renders notes by default', async () => {
createComponent();
await waitForPromises();
+ const { confidential } = workItemQueryResponse.data.workspace.workItems.nodes[0];
+
expect(findNotesWidget().exists()).toBe(true);
+ expect(findNotesWidget().props('isWorkItemConfidential')).toBe(confidential);
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index 803ff950cbe..a624bbe8567 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -93,8 +93,6 @@ describe('WorkItemLinkChild', () => {
expect(findWorkItemLinkChildContents().props()).toEqual({
childItem: workItemObjectiveWithChild,
canUpdate: true,
- parentWorkItemId: 'gid://gitlab/WorkItem/2',
- workItemType: 'Objective',
childPath: '/gitlab-org/gitlab-test/-/work_items/12',
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index 8caacc2dc97..aaab22fd18d 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -1,11 +1,12 @@
import Vue from 'vue';
-import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip, GlTokenSelector } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { sprintf, s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
+import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_input.vue';
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_TASK,
@@ -70,10 +71,12 @@ describe('WorkItemLinksForm', () => {
};
const findForm = () => wrapper.findComponent(GlForm);
- const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
+ const findWorkItemTokenInput = () => wrapper.findComponent(WorkItemTokenInput);
const findInput = () => wrapper.findComponent(GlFormInput);
const findConfidentialCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
+ const findValidationElement = () => wrapper.findByTestId('work-items-invalid');
describe('creating a new work item', () => {
beforeEach(async () => {
@@ -84,7 +87,7 @@ describe('WorkItemLinksForm', () => {
expect(findForm().exists()).toBe(true);
expect(findInput().exists()).toBe(true);
expect(findAddChildButton().text()).toBe('Create task');
- expect(findTokenSelector().exists()).toBe(false);
+ expect(findWorkItemTokenInput().exists()).toBe(false);
});
it('creates child task in non confidential parent', async () => {
@@ -137,7 +140,7 @@ describe('WorkItemLinksForm', () => {
const confidentialCheckbox = findConfidentialCheckbox();
expect(confidentialCheckbox.exists()).toBe(true);
- expect(wrapper.findComponent(GlTooltip).exists()).toBe(false);
+ expect(findTooltip().exists()).toBe(false);
expect(confidentialCheckbox.text()).toBe(
sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL, {
workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(),
@@ -149,12 +152,11 @@ describe('WorkItemLinksForm', () => {
createComponent({ parentConfidential: true });
const confidentialCheckbox = findConfidentialCheckbox();
- const confidentialTooltip = wrapper.findComponent(GlTooltip);
expect(confidentialCheckbox.attributes('disabled')).toBeDefined();
expect(confidentialCheckbox.attributes('checked')).toBe('true');
- expect(confidentialTooltip.exists()).toBe(true);
- expect(confidentialTooltip.text()).toBe(
+ expect(findTooltip().exists()).toBe(true);
+ expect(findTooltip().text()).toBe(
sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP, {
workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(),
parentWorkItemType: WORK_ITEM_TYPE_VALUE_ISSUE.toLocaleLowerCase(),
@@ -165,14 +167,11 @@ describe('WorkItemLinksForm', () => {
});
describe('adding an existing work item', () => {
- const selectAvailableWorkItemTokens = async () => {
- findTokenSelector().vm.$emit(
+ const selectAvailableWorkItemTokens = () => {
+ findWorkItemTokenInput().vm.$emit(
'input',
availableWorkItemsResponse.data.workspace.workItems.nodes,
);
- findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
-
- await waitForPromises();
};
beforeEach(async () => {
@@ -181,24 +180,31 @@ describe('WorkItemLinksForm', () => {
it('renders add form', () => {
expect(findForm().exists()).toBe(true);
- expect(findTokenSelector().exists()).toBe(true);
+ expect(findWorkItemTokenInput().exists()).toBe(true);
expect(findAddChildButton().text()).toBe('Add task');
expect(findInput().exists()).toBe(false);
expect(findConfidentialCheckbox().exists()).toBe(false);
});
- it('searches for available work items as prop when typing in input', async () => {
- findTokenSelector().vm.$emit('focus');
- findTokenSelector().vm.$emit('text-input', 'Task');
- await waitForPromises();
-
- expect(availableWorkItemsResolver).toHaveBeenCalled();
+ it('renders work item token input with default props', () => {
+ expect(findWorkItemTokenInput().props()).toMatchObject({
+ value: [],
+ fullPath: 'project/path',
+ childrenType: WORK_ITEM_TYPE_ENUM_TASK,
+ childrenIds: [],
+ parentWorkItemId: 'gid://gitlab/WorkItem/1',
+ areWorkItemsToAddValid: true,
+ });
});
it('selects and adds children', async () => {
await selectAvailableWorkItemTokens();
expect(findAddChildButton().text()).toBe('Add tasks');
+ expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(true);
+ expect(findWorkItemTokenInput().props('value')).toBe(
+ availableWorkItemsResponse.data.workspace.workItems.nodes,
+ );
findForm().vm.$emit('submit', {
preventDefault: jest.fn(),
});
@@ -211,9 +217,9 @@ describe('WorkItemLinksForm', () => {
await selectAvailableWorkItemTokens();
- const validationEl = wrapper.findByTestId('work-items-invalid');
- expect(validationEl.exists()).toBe(true);
- expect(validationEl.text().trim()).toBe(
+ expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(false);
+ expect(findValidationElement().exists()).toBe(true);
+ expect(findValidationElement().text().trim()).toBe(
sprintf(
s__(
'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.',
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index c2821cc99f9..35f01c85ec8 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -88,6 +88,7 @@ describe('WorkItemNotes component', () => {
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
isModal = false,
+ isWorkItemConfidential = false,
} = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
@@ -106,6 +107,7 @@ describe('WorkItemNotes component', () => {
workItemType: 'task',
reportAbusePath: '/report/abuse/path',
isModal,
+ isWorkItemConfidential,
},
stubs: {
GlModal: stubComponent(GlModal, { methods: { show: showModal } }),
@@ -344,4 +346,14 @@ describe('WorkItemNotes component', () => {
});
});
});
+
+ it('passes confidential props when the work item is confidential', async () => {
+ createComponent({
+ isWorkItemConfidential: true,
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+
+ expect(findWorkItemCommentNoteAtIndex(0).props('isWorkItemConfidential')).toBe(true);
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
new file mode 100644
index 00000000000..9105e4de5e0
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WorkItemRelationshipList renders linked item list 1`] = `
+<div>
+ <h4
+ class="gl-font-sm gl-font-weight-semibold gl-mb-2 gl-mt-3 gl-mx-2 gl-text-gray-700"
+ data-testid="work-items-list-heading"
+ >
+ Blocking
+ </h4>
+ <div
+ class="work-items-list-body"
+ >
+ <ul
+ class="content-list work-items-list"
+ >
+ <li
+ class="gl-border-b-0! gl-pb-0! gl-pt-0!"
+ >
+ <work-item-link-child-contents-stub
+ canupdate="true"
+ childitem="[object Object]"
+ childpath="/test-project-path/-/work_items/83"
+ />
+ </li>
+ </ul>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js
new file mode 100644
index 00000000000..759ab7e14da
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js
@@ -0,0 +1,41 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue';
+import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue';
+
+import { mockBlockingLinkedItem } from '../../mock_data';
+
+describe('WorkItemRelationshipList', () => {
+ let wrapper;
+ const mockLinkedItems = mockBlockingLinkedItem.linkedItems.nodes;
+
+ const createComponent = ({ linkedItems = [], heading = 'Blocking', canUpdate = true } = {}) => {
+ wrapper = shallowMountExtended(WorkItemRelationshipList, {
+ propsData: {
+ linkedItems,
+ heading,
+ canUpdate,
+ workItemFullPath: 'test-project-path',
+ },
+ });
+ };
+
+ const findHeading = () => wrapper.findByTestId('work-items-list-heading');
+ const findWorkItemLinkChildContents = () => wrapper.findComponent(WorkItemLinkChildContents);
+
+ beforeEach(() => {
+ createComponent({ linkedItems: mockLinkedItems });
+ });
+
+ it('renders linked item list', () => {
+ expect(findHeading().text()).toBe('Blocking');
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+
+ it('renders work item link child contents with correct props', () => {
+ expect(findWorkItemLinkChildContents().props()).toMatchObject({
+ childItem: mockLinkedItems[0].workItem,
+ canUpdate: true,
+ childPath: '/test-project-path/-/work_items/83',
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
new file mode 100644
index 00000000000..c9a2499b127
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
+import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
+import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+
+import {
+ workItemByIidResponseFactory,
+ mockLinkedItems,
+ mockBlockingLinkedItem,
+} from '../../mock_data';
+
+describe('WorkItemRelationships', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ const emptyLinkedWorkItemsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory());
+ const linkedWorkItemsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems }));
+ const blockingLinkedWorkItemQueryHandler = jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockBlockingLinkedItem }));
+
+ const createComponent = async ({
+ workItemQueryHandler = emptyLinkedWorkItemsQueryHandler,
+ } = {}) => {
+ const mockApollo = createMockApollo([[workItemByIidQuery, workItemQueryHandler]]);
+
+ wrapper = shallowMountExtended(WorkItemRelationships, {
+ apolloProvider: mockApollo,
+ propsData: {
+ workItemIid: '1',
+ workItemFullPath: 'test-project-path',
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
+ const findEmptyRelatedMessageContainer = () => wrapper.findByTestId('links-empty');
+ const findLinkedItemsCountContainer = () => wrapper.findByTestId('linked-items-count');
+ const findAllWorkItemRelationshipListComponents = () =>
+ wrapper.findAllComponents(WorkItemRelationshipList);
+
+ it('shows loading icon when query is not processed', () => {
+ createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('renders the component with empty message when there are no items', async () => {
+ await createComponent();
+
+ expect(wrapper.find('.work-item-relationships').exists()).toBe(true);
+ expect(findEmptyRelatedMessageContainer().exists()).toBe(true);
+ });
+
+ it('renders blocking linked item lists', async () => {
+ await createComponent({ workItemQueryHandler: blockingLinkedWorkItemQueryHandler });
+
+ expect(findAllWorkItemRelationshipListComponents().length).toBe(1);
+ expect(findLinkedItemsCountContainer().text()).toBe('1');
+ });
+
+ it('renders blocking, blocked by and related to linked item lists with proper count', async () => {
+ await createComponent({ workItemQueryHandler: linkedWorkItemsQueryHandler });
+
+ // renders all 3 lists: blocking, blocked by and related to
+ expect(findAllWorkItemRelationshipListComponents().length).toBe(3);
+ expect(findLinkedItemsCountContainer().text()).toBe('3');
+ });
+
+ it('shows an alert when list loading fails', async () => {
+ const errorMessage = 'Some error';
+ await createComponent({
+ workItemQueryHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
+ });
+
+ expect(findWidgetWrapper().props('error')).toBe(errorMessage);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_state_badge_spec.js b/spec/frontend/work_items/components/work_item_state_badge_spec.js
index 888d712cc5a..248f16a4081 100644
--- a/spec/frontend/work_items/components/work_item_state_badge_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_badge_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
import WorkItemStateBadge from '~/work_items/components/work_item_state_badge.vue';
@@ -14,6 +14,7 @@ describe('WorkItemStateBadge', () => {
});
};
const findStatusBadge = () => wrapper.findComponent(GlBadge);
+ const findStatusBadgeIcon = () => wrapper.findComponent(GlIcon);
it.each`
state | icon | stateText | variant
@@ -24,7 +25,7 @@ describe('WorkItemStateBadge', () => {
({ state, icon, stateText, variant }) => {
createComponent({ workItemState: state });
- expect(findStatusBadge().props('icon')).toBe(icon);
+ expect(findStatusBadgeIcon().props('name')).toBe(icon);
expect(findStatusBadge().props('variant')).toBe(variant);
expect(findStatusBadge().text()).toBe(stateText);
},
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index c92d092eb43..96083478e77 100644
--- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -2,6 +2,8 @@ import * as Sentry from '@sentry/browser';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { STATUS_OPEN } from '~/issues/constants';
@@ -20,6 +22,8 @@ describe('WorkItemsListApp component', () => {
const defaultQueryHandler = jest.fn().mockResolvedValue(groupWorkItemsQueryResponse);
const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
+ const findIssueCardTimeInfo = () => wrapper.findComponent(IssueCardTimeInfo);
const mountComponent = ({ queryHandler = defaultQueryHandler } = {}) => {
wrapper = shallowMount(WorkItemsListApp, {
@@ -37,9 +41,9 @@ describe('WorkItemsListApp component', () => {
currentTab: STATUS_OPEN,
error: '',
issuables: [],
+ issuablesLoading: true,
namespace: 'work-items',
recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: 'Search or filter results...',
searchTokens: [],
showWorkItemTypeIcon: true,
sortOptions: [],
@@ -47,6 +51,18 @@ describe('WorkItemsListApp component', () => {
});
});
+ it('renders IssueCardStatistics component', () => {
+ mountComponent();
+
+ expect(findIssueCardStatistics().exists()).toBe(true);
+ });
+
+ it('renders IssueCardTimeInfo component', () => {
+ mountComponent();
+
+ expect(findIssueCardTimeInfo().exists()).toBe(true);
+ });
+
it('renders work items', async () => {
mountComponent();
await waitForPromises();
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 05e83c0df3d..ba244b19eb5 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -1,3 +1,5 @@
+import { WIDGET_TYPE_LINKED_ITEMS } from '~/work_items/constants';
+
export const mockAssignees = [
{
__typename: 'UserCore',
@@ -451,6 +453,126 @@ export const objectiveType = {
iconName: 'issue-type-objective',
};
+export const mockEmptyLinkedItems = {
+ type: WIDGET_TYPE_LINKED_ITEMS,
+ blocked: false,
+ blockedByCount: 0,
+ blockingCount: 0,
+ linkedItems: {
+ nodes: [],
+ __typename: 'LinkedWorkItemTypeConnection',
+ },
+ __typename: 'WorkItemWidgetLinkedItems',
+};
+
+export const mockBlockingLinkedItem = {
+ type: WIDGET_TYPE_LINKED_ITEMS,
+ linkedItems: {
+ nodes: [
+ {
+ linkId: 'gid://gitlab/WorkItems::RelatedWorkItemLink/8',
+ linkType: 'blocks',
+ workItem: {
+ id: 'gid://gitlab/WorkItem/675',
+ iid: '83',
+ confidential: true,
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ __typename: 'WorkItemType',
+ },
+ title: 'Task 1201',
+ state: 'OPEN',
+ createdAt: '2023-03-28T10:50:16Z',
+ closedAt: null,
+ widgets: [],
+ __typename: 'WorkItem',
+ },
+ __typename: 'LinkedWorkItemType',
+ },
+ ],
+ __typename: 'LinkedWorkItemTypeConnection',
+ },
+ __typename: 'WorkItemWidgetLinkedItems',
+};
+
+export const mockLinkedItems = {
+ type: WIDGET_TYPE_LINKED_ITEMS,
+ linkedItems: {
+ nodes: [
+ {
+ linkId: 'gid://gitlab/WorkItems::RelatedWorkItemLink/8',
+ linkType: 'relates_to',
+ workItem: {
+ id: 'gid://gitlab/WorkItem/675',
+ iid: '83',
+ confidential: true,
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ __typename: 'WorkItemType',
+ },
+ title: 'Task 1201',
+ state: 'OPEN',
+ createdAt: '2023-03-28T10:50:16Z',
+ closedAt: null,
+ widgets: [],
+ __typename: 'WorkItem',
+ },
+ __typename: 'LinkedWorkItemType',
+ },
+ {
+ linkId: 'gid://gitlab/WorkItems::RelatedWorkItemLink/9',
+ linkType: 'is_blocked_by',
+ workItem: {
+ id: 'gid://gitlab/WorkItem/646',
+ iid: '55',
+ confidential: true,
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ title: 'Multilevel Objective 1',
+ state: 'OPEN',
+ createdAt: '2023-03-28T10:50:16Z',
+ closedAt: null,
+ widgets: [],
+ __typename: 'WorkItem',
+ },
+ __typename: 'LinkedWorkItemType',
+ },
+ {
+ linkId: 'gid://gitlab/WorkItems::RelatedWorkItemLink/10',
+ linkType: 'blocks',
+ workItem: {
+ id: 'gid://gitlab/WorkItem/647',
+ iid: '56',
+ confidential: true,
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ title: 'Multilevel Objective 2',
+ state: 'OPEN',
+ createdAt: '2023-03-28T10:50:16Z',
+ closedAt: null,
+ widgets: [],
+ __typename: 'WorkItem',
+ },
+ __typename: 'LinkedWorkItemType',
+ },
+ ],
+ __typename: 'LinkedWorkItemTypeConnection',
+ },
+ __typename: 'WorkItemWidgetLinkedItems',
+};
+
export const workItemResponseFactory = ({
iid = '1',
canUpdate = false,
@@ -473,6 +595,7 @@ export const workItemResponseFactory = ({
confidential = false,
canInviteMembers = false,
labelsWidgetPresent = true,
+ linkedItemsWidgetPresent = true,
labels = mockLabels,
allowsScopedLabels = false,
lastEditedAt = null,
@@ -485,6 +608,7 @@ export const workItemResponseFactory = ({
updatedAt = '2022-08-08T12:32:54Z',
awardEmoji = mockAwardsWidget,
state = 'OPEN',
+ linkedItems = mockEmptyLinkedItems,
} = {}) => ({
data: {
workItem: {
@@ -683,6 +807,7 @@ export const workItemResponseFactory = ({
awardEmoji,
}
: { type: 'MOCK TYPE' },
+ linkedItemsWidgetPresent ? linkedItems : { type: 'MOCK TYPE' },
],
},
},
@@ -1471,6 +1596,27 @@ export const availableWorkItemsResponse = {
},
};
+export const searchedWorkItemsResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/459',
+ title: 'Task 2',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ },
+ },
+};
+
export const projectMembersResponseWithCurrentUser = {
data: {
workspace: {
@@ -1883,8 +2029,7 @@ export const mockWorkItemNotesResponse = {
systemNoteIconName: 'link',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_199',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_199',
lastEditedBy: null,
system: true,
internal: false,
@@ -1934,8 +2079,7 @@ export const mockWorkItemNotesResponse = {
systemNoteIconName: 'clock',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_201',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_201',
lastEditedBy: null,
system: true,
internal: false,
@@ -1984,8 +2128,7 @@ export const mockWorkItemNotesResponse = {
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_202',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_202',
lastEditedBy: null,
system: true,
internal: false,
@@ -2097,8 +2240,7 @@ export const mockWorkItemNotesByIidResponse = {
systemNoteIconName: 'link',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2153,8 +2295,7 @@ export const mockWorkItemNotesByIidResponse = {
systemNoteIconName: 'clock',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2210,8 +2351,7 @@ export const mockWorkItemNotesByIidResponse = {
systemNoteIconName: 'iteration',
createdAt: '2022-11-14T04:19:00Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2325,8 +2465,7 @@ export const mockMoreWorkItemNotesResponse = {
systemNoteIconName: 'link',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2381,8 +2520,7 @@ export const mockMoreWorkItemNotesResponse = {
systemNoteIconName: 'clock',
createdAt: '2022-11-14T04:18:59Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2435,8 +2573,7 @@ export const mockMoreWorkItemNotesResponse = {
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2511,7 +2648,7 @@ export const createWorkItemNoteResponse = {
systemNoteIconName: null,
createdAt: '2023-01-25T04:49:46Z',
lastEditedAt: null,
- url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
@@ -2565,7 +2702,7 @@ export const mockWorkItemCommentNote = {
systemNoteIconName: false,
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
- url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: false,
internal: false,
@@ -2665,8 +2802,7 @@ export const mockWorkItemNotesResponseWithComments = {
systemNoteIconName: null,
createdAt: '2023-01-12T07:47:40Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
@@ -2708,8 +2844,7 @@ export const mockWorkItemNotesResponseWithComments = {
systemNoteIconName: null,
createdAt: '2023-01-18T09:09:54Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
maxAccessLevelOfAuthor: 'Owner',
authorIsContributor: false,
@@ -2758,8 +2893,7 @@ export const mockWorkItemNotesResponseWithComments = {
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
- url:
- 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: false,
internal: false,
@@ -2821,7 +2955,7 @@ export const workItemNotesCreateSubscriptionResponse = {
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
- url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2836,7 +2970,7 @@ export const workItemNotesCreateSubscriptionResponse = {
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
- url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
@@ -2914,7 +3048,7 @@ export const workItemNotesUpdateSubscriptionResponse = {
systemNoteIconName: 'pencil',
createdAt: '2022-11-25T07:16:20Z',
lastEditedAt: null,
- url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37?iid_path=true#note_191',
+ url: 'http://127.0.0.1:3000/flightjs/Flight/-/work_items/37#note_191',
lastEditedBy: null,
system: true,
internal: false,
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
index aa24b80cf08..8a49140119d 100644
--- a/spec/frontend/work_items/utils_spec.js
+++ b/spec/frontend/work_items/utils_spec.js
@@ -1,4 +1,4 @@
-import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
+import { autocompleteDataSources, markdownPreviewPath, workItemPath } from '~/work_items/utils';
describe('autocompleteDataSources', () => {
beforeEach(() => {
@@ -25,3 +25,14 @@ describe('markdownPreviewPath', () => {
);
});
});
+
+describe('workItemPath', () => {
+ it('returns corrrect data sources', () => {
+ expect(workItemPath('project/group', '2')).toEqual('/project/group/-/work_items/2');
+ });
+
+ it('returns corrrect data sources with relative url root', () => {
+ gon.relative_url_root = '/foobar';
+ expect(workItemPath('project/group', '2')).toEqual('/foobar/project/group/-/work_items/2');
+ });
+});