summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/.eslintrc.yml1
-rw-r--r--spec/frontend/__helpers__/assert_props.js41
-rw-r--r--spec/frontend/__helpers__/create_mock_source_editor_extension.js12
-rw-r--r--spec/frontend/__helpers__/experimentation_helper.js7
-rw-r--r--spec/frontend/__helpers__/fixtures.js5
-rw-r--r--spec/frontend/__helpers__/gon_helper.js5
-rw-r--r--spec/frontend/__helpers__/init_vue_mr_page_helper.js10
-rw-r--r--spec/frontend/__helpers__/keep_alive_component_helper_spec.js4
-rw-r--r--spec/frontend/__helpers__/shared_test_setup.js16
-rw-r--r--spec/frontend/__helpers__/vue_mock_directive.js32
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper.js18
-rw-r--r--spec/frontend/__helpers__/vue_test_utils_helper_spec.js49
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper.js2
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper_spec.js14
-rw-r--r--spec/frontend/__helpers__/wait_for_text.js2
-rw-r--r--spec/frontend/__mocks__/@gitlab/ui.js18
-rw-r--r--spec/frontend/__mocks__/file_mock.js2
-rw-r--r--spec/frontend/__mocks__/lodash/debounce.js19
-rw-r--r--spec/frontend/__mocks__/lodash/throttle.js2
-rw-r--r--spec/frontend/__mocks__/mousetrap/index.js6
-rw-r--r--spec/frontend/abuse_reports/components/abuse_category_selector_spec.js4
-rw-r--r--spec/frontend/access_tokens/components/expires_at_field_spec.js4
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js5
-rw-r--r--spec/frontend/access_tokens/components/token_spec.js4
-rw-r--r--spec/frontend/access_tokens/components/tokens_app_spec.js4
-rw-r--r--spec/frontend/access_tokens/index_spec.js2
-rw-r--r--spec/frontend/activities_spec.js6
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap12
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js37
-rw-r--r--spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js4
-rw-r--r--spec/frontend/add_context_commits_modal/store/actions_spec.js2
-rw-r--r--spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js76
-rw-r--r--spec/frontend/admin/abuse_report/components/history_items_spec.js66
-rw-r--r--spec/frontend/admin/abuse_report/components/report_header_spec.js59
-rw-r--r--spec/frontend/admin/abuse_report/components/reported_content_spec.js193
-rw-r--r--spec/frontend/admin/abuse_report/components/user_detail_spec.js66
-rw-r--r--spec/frontend/admin/abuse_report/components/user_details_spec.js210
-rw-r--r--spec/frontend/admin/abuse_report/mock_data.js61
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js202
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js91
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js225
-rw-r--r--spec/frontend/admin/abuse_reports/components/app_spec.js104
-rw-r--r--spec/frontend/admin/abuse_reports/mock_data.js18
-rw-r--r--spec/frontend/admin/abuse_reports/utils_spec.js31
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js4
-rw-r--r--spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js4
-rw-r--r--spec/frontend/admin/application_settings/network_outbound_spec.js70
-rw-r--r--spec/frontend/admin/applications/components/delete_application_spec.js1
-rw-r--r--spec/frontend/admin/background_migrations/components/database_listbox_spec.js4
-rw-r--r--spec/frontend/admin/broadcast_messages/components/base_spec.js9
-rw-r--r--spec/frontend/admin/broadcast_messages/components/message_form_spec.js39
-rw-r--r--spec/frontend/admin/broadcast_messages/components/messages_table_spec.js19
-rw-r--r--spec/frontend/admin/broadcast_messages/mock_data.js5
-rw-r--r--spec/frontend/admin/deploy_keys/components/table_spec.js10
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js4
-rw-r--r--spec/frontend/admin/signup_restrictions/components/signup_form_spec.js20
-rw-r--r--spec/frontend/admin/signup_restrictions/mock_data.js4
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js4
-rw-r--r--spec/frontend/admin/topics/components/remove_avatar_spec.js6
-rw-r--r--spec/frontend/admin/topics/components/topic_select_spec.js1
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js13
-rw-r--r--spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js12
-rw-r--r--spec/frontend/admin/users/components/app_spec.js5
-rw-r--r--spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap34
-rw-r--r--spec/frontend/admin/users/components/associations/associations_list_spec.js52
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js13
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js19
-rw-r--r--spec/frontend/admin/users/components/user_avatar_spec.js7
-rw-r--r--spec/frontend/admin/users/components/user_date_spec.js5
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js11
-rw-r--r--spec/frontend/admin/users/index_spec.js4
-rw-r--r--spec/frontend/admin/users/new_spec.js7
-rw-r--r--spec/frontend/airflow/dags/components/dags_spec.js115
-rw-r--r--spec/frontend/airflow/dags/components/mock_data.js67
-rw-r--r--spec/frontend/alert_management/components/alert_management_empty_state_spec.js6
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js6
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js5
-rw-r--r--spec/frontend/alert_spec.js (renamed from spec/frontend/flash_spec.js)2
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap34
-rw-r--r--spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js9
-rw-r--r--spec/frontend/alerts_settings/components/alerts_form_spec.js6
-rw-r--r--spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js7
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js20
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js14
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/__snapshots__/total_time_spec.js.snap (renamed from spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap)0
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/base_spec.js (renamed from spec/frontend/analytics/cycle_analytics/base_spec.js)36
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js (renamed from spec/frontend/analytics/cycle_analytics/filter_bar_spec.js)3
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/formatted_stage_count_spec.js (renamed from spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/path_navigation_spec.js (renamed from spec/frontend/analytics/cycle_analytics/path_navigation_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/stage_table_spec.js (renamed from spec/frontend/analytics/cycle_analytics/stage_table_spec.js)11
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/total_time_spec.js (renamed from spec/frontend/analytics/cycle_analytics/total_time_spec.js)4
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/value_stream_filters_spec.js (renamed from spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js)14
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/value_stream_metrics_spec.js (renamed from spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js)34
-rw-r--r--spec/frontend/analytics/cycle_analytics/mock_data.js4
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/actions_spec.js31
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/mutations_spec.js13
-rw-r--r--spec/frontend/analytics/cycle_analytics/utils_spec.js30
-rw-r--r--spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js4
-rw-r--r--spec/frontend/analytics/product_analytics/components/activity_chart_spec.js (renamed from spec/frontend/analytics/components/activity_chart_spec.js)5
-rw-r--r--spec/frontend/analytics/shared/components/daterange_spec.js15
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js4
-rw-r--r--spec/frontend/analytics/shared/components/metric_tile_spec.js10
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js30
-rw-r--r--spec/frontend/analytics/shared/utils_spec.js28
-rw-r--r--spec/frontend/analytics/usage_trends/components/app_spec.js5
-rw-r--r--spec/frontend/analytics/usage_trends/components/usage_counts_spec.js4
-rw-r--r--spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js5
-rw-r--r--spec/frontend/analytics/usage_trends/components/users_chart_spec.js5
-rw-r--r--spec/frontend/api/alert_management_alerts_api_spec.js3
-rw-r--r--spec/frontend/api/groups_api_spec.js13
-rw-r--r--spec/frontend/api/packages_api_spec.js12
-rw-r--r--spec/frontend/api/projects_api_spec.js25
-rw-r--r--spec/frontend/api/tags_api_spec.js3
-rw-r--r--spec/frontend/api/user_api_spec.js26
-rw-r--r--spec/frontend/api_spec.js25
-rw-r--r--spec/frontend/approvals/mock_data.js10
-rw-r--r--spec/frontend/artifacts/components/artifact_row_spec.js80
-rw-r--r--spec/frontend/artifacts/components/job_artifacts_table_spec.js363
-rw-r--r--spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js20
-rw-r--r--spec/frontend/authentication/password/components/password_input_spec.js64
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js25
-rw-r--r--spec/frontend/authentication/u2f/authenticate_spec.js104
-rw-r--r--spec/frontend/authentication/u2f/mock_u2f_device.js23
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js84
-rw-r--r--spec/frontend/authentication/u2f/util_spec.js61
-rw-r--r--spec/frontend/authentication/webauthn/authenticate_spec.js5
-rw-r--r--spec/frontend/authentication/webauthn/components/registration_spec.js255
-rw-r--r--spec/frontend/authentication/webauthn/error_spec.js13
-rw-r--r--spec/frontend/authentication/webauthn/register_spec.js5
-rw-r--r--spec/frontend/authentication/webauthn/util_spec.js31
-rw-r--r--spec/frontend/awards_handler_spec.js10
-rw-r--r--spec/frontend/badges/components/badge_form_spec.js1
-rw-r--r--spec/frontend/badges/components/badge_list_row_spec.js1
-rw-r--r--spec/frontend/badges/components/badge_list_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_settings_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/diff_file_drafts_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/drafts_count_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js19
-rw-r--r--spec/frontend/batch_comments/components/preview_item_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/review_bar_spec.js8
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js21
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js6
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js26
-rw-r--r--spec/frontend/behaviors/components/diagram_performance_warning_spec.js4
-rw-r--r--spec/frontend/behaviors/components/json_table_spec.js7
-rw-r--r--spec/frontend/behaviors/copy_to_clipboard_spec.js2
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js14
-rw-r--r--spec/frontend/behaviors/markdown/highlight_current_user_spec.js10
-rw-r--r--spec/frontend/behaviors/markdown/render_gfm_spec.js26
-rw-r--r--spec/frontend/behaviors/markdown/render_observability_spec.js61
-rw-r--r--spec/frontend/behaviors/quick_submit_spec.js18
-rw-r--r--spec/frontend/behaviors/requires_input_spec.js5
-rw-r--r--spec/frontend/behaviors/shortcuts/keybindings_spec.js6
-rw-r--r--spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js7
-rw-r--r--spec/frontend/blame/blame_redirect_spec.js5
-rw-r--r--spec/frontend/blame/streaming/index_spec.js110
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_content_error_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js24
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js11
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js4
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js170
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js53
-rw-r--r--spec/frontend/blob/components/mock_data.js2
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js1
-rw-r--r--spec/frontend/blob/csv/csv_viewer_spec.js4
-rw-r--r--spec/frontend/blob/file_template_selector_spec.js2
-rw-r--r--spec/frontend/blob/line_highlighter_spec.js6
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js2
-rw-r--r--spec/frontend/blob/pdf/pdf_viewer_spec.js5
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js1
-rw-r--r--spec/frontend/blob/sketch/index_spec.js5
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js5
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js11
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js2
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js12
-rw-r--r--spec/frontend/boards/board_list_helper.js1
-rw-r--r--spec/frontend/boards/board_list_spec.js124
-rw-r--r--spec/frontend/boards/components/board_add_new_column_form_spec.js124
-rw-r--r--spec/frontend/boards/components/board_add_new_column_spec.js61
-rw-r--r--spec/frontend/boards/components/board_add_new_column_trigger_spec.js6
-rw-r--r--spec/frontend/boards/components/board_app_spec.js51
-rw-r--r--spec/frontend/boards/components/board_card_spec.js47
-rw-r--r--spec/frontend/boards/components/board_column_spec.js8
-rw-r--r--spec/frontend/boards/components/board_configuration_options_spec.js8
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js93
-rw-r--r--spec/frontend/boards/components/board_content_spec.js91
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js52
-rw-r--r--spec/frontend/boards/components/board_form_spec.js79
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js178
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js4
-rw-r--r--spec/frontend/boards/components/board_new_item_spec.js4
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js48
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js19
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js17
-rw-r--r--spec/frontend/boards/components/config_toggle_spec.js4
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js16
-rw-r--r--spec/frontend/boards/components/issue_due_date_spec.js4
-rw-r--r--spec/frontend/boards/components/issue_time_estimate_spec.js4
-rw-r--r--spec/frontend/boards/components/item_count_spec.js8
-rw-r--r--spec/frontend/boards/components/new_board_button_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_editable_item_spec.js5
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js5
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js118
-rw-r--r--spec/frontend/boards/components/toggle_focus_spec.js6
-rw-r--r--spec/frontend/boards/mock_data.js195
-rw-r--r--spec/frontend/boards/project_select_spec.js5
-rw-r--r--spec/frontend/boards/stores/actions_spec.js22
-rw-r--r--spec/frontend/boards/stores/getters_spec.js8
-rw-r--r--spec/frontend/bootstrap_linked_tabs_spec.js5
-rw-r--r--spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap41
-rw-r--r--spec/frontend/branches/components/delete_branch_button_spec.js4
-rw-r--r--spec/frontend/branches/components/delete_branch_modal_spec.js94
-rw-r--r--spec/frontend/branches/components/delete_merged_branches_spec.js44
-rw-r--r--spec/frontend/branches/components/divergence_graph_spec.js4
-rw-r--r--spec/frontend/branches/components/graph_bar_spec.js4
-rw-r--r--spec/frontend/branches/components/sort_dropdown_spec.js6
-rw-r--r--spec/frontend/captcha/captcha_modal_spec.js74
-rw-r--r--spec/frontend/captcha/init_recaptcha_script_spec.js2
-rw-r--r--spec/frontend/ci/artifacts/components/app_spec.js (renamed from spec/frontend/artifacts/components/app_spec.js)75
-rw-r--r--spec/frontend/ci/artifacts/components/artifact_row_spec.js127
-rw-r--r--spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js58
-rw-r--r--spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js (renamed from spec/frontend/artifacts/components/artifacts_table_row_details_spec.js)45
-rw-r--r--spec/frontend/ci/artifacts/components/feedback_banner_spec.js (renamed from spec/frontend/artifacts/components/feedback_banner_spec.js)8
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js684
-rw-r--r--spec/frontend/ci/artifacts/components/job_checkbox_spec.js132
-rw-r--r--spec/frontend/ci/artifacts/graphql/cache_update_spec.js (renamed from spec/frontend/artifacts/graphql/cache_update_spec.js)4
-rw-r--r--spec/frontend/ci/ci_lint/components/ci_lint_spec.js1
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js10
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js26
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js153
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js26
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js31
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js57
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js67
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js749
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js189
-rw-r--r--spec/frontend/ci/ci_variable_list/mocks.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js12
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js46
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js43
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js76
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js9
-rw-r--r--spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_header_spec.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js102
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js127
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js39
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js60
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js70
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item_spec.js79
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js252
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js7
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js5
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js10
-rw-r--r--spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js32
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js20
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js113
-rw-r--r--spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js190
-rw-r--r--spec/frontend/ci/pipeline_new/mock_data.js15
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js8
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js6
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js20
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js7
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_legacy_spec.js14
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js2
-rw-r--r--spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js9
-rw-r--r--spec/frontend/ci/reports/components/grouped_issues_list_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/issue_status_icon_spec.js5
-rw-r--r--spec/frontend/ci/reports/components/report_link_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/report_section_spec.js4
-rw-r--r--spec/frontend/ci/reports/components/summary_row_spec.js5
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js127
-rw-r--r--spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js122
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js15
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js44
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js12
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js58
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap201
-rw-r--r--spec/frontend/ci/runner/components/registration/cli_command_spec.js39
-rw-r--r--spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js108
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_compatibility_alert_spec.js53
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js149
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js52
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_instructions_spec.js326
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js16
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js46
-rw-r--r--spec/frontend/ci/runner/components/registration/utils_spec.js94
-rw-r--r--spec/frontend/ci/runner/components/runner_assigned_item_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js18
-rw-r--r--spec/frontend/ci/runner/components/runner_create_form_spec.js189
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_button_spec.js24
-rw-r--r--spec/frontend/ci/runner/components/runner_details_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_button_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js18
-rw-r--r--spec/frontend/ci/runner/components/runner_groups_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js35
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_spec.js16
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_table_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js78
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_membership_toggle_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_pagination_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_button_spec.js12
-rw-r--r--spec/frontend/ci/runner/components/runner_paused_badge_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_projects_spec.js9
-rw-r--r--spec/frontend/ci/runner/components/runner_status_badge_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_tag_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/runner_tags_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_type_badge_spec.js9
-rw-r--r--spec/frontend/ci/runner/components/runner_type_tabs_spec.js7
-rw-r--r--spec/frontend/ci/runner/components/runner_update_form_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_count_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/stat/runner_stats_spec.js4
-rw-r--r--spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js124
-rw-r--r--spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js120
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js13
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js89
-rw-r--r--spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js4
-rw-r--r--spec/frontend/ci/runner/mock_data.js138
-rw-r--r--spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js125
-rw-r--r--spec/frontend/ci/runner/project_register_runner_app/project_register_runner_app_spec.js120
-rw-r--r--spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/runner_search_utils_spec.js5
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js2
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap6
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/button_spec.js4
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/modal_spec.js1
-rw-r--r--spec/frontend/ci_secure_files/components/secure_files_list_spec.js13
-rw-r--r--spec/frontend/ci_secure_files/mock_data.js4
-rw-r--r--spec/frontend/clusters/agents/components/activity_events_list_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/activity_history_item_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/create_token_button_spec.js8
-rw-r--r--spec/frontend/clusters/agents/components/create_token_modal_spec.js7
-rw-r--r--spec/frontend/clusters/agents/components/integration_status_spec.js4
-rw-r--r--spec/frontend/clusters/agents/components/revoke_token_button_spec.js10
-rw-r--r--spec/frontend/clusters/agents/components/show_spec.js7
-rw-r--r--spec/frontend/clusters/agents/components/token_table_spec.js4
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js5
-rw-r--r--spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap2
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap209
-rw-r--r--spec/frontend/clusters/components/new_cluster_spec.js4
-rw-r--r--spec/frontend/clusters/components/remove_cluster_confirmation_spec.js22
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js31
-rw-r--r--spec/frontend/clusters_list/components/agent_empty_state_spec.js6
-rw-r--r--spec/frontend/clusters_list/components/agent_table_spec.js204
-rw-r--r--spec/frontend/clusters_list/components/agent_token_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/agents_spec.js2
-rw-r--r--spec/frontend/clusters_list/components/ancestor_notice_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js5
-rw-r--r--spec/frontend/clusters_list/components/clusters_empty_state_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_main_view_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js5
-rw-r--r--spec/frontend/clusters_list/components/clusters_view_all_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/delete_agent_button_spec.js9
-rw-r--r--spec/frontend/clusters_list/components/install_agent_modal_spec.js5
-rw-r--r--spec/frontend/clusters_list/components/mock_data.js108
-rw-r--r--spec/frontend/clusters_list/components/node_error_help_text_spec.js4
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js6
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js4
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js4
-rw-r--r--spec/frontend/code_review/signals_spec.js145
-rw-r--r--spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap140
-rw-r--r--spec/frontend/comment_templates/components/form_spec.js145
-rw-r--r--spec/frontend/comment_templates/components/list_item_spec.js154
-rw-r--r--spec/frontend/comment_templates/components/list_spec.js46
-rw-r--r--spec/frontend/comment_templates/pages/index_spec.js45
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js18
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js11
-rw-r--r--spec/frontend/commit/components/commit_box_pipeline_status_spec.js16
-rw-r--r--spec/frontend/commit/components/signature_badge_spec.js134
-rw-r--r--spec/frontend/commit/components/x509_certificate_details_spec.js36
-rw-r--r--spec/frontend/commit/mock_data.js31
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js118
-rw-r--r--spec/frontend/commons/nav/user_merge_requests_spec.js33
-rw-r--r--spec/frontend/confidential_merge_request/components/project_form_group_spec.js1
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_button_spec.js.snap2
-rw-r--r--spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap33
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js6
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js10
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js91
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js101
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js184
-rw-r--r--spec/frontend/content_editor/components/content_editor_alert_spec.js6
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js70
-rw-r--r--spec/frontend/content_editor/components/editor_state_observer_spec.js4
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js25
-rw-r--r--spec/frontend/content_editor/components/loading_indicator_spec.js4
-rw-r--r--spec/frontend/content_editor/components/suggestions_dropdown_spec.js22
-rw-r--r--spec/frontend/content_editor/components/toolbar_attachment_button_spec.js60
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js6
-rw-r--r--spec/frontend/content_editor/components/toolbar_image_button_spec.js97
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js224
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js42
-rw-r--r--spec/frontend/content_editor/components/toolbar_table_button_spec.js1
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js5
-rw-r--r--spec/frontend/content_editor/components/wrappers/__snapshots__/table_of_contents_spec.js.snap52
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js8
-rw-r--r--spec/frontend/content_editor/components/wrappers/details_spec.js6
-rw-r--r--spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js6
-rw-r--r--spec/frontend/content_editor/components/wrappers/reference_label_spec.js (renamed from spec/frontend/content_editor/components/wrappers/label_spec.js)12
-rw-r--r--spec/frontend/content_editor/components/wrappers/reference_spec.js46
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js32
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js6
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js6
-rw-r--r--spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js6
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js556
-rw-r--r--spec/frontend/content_editor/extensions/drawio_diagram_spec.js103
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js5
-rw-r--r--spec/frontend/content_editor/extensions/paste_markdown_spec.js54
-rw-r--r--spec/frontend/content_editor/markdown_snapshot_spec.js6
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js2
-rw-r--r--spec/frontend/content_editor/render_html_and_json_for_all_examples.js2
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js4
-rw-r--r--spec/frontend/content_editor/services/create_content_editor_spec.js18
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js24
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js112
-rw-r--r--spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js4
-rw-r--r--spec/frontend/content_editor/test_constants.js12
-rw-r--r--spec/frontend/content_editor/test_utils.js27
-rw-r--r--spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap66
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js6
-rw-r--r--spec/frontend/contributors/store/actions_spec.js6
-rw-r--r--spec/frontend/create_item_dropdown_spec.js5
-rw-r--r--spec/frontend/crm/contact_form_wrapper_spec.js1
-rw-r--r--spec/frontend/crm/contacts_root_spec.js1
-rw-r--r--spec/frontend/crm/crm_form_spec.js4
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js4
-rw-r--r--spec/frontend/crm/organizations_root_spec.js1
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js3
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js9
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js5
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js5
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js4
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js1
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js38
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js5
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js51
-rw-r--r--spec/frontend/deploy_tokens/components/revoke_button_spec.js4
-rw-r--r--spec/frontend/deprecated_jquery_dropdown_spec.js5
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js6
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap3
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js170
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js56
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js258
-rw-r--r--spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js48
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js6
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js7
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js10
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js6
-rw-r--r--spec/frontend/design_management/components/image_spec.js4
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap10
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js4
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap39
-rw-r--r--spec/frontend/design_management/components/toolbar/design_navigation_spec.js71
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js46
-rw-r--r--spec/frontend/design_management/components/upload/button_spec.js4
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js49
-rw-r--r--spec/frontend/design_management/components/upload/mock_data/all_versions.js20
-rw-r--r--spec/frontend/design_management/mock_data/all_versions.js8
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js205
-rw-r--r--spec/frontend/design_management/mock_data/project.js17
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap8
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap4
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js83
-rw-r--r--spec/frontend/design_management/pages/index_spec.js54
-rw-r--r--spec/frontend/design_management/router_spec.js6
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js4
-rw-r--r--spec/frontend/diffs/components/app_spec.js135
-rw-r--r--spec/frontend/diffs/components/collapsed_files_warning_spec.js4
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js27
-rw-r--r--spec/frontend/diffs/components/compare_dropdown_layout_spec.js5
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js24
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_item_spec.js66
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_spec.js44
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js5
-rw-r--r--spec/frontend/diffs/components/diff_discussion_reply_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js10
-rw-r--r--spec/frontend/diffs/components/diff_file_row_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js40
-rw-r--r--spec/frontend/diffs/components/diff_gutter_avatars_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js17
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js19
-rw-r--r--spec/frontend/diffs/components/hidden_files_warning_spec.js8
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js4
-rw-r--r--spec/frontend/diffs/components/merge_conflict_warning_spec.js4
-rw-r--r--spec/frontend/diffs/components/no_changes_spec.js5
-rw-r--r--spec/frontend/diffs/components/settings_dropdown_spec.js1
-rw-r--r--spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap126
-rw-r--r--spec/frontend/diffs/components/shared/findings_drawer_spec.js19
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js83
-rw-r--r--spec/frontend/diffs/create_diffs_store.js2
-rw-r--r--spec/frontend/diffs/mock_data/diff_code_quality.js5
-rw-r--r--spec/frontend/diffs/mock_data/findings_drawer.js21
-rw-r--r--spec/frontend/diffs/store/actions_spec.js499
-rw-r--r--spec/frontend/diffs/store/getters_spec.js25
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js9
-rw-r--r--spec/frontend/diffs/store/utils_spec.js57
-rw-r--r--spec/frontend/diffs/utils/merge_request_spec.js94
-rw-r--r--spec/frontend/diffs/utils/tree_worker_utils_spec.js30
-rw-r--r--spec/frontend/drawio/content_editor_facade_spec.js138
-rw-r--r--spec/frontend/drawio/drawio_editor_spec.js479
-rw-r--r--spec/frontend/drawio/markdown_field_editor_facade_spec.js148
-rw-r--r--spec/frontend/dropzone_input_spec.js7
-rw-r--r--spec/frontend/editor/components/helpers.js3
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js36
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_spec.js37
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js12
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules_needs.yml46
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml38
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules_needs.yml32
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml31
-rw-r--r--spec/frontend/editor/source_editor_ci_schema_ext_spec.js11
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js112
-rw-r--r--spec/frontend/editor/source_editor_markdown_ext_spec.js25
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js106
-rw-r--r--spec/frontend/editor/source_editor_webide_ext_spec.js1
-rw-r--r--spec/frontend/editor/utils_spec.js30
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js4
-rw-r--r--spec/frontend/emoji/components/category_spec.js21
-rw-r--r--spec/frontend/emoji/components/emoji_group_spec.js4
-rw-r--r--spec/frontend/emoji/components/emoji_list_spec.js33
-rw-r--r--spec/frontend/environment.js19
-rw-r--r--spec/frontend/environments/canary_ingress_spec.js10
-rw-r--r--spec/frontend/environments/canary_update_modal_spec.js10
-rw-r--r--spec/frontend/environments/confirm_rollback_modal_spec.js36
-rw-r--r--spec/frontend/environments/delete_environment_modal_spec.js6
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js4
-rw-r--r--spec/frontend/environments/deploy_freeze_alert_spec.js111
-rw-r--r--spec/frontend/environments/edit_environment_spec.js5
-rw-r--r--spec/frontend/environments/empty_state_spec.js69
-rw-r--r--spec/frontend/environments/enable_review_app_modal_spec.js6
-rw-r--r--spec/frontend/environments/environment_actions_spec.js115
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_actions_spec.js119
-rw-r--r--spec/frontend/environments/environment_details/deployments_table_spec.js58
-rw-r--r--spec/frontend/environments/environment_details/index_spec.js (renamed from spec/frontend/environments/environment_details/page_spec.js)60
-rw-r--r--spec/frontend/environments/environment_external_url_spec.js33
-rw-r--r--spec/frontend/environments/environment_folder_spec.js4
-rw-r--r--spec/frontend/environments/environment_form_spec.js20
-rw-r--r--spec/frontend/environments/environment_item_spec.js34
-rw-r--r--spec/frontend/environments/environment_pin_spec.js12
-rw-r--r--spec/frontend/environments/environment_stop_spec.js2
-rw-r--r--spec/frontend/environments/environment_table_spec.js8
-rw-r--r--spec/frontend/environments/environments_app_spec.js16
-rw-r--r--spec/frontend/environments/environments_detail_header_spec.js57
-rw-r--r--spec/frontend/environments/environments_folder_view_spec.js1
-rw-r--r--spec/frontend/environments/folder/environments_folder_view_spec.js18
-rw-r--r--spec/frontend/environments/graphql/mock_data.js109
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js182
-rw-r--r--spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap34
-rw-r--r--spec/frontend/environments/kubernetes_agent_info_spec.js124
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js131
-rw-r--r--spec/frontend/environments/kubernetes_pods_spec.js114
-rw-r--r--spec/frontend/environments/kubernetes_summary_spec.js115
-rw-r--r--spec/frontend/environments/kubernetes_tabs_spec.js168
-rw-r--r--spec/frontend/environments/mock_data.js3
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js127
-rw-r--r--spec/frontend/environments/new_environment_spec.js5
-rw-r--r--spec/frontend/environments/stop_stale_environments_modal_spec.js10
-rw-r--r--spec/frontend/error_tracking/components/error_details_info_spec.js190
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js132
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_actions_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js49
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_spec.js6
-rw-r--r--spec/frontend/error_tracking/events_tracking_spec.js (renamed from spec/frontend/error_tracking/utils_spec.js)2
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js4
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js6
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js6
-rw-r--r--spec/frontend/error_tracking_settings/components/app_spec.js8
-rw-r--r--spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js6
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js6
-rw-r--r--spec/frontend/experimentation/components/gitlab_experiment_spec.js7
-rw-r--r--spec/frontend/experimentation/utils_spec.js3
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js19
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js5
-rw-r--r--spec/frontend/feature_flags/components/empty_state_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/environments_dropdown_spec.js1
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js64
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_table_spec.js48
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js4
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/strategies/users_with_id_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/strategy_parameters_spec.js8
-rw-r--r--spec/frontend/feature_flags/components/strategy_spec.js11
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js6
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_popover_spec.js5
-rw-r--r--spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js5
-rw-r--r--spec/frontend/filtered_search/dropdown_user_spec.js11
-rw-r--r--spec/frontend/filtered_search/dropdown_utils_spec.js7
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js4
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js4
-rw-r--r--spec/frontend/fixtures/abuse_reports.rb2
-rw-r--r--spec/frontend/fixtures/api_deploy_keys.rb5
-rw-r--r--spec/frontend/fixtures/api_projects.rb15
-rw-r--r--spec/frontend/fixtures/comment_templates.rb74
-rw-r--r--spec/frontend/fixtures/environments.rb2
-rw-r--r--spec/frontend/fixtures/issues.rb9
-rw-r--r--spec/frontend/fixtures/job_artifacts.rb2
-rw-r--r--spec/frontend/fixtures/jobs.rb84
-rw-r--r--spec/frontend/fixtures/merge_requests.rb6
-rw-r--r--spec/frontend/fixtures/metrics_dashboard.rb1
-rw-r--r--spec/frontend/fixtures/milestones.rb43
-rw-r--r--spec/frontend/fixtures/pipelines.rb25
-rw-r--r--spec/frontend/fixtures/projects.rb2
-rw-r--r--spec/frontend/fixtures/prometheus_integration.rb1
-rw-r--r--spec/frontend/fixtures/runner.rb48
-rw-r--r--spec/frontend/fixtures/saved_replies.rb46
-rw-r--r--spec/frontend/fixtures/startup_css.rb17
-rw-r--r--spec/frontend/fixtures/static/oauth_remember_me.html2
-rw-r--r--spec/frontend/fixtures/static/search_autocomplete.html15
-rw-r--r--spec/frontend/fixtures/timelogs.rb53
-rw-r--r--spec/frontend/fixtures/u2f.rb48
-rw-r--r--spec/frontend/fixtures/users.rb75
-rw-r--r--spec/frontend/fixtures/webauthn.rb1
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js2
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js2
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_spec.js6
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js1
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js5
-rw-r--r--spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js8
-rw-r--r--spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js1
-rw-r--r--spec/frontend/gl_field_errors_spec.js5
-rw-r--r--spec/frontend/google_cloud/aiml/panel_spec.js43
-rw-r--r--spec/frontend/google_cloud/aiml/service_table_spec.js34
-rw-r--r--spec/frontend/google_cloud/components/google_cloud_menu_spec.js11
-rw-r--r--spec/frontend/google_cloud/components/incubation_banner_spec.js4
-rw-r--r--spec/frontend/google_cloud/components/revoke_oauth_spec.js4
-rw-r--r--spec/frontend/google_cloud/configuration/panel_spec.js4
-rw-r--r--spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js4
-rw-r--r--spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js4
-rw-r--r--spec/frontend/google_cloud/databases/panel_spec.js4
-rw-r--r--spec/frontend/google_cloud/databases/service_table_spec.js4
-rw-r--r--spec/frontend/google_cloud/deployments/panel_spec.js4
-rw-r--r--spec/frontend/google_cloud/deployments/service_table_spec.js4
-rw-r--r--spec/frontend/google_cloud/gcp_regions/form_spec.js4
-rw-r--r--spec/frontend/google_cloud/gcp_regions/list_spec.js4
-rw-r--r--spec/frontend/google_cloud/service_accounts/form_spec.js4
-rw-r--r--spec/frontend/google_cloud/service_accounts/list_spec.js4
-rw-r--r--spec/frontend/google_tag_manager/index_spec.js4
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js13
-rw-r--r--spec/frontend/graphql_shared/utils_spec.js124
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js69
-rw-r--r--spec/frontend/groups/components/app_spec.js15
-rw-r--r--spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js4
-rw-r--r--spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js4
-rw-r--r--spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js5
-rw-r--r--spec/frontend/groups/components/group_folder_spec.js4
-rw-r--r--spec/frontend/groups/components/group_item_spec.js4
-rw-r--r--spec/frontend/groups/components/group_name_and_path_spec.js4
-rw-r--r--spec/frontend/groups/components/groups_spec.js6
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js9
-rw-r--r--spec/frontend/groups/components/item_actions_spec.js5
-rw-r--r--spec/frontend/groups/components/item_caret_spec.js7
-rw-r--r--spec/frontend/groups/components/item_stats_spec.js7
-rw-r--r--spec/frontend/groups/components/item_stats_value_spec.js7
-rw-r--r--spec/frontend/groups/components/item_type_icon_spec.js7
-rw-r--r--spec/frontend/groups/components/new_top_level_group_alert_spec.js4
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js2
-rw-r--r--spec/frontend/groups/components/transfer_group_form_spec.js6
-rw-r--r--spec/frontend/groups/settings/components/group_settings_readme_spec.js112
-rw-r--r--spec/frontend/groups/settings/mock_data.js6
-rw-r--r--spec/frontend/groups_projects/components/transfer_locations_spec.js4
-rw-r--r--spec/frontend/header_search/components/app_spec.js90
-rw-r--r--spec/frontend/header_search/components/header_search_autocomplete_items_spec.js11
-rw-r--r--spec/frontend/header_search/components/header_search_default_items_spec.js4
-rw-r--r--spec/frontend/header_search/components/header_search_scoped_items_spec.js7
-rw-r--r--spec/frontend/header_search/init_spec.js18
-rw-r--r--spec/frontend/header_search/mock_data.js10
-rw-r--r--spec/frontend/header_search/store/actions_spec.js2
-rw-r--r--spec/frontend/header_search/store/getters_spec.js7
-rw-r--r--spec/frontend/header_spec.js6
-rw-r--r--spec/frontend/helpers/init_simple_app_helper_spec.js6
-rw-r--r--spec/frontend/helpers/startup_css_helper_spec.js7
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js59
-rw-r--r--spec/frontend/ide/components/branches/item_spec.js4
-rw-r--r--spec/frontend/ide/components/branches/search_list_spec.js5
-rw-r--r--spec/frontend/ide/components/cannot_push_code_alert_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js39
-rw-r--r--spec/frontend/ide/components/commit_sidebar/empty_state_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js27
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_item_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/message_field_spec.js4
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js8
-rw-r--r--spec/frontend/ide/components/commit_sidebar/radio_group_spec.js6
-rw-r--r--spec/frontend/ide/components/commit_sidebar/success_message_spec.js4
-rw-r--r--spec/frontend/ide/components/error_message_spec.js5
-rw-r--r--spec/frontend/ide/components/file_row_extra_spec.js2
-rw-r--r--spec/frontend/ide/components/file_templates/bar_spec.js4
-rw-r--r--spec/frontend/ide/components/file_templates/dropdown_spec.js5
-rw-r--r--spec/frontend/ide/components/ide_file_row_spec.js5
-rw-r--r--spec/frontend/ide/components/ide_project_header_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js6
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js5
-rw-r--r--spec/frontend/ide/components/ide_sidebar_nav_spec.js11
-rw-r--r--spec/frontend/ide/components/ide_spec.js13
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_status_list_spec.js3
-rw-r--r--spec/frontend/ide/components/ide_status_mr_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_tree_list_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js74
-rw-r--r--spec/frontend/ide/components/jobs/detail/description_spec.js4
-rw-r--r--spec/frontend/ide/components/jobs/detail/scroll_button_spec.js6
-rw-r--r--spec/frontend/ide/components/jobs/detail_spec.js16
-rw-r--r--spec/frontend/ide/components/jobs/item_spec.js4
-rw-r--r--spec/frontend/ide/components/jobs/list_spec.js5
-rw-r--r--spec/frontend/ide/components/jobs/stage_spec.js5
-rw-r--r--spec/frontend/ide/components/merge_requests/item_spec.js5
-rw-r--r--spec/frontend/ide/components/merge_requests/list_spec.js5
-rw-r--r--spec/frontend/ide/components/nav_dropdown_button_spec.js4
-rw-r--r--spec/frontend/ide/components/nav_dropdown_spec.js4
-rw-r--r--spec/frontend/ide/components/new_dropdown/button_spec.js4
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js60
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js21
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js60
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js5
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js5
-rw-r--r--spec/frontend/ide/components/pipelines/empty_state_spec.js4
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js5
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js22
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js16
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js84
-rw-r--r--spec/frontend/ide/components/repo_tabs_spec.js4
-rw-r--r--spec/frontend/ide/components/resizable_panel_spec.js5
-rw-r--r--spec/frontend/ide/components/shared/commit_message_field_spec.js6
-rw-r--r--spec/frontend/ide/components/shared/tokened_input_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/empty_state_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/terminal_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal/view_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js4
-rw-r--r--spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js4
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js7
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js7
-rw-r--r--spec/frontend/ide/lib/languages/codeowners_spec.js85
-rw-r--r--spec/frontend/ide/services/index_spec.js3
-rw-r--r--spec/frontend/ide/services/terminals_spec.js2
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js4
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js8
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js6
-rw-r--r--spec/frontend/ide/stores/actions_spec.js10
-rw-r--r--spec/frontend/ide/stores/extend_spec.js5
-rw-r--r--spec/frontend/ide/stores/getters_spec.js7
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js10
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js8
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js6
-rw-r--r--spec/frontend/import/details/components/import_details_app_spec.js18
-rw-r--r--spec/frontend/import/details/components/import_details_table_spec.js113
-rw-r--r--spec/frontend/import/details/mock_data.js53
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js8
-rw-r--r--spec/frontend/import_entities/components/import_status_spec.js78
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js47
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js62
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js11
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js4
-rw-r--r--spec/frontend/import_entities/import_groups/services/status_poller_spec.js6
-rw-r--r--spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js8
-rw-r--r--spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js24
-rw-r--r--spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js97
-rw-r--r--spec/frontend/import_entities/import_projects/components/github_status_table_spec.js125
-rw-r--r--spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js11
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js19
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js18
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js13
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js9
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap59
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js6
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js30
-rw-r--r--spec/frontend/incidents_settings/components/pagerduty_form_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/active_checkbox_spec.js8
-rw-r--r--spec/frontend/integrations/edit/components/confirmation_modal_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js53
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js9
-rw-r--r--spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js6
-rw-r--r--spec/frontend/integrations/edit/components/override_dropdown_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js57
-rw-r--r--spec/frontend/integrations/edit/components/sections/configuration_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/connection_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/google_play_spec.js54
-rw-r--r--spec/frontend/integrations/edit/components/sections/jira_issues_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/sections/trigger_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/trigger_field_spec.js6
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js88
-rw-r--r--spec/frontend/integrations/index/components/integrations_list_spec.js4
-rw-r--r--spec/frontend/integrations/index/components/integrations_table_spec.js61
-rw-r--r--spec/frontend/integrations/index/mock_data.js9
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js1
-rw-r--r--spec/frontend/integrations/overrides/components/integration_tabs_spec.js4
-rw-r--r--spec/frontend/invite_members/components/confetti_spec.js8
-rw-r--r--spec/frontend/invite_members/components/group_select_spec.js8
-rw-r--r--spec/frontend/invite_members/components/import_project_members_modal_spec.js18
-rw-r--r--spec/frontend/invite_members/components/import_project_members_trigger_spec.js4
-rw-r--r--spec/frontend/invite_members/components/invite_group_notification_spec.js14
-rw-r--r--spec/frontend/invite_members/components/invite_group_trigger_spec.js5
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js43
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js302
-rw-r--r--spec/frontend/invite_members/components/invite_members_trigger_spec.js72
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js31
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js5
-rw-r--r--spec/frontend/invite_members/components/project_select_spec.js4
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js31
-rw-r--r--spec/frontend/invite_members/utils/member_utils_spec.js30
-rw-r--r--spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js8
-rw-r--r--spec/frontend/issuable/components/csv_export_modal_spec.js18
-rw-r--r--spec/frontend/issuable/components/csv_import_export_buttons_spec.js92
-rw-r--r--spec/frontend/issuable/components/csv_import_modal_spec.js4
-rw-r--r--spec/frontend/issuable/components/issuable_by_email_spec.js2
-rw-r--r--spec/frontend/issuable/components/issuable_header_warnings_spec.js7
-rw-r--r--spec/frontend/issuable/components/issue_assignees_spec.js5
-rw-r--r--spec/frontend/issuable/components/issue_milestone_spec.js159
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js63
-rw-r--r--spec/frontend/issuable/components/status_box_spec.js5
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js69
-rw-r--r--spec/frontend/issuable/popover/components/issue_popover_spec.js4
-rw-r--r--spec/frontend/issuable/popover/components/mr_popover_spec.js8
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js245
-rw-r--r--spec/frontend/issuable/related_issues/components/issue_token_spec.js7
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js152
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_list_spec.js98
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js8
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js8
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js70
-rw-r--r--spec/frontend/issues/issue_spec.js8
-rw-r--r--spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js5
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js4
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js192
-rw-r--r--spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js7
-rw-r--r--spec/frontend/issues/list/mock_data.js25
-rw-r--r--spec/frontend/issues/list/utils_spec.js20
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_item_spec.js4
-rw-r--r--spec/frontend/issues/new/components/title_suggestions_spec.js123
-rw-r--r--spec/frontend/issues/new/components/type_popover_spec.js4
-rw-r--r--spec/frontend/issues/new/components/type_select_spec.js141
-rw-r--r--spec/frontend/issues/new/mock_data.js64
-rw-r--r--spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js1
-rw-r--r--spec/frontend/issues/related_merge_requests/store/actions_spec.js4
-rw-r--r--spec/frontend/issues/show/components/app_spec.js592
-rw-r--r--spec/frontend/issues/show/components/delete_issue_modal_spec.js4
-rw-r--r--spec/frontend/issues/show/components/description_spec.js263
-rw-r--r--spec/frontend/issues/show/components/edit_actions_spec.js6
-rw-r--r--spec/frontend/issues/show/components/edited_spec.js73
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js25
-rw-r--r--spec/frontend/issues/show/components/fields/description_template_spec.js4
-rw-r--r--spec/frontend/issues/show/components/fields/title_spec.js5
-rw-r--r--spec/frontend/issues/show/components/fields/type_spec.js8
-rw-r--r--spec/frontend/issues/show/components/form_spec.js4
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js366
-rw-r--r--spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js6
-rw-r--r--spec/frontend/issues/show/components/incidents/highlight_bar_spec.js7
-rw-r--r--spec/frontend/issues/show/components/incidents/incident_tabs_spec.js57
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js19
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js8
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js10
-rw-r--r--spec/frontend/issues/show/components/incidents/utils_spec.js4
-rw-r--r--spec/frontend/issues/show/components/locked_warning_spec.js9
-rw-r--r--spec/frontend/issues/show/components/new_header_actions_popover_spec.js77
-rw-r--r--spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js6
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js19
-rw-r--r--spec/frontend/issues/show/components/title_spec.js89
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js74
-rw-r--r--spec/frontend/jira_connect/branches/components/new_branch_form_spec.js4
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js4
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js8
-rw-r--r--spec/frontend/jira_connect/branches/pages/index_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/api_spec.js28
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js6
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js108
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js298
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js58
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js6
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/user_link_spec.js80
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js74
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js35
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js18
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js97
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js34
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/store/actions_spec.js20
-rw-r--r--spec/frontend/jira_connect/subscriptions/store/mutations_spec.js16
-rw-r--r--spec/frontend/jira_connect/subscriptions/utils_spec.js46
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap18
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js5
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js5
-rw-r--r--spec/frontend/jira_import/components/jira_import_progress_spec.js5
-rw-r--r--spec/frontend/jira_import/components/jira_import_setup_spec.js5
-rw-r--r--spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js4
-rw-r--r--spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/artifacts_block_spec.js5
-rw-r--r--spec/frontend/jobs/components/job/commit_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/empty_state_spec.js7
-rw-r--r--spec/frontend/jobs/components/job/environments_block_spec.js5
-rw-r--r--spec/frontend/jobs/components/job/erased_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js3
-rw-r--r--spec/frontend/jobs/components/job/job_container_item_spec.js5
-rw-r--r--spec/frontend/jobs/components/job/job_log_controllers_spec.js17
-rw-r--r--spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js19
-rw-r--r--spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js7
-rw-r--r--spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js6
-rw-r--r--spec/frontend/jobs/components/job/jobs_container_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js112
-rw-r--r--spec/frontend/jobs/components/job/mock_data.js27
-rw-r--r--spec/frontend/jobs/components/job/sidebar_detail_row_spec.js32
-rw-r--r--spec/frontend/jobs/components/job/sidebar_header_spec.js2
-rw-r--r--spec/frontend/jobs/components/job/sidebar_spec.js8
-rw-r--r--spec/frontend/jobs/components/job/stages_dropdown_spec.js6
-rw-r--r--spec/frontend/jobs/components/job/stuck_block_spec.js7
-rw-r--r--spec/frontend/jobs/components/job/trigger_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/collapsible_section_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/duration_badge_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/line_number_spec.js4
-rw-r--r--spec/frontend/jobs/components/log/log_spec.js77
-rw-r--r--spec/frontend/jobs/components/log/mock_data.js24
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js8
-rw-r--r--spec/frontend/jobs/components/table/cells/duration_cell_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/cells/job_cell_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/graphql/cache_config_spec.js19
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js144
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_spec.js84
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_tabs_spec.js23
-rw-r--r--spec/frontend/jobs/mixins/delayed_job_mixin_spec.js5
-rw-r--r--spec/frontend/jobs/mock_data.js20
-rw-r--r--spec/frontend/jobs/store/utils_spec.js8
-rw-r--r--spec/frontend/labels/components/delete_label_modal_spec.js83
-rw-r--r--spec/frontend/labels/components/promote_label_modal_spec.js1
-rw-r--r--spec/frontend/language_switcher/components/app_spec.js4
-rw-r--r--spec/frontend/lib/apollo/indexed_db_persistent_storage_spec.js90
-rw-r--r--spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json151
-rw-r--r--spec/frontend/lib/apollo/persist_link_spec.js4
-rw-r--r--spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js5
-rw-r--r--spec/frontend/lib/dompurify_spec.js14
-rw-r--r--spec/frontend/lib/mousetrap_spec.js113
-rw-r--r--spec/frontend/lib/utils/axios_startup_calls_spec.js7
-rw-r--r--spec/frontend/lib/utils/chart_utils_spec.js55
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js42
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js8
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js3
-rw-r--r--spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js18
-rw-r--r--spec/frontend/lib/utils/css_utils_spec.js22
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js12
-rw-r--r--spec/frontend/lib/utils/datetime/time_spent_utility_spec.js25
-rw-r--r--spec/frontend/lib/utils/datetime/timeago_utility_spec.js37
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js40
-rw-r--r--spec/frontend/lib/utils/error_message_spec.js48
-rw-r--r--spec/frontend/lib/utils/file_upload_spec.js22
-rw-r--r--spec/frontend/lib/utils/intersection_observer_spec.js2
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js29
-rw-r--r--spec/frontend/lib/utils/poll_spec.js2
-rw-r--r--spec/frontend/lib/utils/ref_validator_spec.js79
-rw-r--r--spec/frontend/lib/utils/secret_detection_spec.js68
-rw-r--r--spec/frontend/lib/utils/sticky_spec.js77
-rw-r--r--spec/frontend/lib/utils/tappable_promise_spec.js63
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js62
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js32
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js20
-rw-r--r--spec/frontend/lib/utils/vuex_module_mappers_spec.js4
-rw-r--r--spec/frontend/lib/utils/web_ide_navigator_spec.js38
-rw-r--r--spec/frontend/listbox/redirect_behavior_spec.js6
-rw-r--r--spec/frontend/locale/sprintf_spec.js11
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js6
-rw-r--r--spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js7
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js6
-rw-r--r--spec/frontend/members/components/action_buttons/resend_invite_button_spec.js6
-rw-r--r--spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js6
-rw-r--r--spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js4
-rw-r--r--spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js6
-rw-r--r--spec/frontend/members/components/app_spec.js1
-rw-r--r--spec/frontend/members/components/avatars/group_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/avatars/invite_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js9
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js2
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js4
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js4
-rw-r--r--spec/frontend/members/components/modals/remove_group_link_modal_spec.js5
-rw-r--r--spec/frontend/members/components/modals/remove_member_modal_spec.js4
-rw-r--r--spec/frontend/members/components/table/created_at_spec.js4
-rw-r--r--spec/frontend/members/components/table/expiration_datepicker_spec.js6
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_avatar_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js6
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js5
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js4
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js96
-rw-r--r--spec/frontend/members/index_spec.js3
-rw-r--r--spec/frontend/members/utils_spec.js2
-rw-r--r--spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js4
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js6
-rw-r--r--spec/frontend/merge_request_spec.js12
-rw-r--r--spec/frontend/merge_request_tabs_spec.js47
-rw-r--r--spec/frontend/merge_requests/components/compare_app_spec.js4
-rw-r--r--spec/frontend/merge_requests/components/compare_dropdown_spec.js1
-rw-r--r--spec/frontend/milestones/components/delete_milestone_modal_spec.js14
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js5
-rw-r--r--spec/frontend/milestones/components/promote_milestone_modal_spec.js8
-rw-r--r--spec/frontend/milestones/index_spec.js38
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap268
-rw-r--r--spec/frontend/ml/experiment_tracking/components/delete_button_spec.js68
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js47
-rw-r--r--spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js35
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js49
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js119
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js23
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js19
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js (renamed from spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js)189
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js58
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/gauge_spec.js5
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js4
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js4
-rw-r--r--spec/frontend/monitoring/components/create_dashboard_modal_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js17
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js25
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js16
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js5
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js11
-rw-r--r--spec/frontend/monitoring/components/embeds/embed_group_spec.js3
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js3
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js4
-rw-r--r--spec/frontend/monitoring/components/group_empty_state_spec.js4
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js1
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js8
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js6
-rw-r--r--spec/frontend/monitoring/mock_data.js26
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js10
-rw-r--r--spec/frontend/monitoring/pages/panel_new_page_spec.js4
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js11
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js222
-rw-r--r--spec/frontend/nav/components/responsive_app_spec.js4
-rw-r--r--spec/frontend/nav/components/responsive_header_spec.js6
-rw-r--r--spec/frontend/nav/components/responsive_home_spec.js6
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_container_view_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_menu_sections_spec.js4
-rw-r--r--spec/frontend/nav/components/top_nav_new_dropdown_spec.js29
-rw-r--r--spec/frontend/new_branch_spec.js7
-rw-r--r--spec/frontend/notebook/cells/code_spec.js4
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js6
-rw-r--r--spec/frontend/notebook/cells/output/dataframe_spec.js59
-rw-r--r--spec/frontend/notebook/cells/output/dataframe_util_spec.js133
-rw-r--r--spec/frontend/notebook/cells/output/error_spec.js48
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js22
-rw-r--r--spec/frontend/notebook/cells/prompt_spec.js4
-rw-r--r--spec/frontend/notebook/index_spec.js6
-rw-r--r--spec/frontend/notebook/mock_data.js102
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js154
-rw-r--r--spec/frontend/notes/components/comment_type_dropdown_spec.js4
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js11
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js3
-rw-r--r--spec/frontend/notes/components/discussion_filter_note_spec.js5
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js133
-rw-r--r--spec/frontend/notes/components/discussion_navigator_spec.js10
-rw-r--r--spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js5
-rw-r--r--spec/frontend/notes/components/discussion_reply_placeholder_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_resolve_button_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js4
-rw-r--r--spec/frontend/notes/components/email_participants_warning_spec.js5
-rw-r--r--spec/frontend/notes/components/mr_discussion_filter_spec.js110
-rw-r--r--spec/frontend/notes/components/note_actions/reply_button_spec.js5
-rw-r--r--spec/frontend/notes/components/note_actions/timeline_event_button_spec.js6
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js42
-rw-r--r--spec/frontend/notes/components/note_attachment_spec.js5
-rw-r--r--spec/frontend/notes/components/note_awards_list_spec.js236
-rw-r--r--spec/frontend/notes/components/note_body_spec.js12
-rw-r--r--spec/frontend/notes/components/note_edited_text_spec.js69
-rw-r--r--spec/frontend/notes/components/note_form_spec.js230
-rw-r--r--spec/frontend/notes/components/note_header_spec.js33
-rw-r--r--spec/frontend/notes/components/note_signed_out_widget_spec.js4
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js15
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js64
-rw-r--r--spec/frontend/notes/components/notes_activity_header_spec.js4
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js19
-rw-r--r--spec/frontend/notes/components/timeline_toggle_spec.js4
-rw-r--r--spec/frontend/notes/components/toggle_replies_widget_spec.js4
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js12
-rw-r--r--spec/frontend/notes/stores/actions_spec.js32
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js54
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js4
-rw-r--r--spec/frontend/oauth_application/components/oauth_secret_spec.js116
-rw-r--r--spec/frontend/oauth_remember_me_spec.js14
-rw-r--r--spec/frontend/observability/index_spec.js64
-rw-r--r--spec/frontend/observability/observability_app_spec.js144
-rw-r--r--spec/frontend/observability/skeleton_spec.js15
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/delete_modal_spec.js (renamed from spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js)24
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js48
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js45
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js313
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js22
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js11
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js4
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js124
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js77
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/stubs.js2
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js19
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js84
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js35
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js55
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/mock_data.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js35
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js92
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/maven_installation_spec.js.snap3
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap199
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js236
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js17
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js34
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap12
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js18
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js53
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js83
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js218
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js98
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js10
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js1
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js7
-rw-r--r--spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap29
-rw-r--r--spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js32
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_path_spec.js11
-rw-r--r--spec/frontend/packages_and_registries/shared/components/package_tags_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/shared/components/publish_method_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_list_spec.js23
-rw-r--r--spec/frontend/packages_and_registries/shared/components/settings_block_spec.js4
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js6
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js7
-rw-r--r--spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js6
-rw-r--r--spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js (renamed from spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js)8
-rw-r--r--spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js (renamed from spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js)10
-rw-r--r--spec/frontend/pages/admin/jobs/components/jobs_skeleton_loader_spec.js28
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js394
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js32
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js64
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js106
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js4
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js5
-rw-r--r--spec/frontend/pages/groups/new/components/app_spec.js28
-rw-r--r--spec/frontend/pages/groups/new/components/create_group_description_details_spec.js4
-rw-r--r--spec/frontend/pages/import/bitbucket_server/components/bitbucket_server_status_table_spec.js9
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js10
-rw-r--r--spec/frontend/pages/import/history/components/import_error_details_spec.js11
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js14
-rw-r--r--spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js7
-rw-r--r--spec/frontend/pages/projects/forks/new/components/app_spec.js4
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js13
-rw-r--r--spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js17
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js11
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js6
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js8
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js5
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js4
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js9
-rw-r--r--spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js15
-rw-r--r--spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js6
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js5
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_content_spec.js5
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js10
-rw-r--r--spec/frontend/pdf/index_spec.js4
-rw-r--r--spec/frontend/pdf/page_spec.js4
-rw-r--r--spec/frontend/performance_bar/components/add_request_spec.js4
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js4
-rw-r--r--spec/frontend/performance_bar/components/performance_bar_app_spec.js55
-rw-r--r--spec/frontend/performance_bar/components/request_warning_spec.js4
-rw-r--r--spec/frontend/performance_bar/index_spec.js10
-rw-r--r--spec/frontend/performance_bar/services/performance_bar_service_spec.js2
-rw-r--r--spec/frontend/performance_bar/stores/performance_bar_store_spec.js8
-rw-r--r--spec/frontend/persistent_user_callout_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js22
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/input_wrapper_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/step_nav_spec.js14
-rw-r--r--spec/frontend/pipeline_wizard/components/step_spec.js6
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js4
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/list_spec.js12
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/text_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js33
-rw-r--r--spec/frontend/pipeline_wizard/pipeline_wizard_spec.js4
-rw-r--r--spec/frontend/pipelines/components/dag/dag_annotations_spec.js9
-rw-r--r--spec/frontend/pipelines/components/dag/dag_graph_spec.js5
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js21
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js15
-rw-r--r--spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js46
-rw-r--r--spec/frontend/pipelines/components/jobs/jobs_app_spec.js10
-rw-r--r--spec/frontend/pipelines/components/jobs/utils_spec.js14
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js12
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js15
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js7
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipeline_tabs_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js5
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js109
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js5
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js1
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js175
-rw-r--r--spec/frontend/pipelines/graph/graph_view_selector_spec.js19
-rw-r--r--spec/frontend/pipelines/graph/job_group_dropdown_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js51
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js170
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js4
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js1
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js4
-rw-r--r--spec/frontend/pipelines/header_component_spec.js11
-rw-r--r--spec/frontend/pipelines/mock_data.js87
-rw-r--r--spec/frontend/pipelines/nav_controls_spec.js37
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js4
-rw-r--r--spec/frontend/pipelines/pipeline_labels_spec.js4
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_operations_spec.js77
-rw-r--r--spec/frontend/pipelines/pipeline_tabs_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_triggerer_spec.js6
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js4
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js171
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js5
-rw-r--r--spec/frontend/pipelines/pipelines_manual_actions_spec.js216
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js33
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/test_case_details_spec.js5
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js4
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js4
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_status_token_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js5
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js5
-rw-r--r--spec/frontend/popovers/components/popovers_spec.js5
-rw-r--r--spec/frontend/profile/account/components/delete_account_modal_spec.js6
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js52
-rw-r--r--spec/frontend/profile/components/activity_calendar_spec.js120
-rw-r--r--spec/frontend/profile/components/followers_tab_spec.js21
-rw-r--r--spec/frontend/profile/components/following_tab_spec.js21
-rw-r--r--spec/frontend/profile/components/overview_tab_spec.js60
-rw-r--r--spec/frontend/profile/components/profile_tabs_spec.js57
-rw-r--r--spec/frontend/profile/components/user_achievements_spec.js122
-rw-r--r--spec/frontend/profile/mock_data.js22
-rw-r--r--spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js5
-rw-r--r--spec/frontend/profile/preferences/components/diffs_colors_spec.js14
-rw-r--r--spec/frontend/profile/preferences/components/integration_view_spec.js5
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js9
-rw-r--r--spec/frontend/profile/utils_spec.js15
-rw-r--r--spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js4
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js10
-rw-r--r--spec/frontend/projects/commit/components/commit_options_dropdown_spec.js2
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js77
-rw-r--r--spec/frontend/projects/commit/components/projects_dropdown_spec.js15
-rw-r--r--spec/frontend/projects/commit/store/actions_spec.js6
-rw-r--r--spec/frontend/projects/commit_box/info/init_details_button_spec.js47
-rw-r--r--spec/frontend/projects/commit_box/info/load_branches_spec.js10
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js52
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js8
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js5
-rw-r--r--spec/frontend/projects/compare/components/repo_dropdown_spec.js5
-rw-r--r--spec/frontend/projects/compare/components/revision_card_spec.js5
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js51
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js48
-rw-r--r--spec/frontend/projects/components/project_delete_button_spec.js5
-rw-r--r--spec/frontend/projects/components/shared/delete_button_spec.js7
-rw-r--r--spec/frontend/projects/details/upload_button_spec.js4
-rw-r--r--spec/frontend/projects/new/components/app_spec.js46
-rw-r--r--spec/frontend/projects/new/components/deployment_target_select_spec.js3
-rw-r--r--spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js1
-rw-r--r--spec/frontend/projects/new/components/new_project_url_select_spec.js23
-rw-r--r--spec/frontend/projects/pipelines/charts/components/__snapshots__/ci_cd_analytics_area_chart_spec.js.snap30
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js4
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js5
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js7
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js5
-rw-r--r--spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js4
-rw-r--r--spec/frontend/projects/prune_unreachable_objects_button_spec.js7
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js8
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js48
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js2
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js2
-rw-r--r--spec/frontend/projects/settings/components/default_branch_selector_spec.js5
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js20
-rw-r--r--spec/frontend/projects/settings/components/shared_runners_toggle_spec.js20
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js12
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js6
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js4
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js2
-rw-r--r--spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js3
-rw-r--r--spec/frontend/projects/settings/utils_spec.js24
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js5
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js6
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js6
-rw-r--r--spec/frontend/projects/terraform_notification/terraform_notification_spec.js4
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js6
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js7
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js8
-rw-r--r--spec/frontend/read_more_spec.js9
-rw-r--r--spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap80
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js81
-rw-r--r--spec/frontend/ref/stores/actions_spec.js7
-rw-r--r--spec/frontend/ref/stores/mutations_spec.js10
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap8
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js30
-rw-r--r--spec/frontend/releases/components/app_index_spec.js29
-rw-r--r--spec/frontend/releases/components/app_show_spec.js15
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js5
-rw-r--r--spec/frontend/releases/components/confirm_delete_modal_spec.js4
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js4
-rw-r--r--spec/frontend/releases/components/issuable_stats_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js7
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_header_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js5
-rw-r--r--spec/frontend/releases/components/release_block_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js4
-rw-r--r--spec/frontend/releases/components/releases_sort_spec.js7
-rw-r--r--spec/frontend/releases/components/tag_create_spec.js107
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js5
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js231
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js5
-rw-r--r--spec/frontend/releases/components/tag_search_spec.js144
-rw-r--r--spec/frontend/releases/release_notification_service_spec.js107
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js35
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js108
-rw-r--r--spec/frontend/repository/commits_service_spec.js4
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap2
-rw-r--r--spec/frontend/repository/components/blob_button_group_spec.js38
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js10
-rw-r--r--spec/frontend/repository/components/blob_controls_spec.js2
-rw-r--r--spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js2
-rw-r--r--spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js22
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js107
-rw-r--r--spec/frontend/repository/components/delete_blob_modal_spec.js16
-rw-r--r--spec/frontend/repository/components/directory_download_links_spec.js4
-rw-r--r--spec/frontend/repository/components/fork_info_spec.js292
-rw-r--r--spec/frontend/repository/components/fork_suggestion_spec.js2
-rw-r--r--spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js46
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js57
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js14
-rw-r--r--spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap42
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js95
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap3
-rw-r--r--spec/frontend/repository/components/table/index_spec.js4
-rw-r--r--spec/frontend/repository/components/table/parent_row_spec.js4
-rw-r--r--spec/frontend/repository/components/table/row_spec.js293
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js269
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js52
-rw-r--r--spec/frontend/repository/mixins/highlight_mixin_spec.js2
-rw-r--r--spec/frontend/repository/mock_data.js72
-rw-r--r--spec/frontend/repository/pages/blob_spec.js4
-rw-r--r--spec/frontend/repository/pages/index_spec.js2
-rw-r--r--spec/frontend/repository/pages/tree_spec.js2
-rw-r--r--spec/frontend/right_sidebar_spec.js6
-rw-r--r--spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap21
-rw-r--r--spec/frontend/saved_replies/components/list_item_spec.js22
-rw-r--r--spec/frontend/saved_replies/components/list_spec.js68
-rw-r--r--spec/frontend/scripts/frontend/__fixtures__/locale/de/converted.json21
-rw-r--r--spec/frontend/scripts/frontend/__fixtures__/locale/de/gitlab.po13
-rw-r--r--spec/frontend/scripts/frontend/po_to_json_spec.js244
-rw-r--r--spec/frontend/search/highlight_blob_search_result_spec.js6
-rw-r--r--spec/frontend/search/mock_data.js307
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js36
-rw-r--r--spec/frontend/search/sidebar/components/checkbox_filter_spec.js59
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js35
-rw-r--r--spec/frontend/search/sidebar/components/filters_spec.js23
-rw-r--r--spec/frontend/search/sidebar/components/language_filter_spec.js (renamed from spec/frontend/search/sidebar/components/language_filters_spec.js)82
-rw-r--r--spec/frontend/search/sidebar/components/radio_filter_spec.js10
-rw-r--r--spec/frontend/search/sidebar/components/scope_navigation_spec.js57
-rw-r--r--spec/frontend/search/sidebar/components/scope_new_navigation_spec.js83
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js35
-rw-r--r--spec/frontend/search/sort/components/app_spec.js5
-rw-r--r--spec/frontend/search/store/actions_spec.js48
-rw-r--r--spec/frontend/search/store/getters_spec.js55
-rw-r--r--spec/frontend/search/store/utils_spec.js50
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/group_filter_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/searchable_dropdown_spec.js49
-rw-r--r--spec/frontend/search_autocomplete_spec.js293
-rw-r--r--spec/frontend/search_autocomplete_utils_spec.js114
-rw-r--r--spec/frontend/search_settings/components/search_settings_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js160
-rw-r--r--spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js4
-rw-r--r--spec/frontend/security_configuration/components/feature_card_spec.js107
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js7
-rw-r--r--spec/frontend/security_configuration/components/upgrade_banner_spec.js107
-rw-r--r--spec/frontend/security_configuration/constants.js1
-rw-r--r--spec/frontend/security_configuration/mock_data.js32
-rw-r--r--spec/frontend/security_configuration/utils_spec.js38
-rw-r--r--spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap85
-rw-r--r--spec/frontend/self_monitor/components/self_monitor_form_spec.js95
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js254
-rw-r--r--spec/frontend/self_monitor/store/mutations_spec.js64
-rw-r--r--spec/frontend/sentry/index_spec.js52
-rw-r--r--spec/frontend/sentry/legacy_index_spec.js7
-rw-r--r--spec/frontend/sentry/sentry_browser_wrapper_spec.js2
-rw-r--r--spec/frontend/sentry/sentry_config_spec.js7
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js58
-rw-r--r--spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js4
-rw-r--r--spec/frontend/settings_panels_spec.js5
-rw-r--r--spec/frontend/shortcuts_spec.js135
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js7
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_title_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js1
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js3
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js11
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js13
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js8
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js4
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js12
-rw-r--r--spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js7
-rw-r--r--spec/frontend/sidebar/components/copy/copyable_field_spec.js4
-rw-r--r--spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js8
-rw-r--r--spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js5
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js42
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js4
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js4
-rw-r--r--spec/frontend/sidebar/components/incidents/escalation_status_spec.js4
-rw-r--r--spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js101
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js354
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js18
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js13
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js8
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view_spec.js40
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js16
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js32
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_footer_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js8
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js4
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js22
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js11
-rw-r--r--spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js15
-rw-r--r--spec/frontend/sidebar/components/lock/edit_form_spec.js5
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js28
-rw-r--r--spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js267
-rw-r--r--spec/frontend/sidebar/components/move/move_issue_button_spec.js10
-rw-r--r--spec/frontend/sidebar/components/move/move_issues_button_spec.js27
-rw-r--r--spec/frontend/sidebar/components/participants/participants_spec.js197
-rw-r--r--spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js1
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js5
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewers_spec.js4
-rw-r--r--spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js3
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js153
-rw-r--r--spec/frontend/sidebar/components/severity/severity_spec.js7
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js (renamed from spec/frontend/sidebar/components/severity/sidebar_severity_spec.js)97
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js37
-rw-r--r--spec/frontend/sidebar/components/status/status_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js7
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js66
-rw-r--r--spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js4
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js5
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js8
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js7
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js1
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_spec.js4
-rw-r--r--spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js6
-rw-r--r--spec/frontend/sidebar/mock_data.js12
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js2
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap7
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/edit_spec.js31
-rw-r--r--spec/frontend/snippets/components/embed_dropdown_spec.js5
-rw-r--r--spec/frontend/snippets/components/show_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js5
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js8
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js222
-rw-r--r--spec/frontend/snippets/components/snippet_description_edit_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_description_view_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js138
-rw-r--r--spec/frontend/snippets/components/snippet_title_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js4
-rw-r--r--spec/frontend/snippets/mock_data.js19
-rw-r--r--spec/frontend/streaming/chunk_writer_spec.js214
-rw-r--r--spec/frontend/streaming/handle_streamed_anchor_link_spec.js132
-rw-r--r--spec/frontend/streaming/html_stream_spec.js46
-rw-r--r--spec/frontend/streaming/rate_limit_stream_requests_spec.js155
-rw-r--r--spec/frontend/streaming/render_balancer_spec.js69
-rw-r--r--spec/frontend/streaming/render_html_streams_spec.js96
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_spec.js309
-rw-r--r--spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js50
-rw-r--r--spec/frontend/super_sidebar/components/counter_spec.js11
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js69
-rw-r--r--spec/frontend/super_sidebar/components/frequent_items_list_spec.js79
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js128
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js75
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js91
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js372
-rw-r--r--spec/frontend/super_sidebar/components/global_search/mock_data.js456
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/actions_spec.js111
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/getters_spec.js334
-rw-r--r--spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js63
-rw-r--r--spec/frontend/super_sidebar/components/global_search/utils_spec.js60
-rw-r--r--spec/frontend/super_sidebar/components/groups_list_spec.js90
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js163
-rw-r--r--spec/frontend/super_sidebar/components/items_list_spec.js101
-rw-r--r--spec/frontend/super_sidebar/components/menu_section_spec.js102
-rw-r--r--spec/frontend/super_sidebar/components/merge_request_menu_spec.js42
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_link_spec.js37
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_router_link_spec.js56
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js156
-rw-r--r--spec/frontend/super_sidebar/components/pinned_section_spec.js75
-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_menu_spec.js184
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js207
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_portal_spec.js68
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js224
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js106
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js180
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js502
-rw-r--r--spec/frontend/super_sidebar/components/user_name_group_spec.js114
-rw-r--r--spec/frontend/super_sidebar/mock_data.js224
-rw-r--r--spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js139
-rw-r--r--spec/frontend/super_sidebar/user_counts_manager_spec.js166
-rw-r--r--spec/frontend/super_sidebar/utils_spec.js171
-rw-r--r--spec/frontend/surveys/merge_request_performance/app_spec.js28
-rw-r--r--spec/frontend/syntax_highlight_spec.js6
-rw-r--r--spec/frontend/tags/components/delete_tag_modal_spec.js6
-rw-r--r--spec/frontend/tags/components/sort_dropdown_spec.js6
-rw-r--r--spec/frontend/terms/components/app_spec.js5
-rw-r--r--spec/frontend/terraform/components/empty_state_spec.js27
-rw-r--r--spec/frontend/terraform/components/init_command_modal_spec.js74
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js10
-rw-r--r--spec/frontend/terraform/components/states_table_spec.js7
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js13
-rw-r--r--spec/frontend/test_setup.js13
-rw-r--r--spec/frontend/time_tracking/components/timelog_source_cell_spec.js136
-rw-r--r--spec/frontend/time_tracking/components/timelogs_app_spec.js238
-rw-r--r--spec/frontend/time_tracking/components/timelogs_table_spec.js223
-rw-r--r--spec/frontend/toggles/index_spec.js3
-rw-r--r--spec/frontend/token_access/inbound_token_access_spec.js4
-rw-r--r--spec/frontend/token_access/mock_data.js34
-rw-r--r--spec/frontend/token_access/opt_in_jwt_spec.js144
-rw-r--r--spec/frontend/token_access/outbound_token_access_spec.js72
-rw-r--r--spec/frontend/token_access/token_access_app_spec.js20
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js38
-rw-r--r--spec/frontend/tooltips/components/tooltips_spec.js5
-rw-r--r--spec/frontend/tracking/tracking_initialization_spec.js22
-rw-r--r--spec/frontend/tracking/tracking_spec.js91
-rw-r--r--spec/frontend/usage_quotas/components/usage_quotas_app_spec.js4
-rw-r--r--spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js4
-rw-r--r--spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js52
-rw-r--r--spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js5
-rw-r--r--spec/frontend/usage_quotas/storage/components/usage_graph_spec.js28
-rw-r--r--spec/frontend/usage_quotas/storage/mock_data.js26
-rw-r--r--spec/frontend/user_lists/components/edit_user_list_spec.js6
-rw-r--r--spec/frontend/user_lists/components/new_user_list_spec.js4
-rw-r--r--spec/frontend/user_lists/components/user_lists_spec.js7
-rw-r--r--spec/frontend/user_lists/components/user_lists_table_spec.js4
-rw-r--r--spec/frontend/user_lists/store/edit/actions_spec.js4
-rw-r--r--spec/frontend/user_lists/store/new/actions_spec.js4
-rw-r--r--spec/frontend/user_popovers_spec.js26
-rw-r--r--spec/frontend/validators/length_validator_spec.js91
-rw-r--r--spec/frontend/vue3migration/compiler_spec.js38
-rw-r--r--spec/frontend/vue3migration/components/comments_on_root_level.vue5
-rw-r--r--spec/frontend/vue3migration/components/default_slot_with_comment.vue18
-rw-r--r--spec/frontend/vue3migration/components/key_inside_template.vue7
-rw-r--r--spec/frontend/vue3migration/components/simple.vue10
-rw-r--r--spec/frontend/vue3migration/components/slot_with_comment.vue20
-rw-r--r--spec/frontend/vue3migration/components/slots_with_same_name.vue14
-rw-r--r--spec/frontend/vue3migration/components/v_once_inside_v_if.vue12
-rw-r--r--spec/frontend/vue_compat_test_setup.js141
-rw-r--r--spec/frontend/vue_merge_request_widget/components/action_buttons.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js257
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js48
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js245
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js353
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js9
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js10
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js16
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js62
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js44
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap82
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js26
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js10
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js8
-rw-r--r--spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js98
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js25
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js14
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js5
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js189
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js2
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js10
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js4
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_metrics_spec.js63
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js6
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js7
-rw-r--r--spec/frontend/vue_shared/alert_details/router_spec.js35
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js157
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js3
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js6
-rw-r--r--spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap40
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/alert_details_table_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/chronic_duration_input_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/code_block_highlighted_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/code_block_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/confirm_fork_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/confirm_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/content_transition_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/utils_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js250
-rw-r--r--spec/frontend/vue_shared/components/dismissible_alert_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/dom_element_listener_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/ensure_data_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/entity_select/project_select_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/expand_button_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/file_finder/index_spec.js250
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/file_row_header_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/file_tree_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js98
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js89
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js140
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js111
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js129
-rw-r--r--spec/frontend/vue_shared/components/form/__snapshots__/form_footer_actions_spec.js.snap14
-rw-r--r--spec/frontend/vue_shared/components/form/form_footer_actions_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/form/title_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/integration_help_text_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/keep_alive_slots_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/local_storage_sync_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js66
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js37
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js70
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_view_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js84
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js310
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestions_spec.js54
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/memory_graph_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/metric_images/store/actions_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/navigation_tabs_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/notes/__snapshots__/placeholder_note_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/notes/noteable_warning_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/ordered_layout_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/page_size_selector_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_list_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/pagination_bar/pagination_bar_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/pagination_links_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/panel_resizer_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/papa_parse_alert_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/project_avatar_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js136
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js266
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap32
-rw-r--r--spec/frontend/vue_shared/components/registry/__snapshots__/history_item_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/components/registry/code_instruction_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/registry/details_row_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/history_item_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/metadata_item_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/registry/registry_search_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/registry/title_area_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap23
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/__snapshots__/skeleton_loader_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_docker_instructions_spec.js.snap3
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_kubernetes_instructions_spec.js.snap3
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js81
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/security_reports/help_icon_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/security_reports/security_summary_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/segmented_control_button_group_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/settings/settings_block_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/slot_switch_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/smart_virtual_list_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/source_editor_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/stacked_progress_bar_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/time_ago_tooltip_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/truncated_text/truncated_text_spec.js113
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/__snapshots__/upload_dropzone_spec.js.snap14
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/url_sync_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/user_callout_dismisser_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/vuex_module_provider_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js158
-rw-r--r--spec/frontend/vue_shared/directives/track_event_spec.js61
-rw-r--r--spec/frontend/vue_shared/directives/validation_spec.js5
-rw-r--r--spec/frontend/vue_shared/issuable/__snapshots__/issuable_blocked_icon_spec.js.snap2
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js37
-rw-r--r--spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js32
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js8
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js8
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js1
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js6
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js149
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js7
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js24
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_title_spec.js28
-rw-r--r--spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js1
-rw-r--r--spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js2
-rw-r--r--spec/frontend/vue_shared/new_namespace/components/welcome_spec.js2
-rw-r--r--spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js33
-rw-r--r--spec/frontend/vue_shared/plugins/global_toast_spec.js22
-rw-r--r--spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js4
-rw-r--r--spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js42
-rw-r--r--spec/frontend/vue_shared/security_reports/components/security_report_download_dropdown_spec.js5
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js8
-rw-r--r--spec/frontend/webhooks/components/form_url_app_spec.js4
-rw-r--r--spec/frontend/webhooks/components/form_url_mask_item_spec.js6
-rw-r--r--spec/frontend/webhooks/components/push_events_spec.js2
-rw-r--r--spec/frontend/webhooks/components/test_dropdown_spec.js13
-rw-r--r--spec/frontend/whats_new/components/app_spec.js3
-rw-r--r--spec/frontend/whats_new/components/feature_spec.js5
-rw-r--r--spec/frontend/whats_new/utils/get_drawer_body_height_spec.js4
-rw-r--r--spec/frontend/whats_new/utils/notification_spec.js5
-rw-r--r--spec/frontend/work_items/components/app_spec.js4
-rw-r--r--spec/frontend/work_items/components/item_state_spec.js4
-rw-r--r--spec/frontend/work_items/components/item_title_spec.js6
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap2
-rw-r--r--spec/frontend/work_items/components/notes/activity_filter_spec.js74
-rw-r--r--spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js109
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js112
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js100
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js44
-rw-r--r--spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js44
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js207
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js137
-rw-r--r--spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js63
-rw-r--r--spec/frontend/work_items/components/widget_wrapper_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js235
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js37
-rw-r--r--spec/frontend/work_items/components/work_item_award_emoji_spec.js170
-rw-r--r--spec/frontend/work_items/components/work_item_created_updated_spec.js85
-rw-r--r--spec/frontend/work_items/components/work_item_description_rendered_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js56
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js137
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js213
-rw-r--r--spec/frontend/work_items/components/work_item_due_date_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js58
-rw-r--r--spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js98
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js19
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js152
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js8
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js228
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js100
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js64
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js158
-rw-r--r--spec/frontend/work_items/components/work_item_state_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_title_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_todos_spec.js97
-rw-r--r--spec/frontend/work_items/components/work_item_type_icon_spec.js6
-rw-r--r--spec/frontend/work_items/mock_data.js1248
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js25
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js6
-rw-r--r--spec/frontend/work_items/router_spec.js17
-rw-r--r--spec/frontend/work_items/utils_spec.js21
-rw-r--r--spec/frontend/work_items_hierarchy/components/app_spec.js4
-rw-r--r--spec/frontend/work_items_hierarchy/components/hierarchy_spec.js4
-rw-r--r--spec/frontend/zen_mode_spec.js37
2057 files changed, 45251 insertions, 25669 deletions
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index 45639f4c948..200f539fb3e 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -12,6 +12,7 @@ settings:
jest:
jestConfigFile: 'jest.config.js'
rules:
+ '@gitlab/vtu-no-explicit-wrapper-destroy': error
jest/expect-expect:
- off
- assertFunctionNames:
diff --git a/spec/frontend/__helpers__/assert_props.js b/spec/frontend/__helpers__/assert_props.js
new file mode 100644
index 00000000000..9935719580a
--- /dev/null
+++ b/spec/frontend/__helpers__/assert_props.js
@@ -0,0 +1,41 @@
+import { mount } from '@vue/test-utils';
+import { ErrorWithStack } from 'jest-util';
+
+function installConsoleHandler(method) {
+ const originalHandler = global.console[method];
+
+ global.console[method] = function throwableHandler(...args) {
+ if (args[0]?.includes('Invalid prop') || args[0]?.includes('Missing required prop')) {
+ throw new ErrorWithStack(
+ `Unexpected call of console.${method}() with:\n\n${args.join(', ')}`,
+ this[method],
+ );
+ }
+
+ originalHandler.apply(this, args);
+ };
+
+ return function restore() {
+ global.console[method] = originalHandler;
+ };
+}
+
+export function assertProps(Component, props, extraMountArgs = {}) {
+ const [restoreError, restoreWarn] = [
+ installConsoleHandler('error'),
+ installConsoleHandler('warn'),
+ ];
+ const ComponentWithoutRenderFn = {
+ ...Component,
+ render() {
+ return '';
+ },
+ };
+
+ try {
+ mount(ComponentWithoutRenderFn, { propsData: props, ...extraMountArgs });
+ } finally {
+ restoreError();
+ restoreWarn();
+ }
+}
diff --git a/spec/frontend/__helpers__/create_mock_source_editor_extension.js b/spec/frontend/__helpers__/create_mock_source_editor_extension.js
new file mode 100644
index 00000000000..fa529604d6f
--- /dev/null
+++ b/spec/frontend/__helpers__/create_mock_source_editor_extension.js
@@ -0,0 +1,12 @@
+export const createMockSourceEditorExtension = (ActualExtension) => {
+ const { extensionName } = ActualExtension;
+ const providedKeys = Object.keys(new ActualExtension().provides());
+
+ const mockedMethods = Object.fromEntries(providedKeys.map((key) => [key, jest.fn()]));
+ const MockExtension = function MockExtension() {};
+ MockExtension.extensionName = extensionName;
+ MockExtension.mockedMethods = mockedMethods;
+ MockExtension.prototype.provides = jest.fn().mockReturnValue(mockedMethods);
+
+ return MockExtension;
+};
diff --git a/spec/frontend/__helpers__/experimentation_helper.js b/spec/frontend/__helpers__/experimentation_helper.js
index d5044be88d7..7e8dd463d28 100644
--- a/spec/frontend/__helpers__/experimentation_helper.js
+++ b/spec/frontend/__helpers__/experimentation_helper.js
@@ -2,16 +2,9 @@ import { merge } from 'lodash';
// This helper is for specs that use `gitlab/experimentation` module
export function withGonExperiment(experimentKey, value = true) {
- let origGon;
-
beforeEach(() => {
- origGon = window.gon;
window.gon = merge({}, window.gon || {}, { experiments: { [experimentKey]: value } });
});
-
- afterEach(() => {
- window.gon = origGon;
- });
}
// The following helper is for specs that use `gitlab-experiment` utilities,
diff --git a/spec/frontend/__helpers__/fixtures.js b/spec/frontend/__helpers__/fixtures.js
index a6f7b37161e..c66411979e9 100644
--- a/spec/frontend/__helpers__/fixtures.js
+++ b/spec/frontend/__helpers__/fixtures.js
@@ -12,7 +12,10 @@ export function getFixture(relativePath) {
throw new ErrorWithStack(
`Fixture file ${relativePath} does not exist.
-Did you run bin/rake frontend:fixtures?`,
+Did you run bin/rake frontend:fixtures? You can also download fixtures from the gitlab-org/gitlab package registry.
+
+See https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html#download-fixtures for more info.
+`,
getFixture,
);
}
diff --git a/spec/frontend/__helpers__/gon_helper.js b/spec/frontend/__helpers__/gon_helper.js
new file mode 100644
index 00000000000..51d5ece5fc1
--- /dev/null
+++ b/spec/frontend/__helpers__/gon_helper.js
@@ -0,0 +1,5 @@
+export const createGon = (IS_EE) => {
+ return {
+ ee: IS_EE,
+ };
+};
diff --git a/spec/frontend/__helpers__/init_vue_mr_page_helper.js b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
index d01affdaeac..3dccbd9fbef 100644
--- a/spec/frontend/__helpers__/init_vue_mr_page_helper.js
+++ b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
@@ -6,9 +6,17 @@ import { getDiffFileMock } from '../diffs/mock_data/diff_file';
import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data';
export default function initVueMRPage() {
+ const contentWrapperEl = document.createElement('div');
+ contentWrapperEl.className = 'content-wrapper';
+ document.body.appendChild(contentWrapperEl);
+
+ const containerEl = document.createElement('div');
+ containerEl.className = 'container-fluid';
+ contentWrapperEl.appendChild(containerEl);
+
const mrTestEl = document.createElement('div');
mrTestEl.className = 'js-merge-request-test';
- document.body.appendChild(mrTestEl);
+ containerEl.appendChild(mrTestEl);
const diffsAppEndpoint = '/diffs/app/endpoint';
const diffsAppProjectPath = 'testproject';
diff --git a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
index 54d397d0997..8b6cdedfd9f 100644
--- a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
+++ b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js
@@ -12,10 +12,6 @@ describe('keepAlive', () => {
wrapper = mount(keepAlive(component));
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('converts a component to a keep-alive component', async () => {
const { element } = wrapper.findComponent(component);
diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js
index 2fe9fe89a90..0217835b2a3 100644
--- a/spec/frontend/__helpers__/shared_test_setup.js
+++ b/spec/frontend/__helpers__/shared_test_setup.js
@@ -1,10 +1,12 @@
/* Common setup for both unit and integration test environments */
+import { ReadableStream, WritableStream } from 'node:stream/web';
import * as jqueryMatchers from 'custom-jquery-matchers';
import Vue from 'vue';
import { enableAutoDestroy } from '@vue/test-utils';
import 'jquery';
import Translate from '~/vue_shared/translate';
import setWindowLocation from './set_window_location_helper';
+import { createGon } from './gon_helper';
import { setGlobalDateToFakeDate } from './fake_date';
import { TEST_HOST } from './test_constants';
import * as customMatchers from './matchers';
@@ -13,6 +15,9 @@ import './dom_shims';
import './jquery';
import '~/commons/bootstrap';
+global.ReadableStream = ReadableStream;
+global.WritableStream = WritableStream;
+
enableAutoDestroy(afterEach);
// This module has some fairly decent visual test coverage in it's own repository.
@@ -67,8 +72,13 @@ beforeEach(() => {
// eslint-disable-next-line jest/no-standalone-expect
expect.hasAssertions();
- // Reset the mocked window.location. This ensures tests don't interfere with
- // each other, and removes the need to tidy up if it was changed for a given
- // test.
+ // Reset globals: This ensures tests don't interfere with
+ // each other, and removes the need to tidy up if it was
+ // changed for a given test.
+
+ // Reset the mocked window.location
setWindowLocation(TEST_HOST);
+
+ // Reset window.gon object
+ window.gon = createGon(window.IS_EE);
});
diff --git a/spec/frontend/__helpers__/vue_mock_directive.js b/spec/frontend/__helpers__/vue_mock_directive.js
index e952f258c4d..e7a2aa7f10d 100644
--- a/spec/frontend/__helpers__/vue_mock_directive.js
+++ b/spec/frontend/__helpers__/vue_mock_directive.js
@@ -2,7 +2,7 @@ export const getKey = (name) => `$_gl_jest_${name}`;
export const getBinding = (el, name) => el[getKey(name)];
-const writeBindingToElement = (el, { name, value, arg, modifiers }) => {
+const writeBindingToElement = (el, name, { value, arg, modifiers }) => {
el[getKey(name)] = {
value,
arg,
@@ -10,16 +10,24 @@ const writeBindingToElement = (el, { name, value, arg, modifiers }) => {
};
};
-export const createMockDirective = () => ({
- bind(el, binding) {
- writeBindingToElement(el, binding);
- },
+export const createMockDirective = (name) => {
+ if (!name) {
+ throw new Error(
+ 'Vue 3 no longer passes the name of the directive to its hooks, an explicit name is required',
+ );
+ }
- update(el, binding) {
- writeBindingToElement(el, binding);
- },
+ return {
+ bind(el, binding) {
+ writeBindingToElement(el, name, binding);
+ },
- unbind(el, { name }) {
- delete el[getKey(name)];
- },
-});
+ update(el, binding) {
+ writeBindingToElement(el, name, binding);
+ },
+
+ unbind(el) {
+ delete el[getKey(name)];
+ },
+ };
+};
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js
index 75bd5df8cbf..c144a256dce 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper.js
@@ -83,6 +83,24 @@ export const extendedWrapper = (wrapper) => {
return this.findAll(`[data-testid="${id}"]`);
},
},
+ /*
+ * Keep in mind that there are some limitations when using `findComponent`
+ * with CSS selectors: https://v1.test-utils.vuejs.org/api/wrapper/#findcomponent
+ */
+ findComponentByTestId: {
+ value(id) {
+ return this.findComponent(`[data-testid="${id}"]`);
+ },
+ },
+ /*
+ * Keep in mind that there are some limitations when using `findAllComponents`
+ * with CSS selectors: https://v1.test-utils.vuejs.org/api/wrapper/#findallcomponents
+ */
+ findAllComponentsByTestId: {
+ value(id) {
+ return this.findAllComponents(`[data-testid="${id}"]`);
+ },
+ },
// `findBy`
...AVAILABLE_QUERIES.reduce((accumulator, query) => {
return {
diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
index 466333f8a89..2f69a2348d9 100644
--- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
+++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js
@@ -128,6 +128,55 @@ describe('Vue test utils helpers', () => {
});
});
+ describe('findComponentByTestId', () => {
+ const testId = 'a-component';
+ let mockChild;
+ let mockComponent;
+
+ beforeEach(() => {
+ mockChild = {
+ template: '<div></div>',
+ };
+ mockComponent = extendedWrapper(
+ shallowMount({
+ render(h) {
+ return h('div', {}, [h(mockChild, { attrs: { 'data-testid': testId } })]);
+ },
+ }),
+ );
+ });
+
+ it('should find the element by test id', () => {
+ expect(mockComponent.findComponentByTestId(testId).exists()).toBe(true);
+ });
+ });
+
+ describe('findAllComponentsByTestId', () => {
+ const testId = 'a-component';
+ let mockComponent;
+ let mockChild;
+
+ beforeEach(() => {
+ mockChild = {
+ template: `<div></div>`,
+ };
+ mockComponent = extendedWrapper(
+ shallowMount({
+ render(h) {
+ return h('div', [
+ h(mockChild, { attrs: { 'data-testid': testId } }),
+ h(mockChild, { attrs: { 'data-testid': testId } }),
+ ]);
+ },
+ }),
+ );
+ });
+
+ it('should find all components by test id', () => {
+ expect(mockComponent.findAllComponentsByTestId(testId)).toHaveLength(2);
+ });
+ });
+
describe.each`
findMethod | expectedQuery
${'findByRole'} | ${'queryAllByRole'}
diff --git a/spec/frontend/__helpers__/vuex_action_helper.js b/spec/frontend/__helpers__/vuex_action_helper.js
index bdd5a0a9034..94164814879 100644
--- a/spec/frontend/__helpers__/vuex_action_helper.js
+++ b/spec/frontend/__helpers__/vuex_action_helper.js
@@ -78,6 +78,8 @@ export default (
}
actions.push(dispatchedAction);
+
+ return Promise.resolve();
};
const validateResults = () => {
diff --git a/spec/frontend/__helpers__/vuex_action_helper_spec.js b/spec/frontend/__helpers__/vuex_action_helper_spec.js
index 4bd21ff150a..64081ca11a3 100644
--- a/spec/frontend/__helpers__/vuex_action_helper_spec.js
+++ b/spec/frontend/__helpers__/vuex_action_helper_spec.js
@@ -83,6 +83,20 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
});
});
+ describe('given an async action (chaining off a dispatch)', () => {
+ it('mocks dispatch accurately', () => {
+ const asyncAction = ({ commit, dispatch }) => {
+ return dispatch('ACTION').then(() => {
+ commit('MUTATION');
+ });
+ };
+
+ assertion = { actions: [{ type: 'ACTION' }], mutations: [{ type: 'MUTATION' }] };
+
+ return testAction(asyncAction, null, {}, assertion.mutations, assertion.actions);
+ });
+ });
+
describe('given an async action (returning a promise)', () => {
const data = { FOO: 'BAR' };
diff --git a/spec/frontend/__helpers__/wait_for_text.js b/spec/frontend/__helpers__/wait_for_text.js
index 6bed8a90a98..991adc5d6c0 100644
--- a/spec/frontend/__helpers__/wait_for_text.js
+++ b/spec/frontend/__helpers__/wait_for_text.js
@@ -1,3 +1,3 @@
import { findByText } from '@testing-library/dom';
-export const waitForText = async (text, container = document) => findByText(container, text);
+export const waitForText = (text, container = document) => findByText(container, text);
diff --git a/spec/frontend/__mocks__/@gitlab/ui.js b/spec/frontend/__mocks__/@gitlab/ui.js
index 4d893bcd0bd..c51f37db384 100644
--- a/spec/frontend/__mocks__/@gitlab/ui.js
+++ b/spec/frontend/__mocks__/@gitlab/ui.js
@@ -13,13 +13,18 @@ export * from '@gitlab/ui';
* are imported internally in `@gitlab/ui`.
*/
-jest.mock('@gitlab/ui/dist/directives/tooltip.js', () => ({
+/* eslint-disable global-require */
+
+jest.mock('@gitlab/ui/src/directives/tooltip.js', () => ({
GlTooltipDirective: {
bind() {},
},
}));
+jest.mock('@gitlab/ui/dist/directives/tooltip.js', () =>
+ require('@gitlab/ui/src/directives/tooltip'),
+);
-jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
+jest.mock('@gitlab/ui/src/components/base/tooltip/tooltip.vue', () => ({
props: ['target', 'id', 'triggers', 'placement', 'container', 'boundary', 'disabled', 'show'],
render(h) {
return h(
@@ -33,7 +38,11 @@ jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () => ({
},
}));
-jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
+jest.mock('@gitlab/ui/dist/components/base/tooltip/tooltip.js', () =>
+ require('@gitlab/ui/src/components/base/tooltip/tooltip.vue'),
+);
+
+jest.mock('@gitlab/ui/src/components/base/popover/popover.vue', () => ({
props: {
cssClasses: {
type: Array,
@@ -65,3 +74,6 @@ jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () => ({
);
},
}));
+jest.mock('@gitlab/ui/dist/components/base/popover/popover.js', () =>
+ require('@gitlab/ui/src/components/base/popover/popover.vue'),
+);
diff --git a/spec/frontend/__mocks__/file_mock.js b/spec/frontend/__mocks__/file_mock.js
index 08d725cd4e4..487d1d69de2 100644
--- a/spec/frontend/__mocks__/file_mock.js
+++ b/spec/frontend/__mocks__/file_mock.js
@@ -1 +1 @@
-export default '';
+export default 'file-mock';
diff --git a/spec/frontend/__mocks__/lodash/debounce.js b/spec/frontend/__mocks__/lodash/debounce.js
index d4fe2ce5406..15f806fc31a 100644
--- a/spec/frontend/__mocks__/lodash/debounce.js
+++ b/spec/frontend/__mocks__/lodash/debounce.js
@@ -9,9 +9,22 @@
// Further reference: https://github.com/facebook/jest/issues/3465
export default (fn) => {
- const debouncedFn = jest.fn().mockImplementation(fn);
- debouncedFn.cancel = jest.fn();
- debouncedFn.flush = jest.fn().mockImplementation(() => {
+ let id;
+ const debouncedFn = jest.fn(function run(...args) {
+ // this is calculated in runtime so beforeAll hook works in tests
+ const timeout = global.JEST_DEBOUNCE_THROTTLE_TIMEOUT;
+ if (timeout) {
+ id = setTimeout(() => {
+ fn.apply(this, args);
+ }, timeout);
+ } else {
+ fn.apply(this, args);
+ }
+ });
+ debouncedFn.cancel = jest.fn(() => {
+ clearTimeout(id);
+ });
+ debouncedFn.flush = jest.fn(() => {
const errorMessage =
"The .flush() method returned by lodash.debounce is not yet implemented/mocked by the mock in 'spec/frontend/__mocks__/lodash/debounce.js'.";
diff --git a/spec/frontend/__mocks__/lodash/throttle.js b/spec/frontend/__mocks__/lodash/throttle.js
index e8a82654c78..b1014662918 100644
--- a/spec/frontend/__mocks__/lodash/throttle.js
+++ b/spec/frontend/__mocks__/lodash/throttle.js
@@ -1,4 +1,4 @@
// Similar to `lodash/debounce`, `lodash/throttle` also causes flaky specs.
// See `./debounce.js` for more details.
-export default (fn) => fn;
+export { default } from './debounce';
diff --git a/spec/frontend/__mocks__/mousetrap/index.js b/spec/frontend/__mocks__/mousetrap/index.js
deleted file mode 100644
index 63c92fa9a09..00000000000
--- a/spec/frontend/__mocks__/mousetrap/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/* global Mousetrap */
-// `mousetrap` uses amd which webpack understands but Jest does not
-// Thankfully it also writes to a global export so we can es6-ify it
-import 'mousetrap';
-
-export default Mousetrap;
diff --git a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
index ec20088c443..5de5f495f01 100644
--- a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
+++ b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
@@ -33,10 +33,6 @@ describe('AbuseCategorySelector', () => {
createComponent({ showDrawer: true });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findTitle = () => wrapper.findByTestId('category-drawer-title');
diff --git a/spec/frontend/access_tokens/components/expires_at_field_spec.js b/spec/frontend/access_tokens/components/expires_at_field_spec.js
index 491d2a0e323..6605faadc17 100644
--- a/spec/frontend/access_tokens/components/expires_at_field_spec.js
+++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js
@@ -25,10 +25,6 @@ describe('~/access_tokens/components/expires_at_field', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render datepicker with input info', () => {
createComponent();
diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
index e4313bdfa26..fb92cc34ce9 100644
--- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -4,12 +4,12 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from '~/access_tokens/components/constants';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import { __, sprintf } from '~/locale';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('~/access_tokens/components/new_access_token_app', () => {
let wrapper;
@@ -52,7 +52,6 @@ describe('~/access_tokens/components/new_access_token_app', () => {
afterEach(() => {
resetHTMLFixture();
- wrapper.destroy();
createAlert.mockClear();
});
diff --git a/spec/frontend/access_tokens/components/token_spec.js b/spec/frontend/access_tokens/components/token_spec.js
index 1af21aaa8cd..f62f7d72e3b 100644
--- a/spec/frontend/access_tokens/components/token_spec.js
+++ b/spec/frontend/access_tokens/components/token_spec.js
@@ -23,10 +23,6 @@ describe('Token', () => {
wrapper = mountExtended(Token, { propsData: defaultPropsData, slots: defaultSlots });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders title slot', () => {
createComponent();
diff --git a/spec/frontend/access_tokens/components/tokens_app_spec.js b/spec/frontend/access_tokens/components/tokens_app_spec.js
index d7acfbb47eb..6e7dee6a2cc 100644
--- a/spec/frontend/access_tokens/components/tokens_app_spec.js
+++ b/spec/frontend/access_tokens/components/tokens_app_spec.js
@@ -54,10 +54,6 @@ describe('TokensApp', () => {
expect(container.props()).toMatchObject(expectedProps);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all enabled tokens', () => {
createComponent();
diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js
index 1157e44f41a..c1158e0d124 100644
--- a/spec/frontend/access_tokens/index_spec.js
+++ b/spec/frontend/access_tokens/index_spec.js
@@ -112,7 +112,7 @@ describe('access tokens', () => {
);
});
- it('mounts component and sets `inputAttrs` prop', async () => {
+ it('mounts component and sets `inputAttrs` prop', () => {
wrapper = createWrapper(initExpiresAtField());
const component = wrapper.findComponent(ExpiresAtField);
diff --git a/spec/frontend/activities_spec.js b/spec/frontend/activities_spec.js
index ebace21217a..e39aae45ce8 100644
--- a/spec/frontend/activities_spec.js
+++ b/spec/frontend/activities_spec.js
@@ -1,13 +1,13 @@
/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow */
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlEventFilter from 'test_fixtures_static/event_filter.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import Activities from '~/activities';
import Pager from '~/pager';
describe('Activities', () => {
window.gon || (window.gon = {});
- const fixtureTemplate = 'static/event_filter.html';
const filters = [
{
id: 'all',
@@ -39,7 +39,7 @@ describe('Activities', () => {
}
beforeEach(() => {
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(htmlEventFilter);
jest.spyOn(Pager, 'init').mockImplementation(() => {});
new Activities();
});
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 2c2151bfb41..ddeab3e3b62 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
@@ -6,8 +6,9 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
body-class="add-review-item pt-0"
cancel-variant="light"
dismisslabel="Close"
- modalclass=""
+ modalclass="add-review-item-modal"
modalid="add-review-item"
+ nofocusonshow="true"
ok-disabled="true"
ok-title="Save changes"
scrollable="true"
@@ -27,9 +28,14 @@ exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
<div
class="gl-mt-3"
>
- <gl-search-box-by-type-stub
+ <gl-filtered-search-stub
+ availabletokens="[object Object],[object Object],[object Object]"
+ class="flex-grow-1"
clearbuttontitle="Clear"
- placeholder="Search by commit title or SHA"
+ placeholder="Search or filter commits"
+ searchbuttonattributes="[object Object]"
+ searchinputattributes="[object Object]"
+ searchtextoptionlabel="Search for this text"
value=""
/>
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index 1d57473943b..27fe010c354 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
+import { GlModal, GlFilteredSearch } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -49,16 +49,12 @@ describe('AddContextCommitsModal', () => {
};
const findModal = () => wrapper.findComponent(GlModal);
- const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
+ const findSearch = () => wrapper.findComponent(GlFilteredSearch);
beforeEach(() => {
wrapper = createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders modal with 2 tabs', () => {
expect(wrapper.element).toMatchSnapshot();
});
@@ -72,12 +68,29 @@ describe('AddContextCommitsModal', () => {
expect(findSearch().exists()).toBe(true);
});
- it('when user starts entering text in search box, it calls action "searchCommits" after waiting for 500s', () => {
- const searchText = 'abcd';
- findSearch().vm.$emit('input', searchText);
- expect(searchCommits).not.toHaveBeenCalled();
- jest.advanceTimersByTime(500);
- expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText);
+ it('when user submits after entering filters in search box, then it calls action "searchCommits"', () => {
+ const search = [
+ 'abcd',
+ {
+ type: 'author',
+ value: { operator: '=', data: 'abhi' },
+ },
+ {
+ type: 'committed-before-date',
+ value: { operator: '=', data: '2022-10-31' },
+ },
+ {
+ type: 'committed-after-date',
+ value: { operator: '=', data: '2022-10-28' },
+ },
+ ];
+ findSearch().vm.$emit('submit', search);
+ expect(searchCommits).toHaveBeenCalledWith(expect.anything(), {
+ searchText: 'abcd',
+ author: 'abhi',
+ committed_before: '2022-10-31',
+ committed_after: '2022-10-28',
+ });
});
it('disabled ok button when no row is selected', () => {
diff --git a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
index f679576182f..975f115c4bb 100644
--- a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
@@ -26,10 +26,6 @@ describe('ReviewTabContainer', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows loading icon when commits are being loaded', () => {
createWrapper({ isLoading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
diff --git a/spec/frontend/add_context_commits_modal/store/actions_spec.js b/spec/frontend/add_context_commits_modal/store/actions_spec.js
index 27c8d760a96..3863eee3795 100644
--- a/spec/frontend/add_context_commits_modal/store/actions_spec.js
+++ b/spec/frontend/add_context_commits_modal/store/actions_spec.js
@@ -31,10 +31,10 @@ describe('AddContextCommitsModalStoreActions', () => {
short_id: 'abcdef',
committed_date: '2020-06-12',
};
- gon.api_version = 'v4';
let mock;
beforeEach(() => {
+ gon.api_version = 'v4';
mock = new MockAdapter(axios);
});
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
new file mode 100644
index 00000000000..cabbb5e1591
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/abuse_report_app_spec.js
@@ -0,0 +1,76 @@
+import { shallowMount } from '@vue/test-utils';
+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 ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
+import HistoryItems from '~/admin/abuse_report/components/history_items.vue';
+import { mockAbuseReport } from '../mock_data';
+
+describe('AbuseReportApp', () => {
+ let wrapper;
+
+ 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, {
+ propsData: {
+ abuseReport: mockAbuseReport,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('ReportHeader', () => {
+ it('renders ReportHeader', () => {
+ expect(findReportHeader().props('user')).toBe(mockAbuseReport.user);
+ expect(findReportHeader().props('actions')).toBe(mockAbuseReport.actions);
+ });
+
+ describe('when no user is present', () => {
+ beforeEach(() => {
+ createComponent({
+ abuseReport: { ...mockAbuseReport, user: undefined },
+ });
+ });
+
+ it('does not render the ReportHeader', () => {
+ expect(findReportHeader().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('UserDetails', () => {
+ it('renders UserDetails', () => {
+ expect(findUserDetails().props('user')).toBe(mockAbuseReport.user);
+ });
+
+ describe('when no user is present', () => {
+ beforeEach(() => {
+ createComponent({
+ abuseReport: { ...mockAbuseReport, user: undefined },
+ });
+ });
+
+ it('does not render the UserDetails', () => {
+ expect(findUserDetails().exists()).toBe(false);
+ });
+ });
+ });
+
+ it('renders ReportedContent', () => {
+ expect(findReportedContent().props('report')).toBe(mockAbuseReport.report);
+ expect(findReportedContent().props('reporter')).toBe(mockAbuseReport.reporter);
+ });
+
+ it('renders HistoryItems', () => {
+ expect(findHistoryItems().props('report')).toBe(mockAbuseReport.report);
+ expect(findHistoryItems().props('reporter')).toBe(mockAbuseReport.reporter);
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/history_items_spec.js b/spec/frontend/admin/abuse_report/components/history_items_spec.js
new file mode 100644
index 00000000000..86e994fdc57
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/history_items_spec.js
@@ -0,0 +1,66 @@
+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 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', () => {
+ let wrapper;
+
+ const { report, reporter } = mockAbuseReport;
+
+ const findHistoryItem = () => wrapper.findComponent(HistoryItem);
+ const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(HistoryItems, {
+ propsData: {
+ report,
+ reporter,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the icon', () => {
+ expect(findHistoryItem().props('icon')).toBe('warning');
+ });
+
+ describe('rendering the title', () => {
+ it('renders the reporters name and the category', () => {
+ const title = sprintf(HISTORY_ITEMS_I18N.reportedByForCategory, {
+ name: reporter.name,
+ category: report.category,
+ });
+ expect(findHistoryItem().text()).toContain(title);
+ });
+
+ describe('when the reporter is not defined', () => {
+ beforeEach(() => {
+ createComponent({ 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,
+ category: report.category,
+ });
+ expect(findHistoryItem().text()).toContain(title);
+ });
+ });
+ });
+
+ it('renders the time-ago tooltip', () => {
+ expect(findTimeAgo().props('time')).toBe(report.reportedAt);
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/report_header_spec.js b/spec/frontend/admin/abuse_report/components/report_header_spec.js
new file mode 100644
index 00000000000..d584cab05b3
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/report_header_spec.js
@@ -0,0 +1,59 @@
+import { GlAvatar, GlLink, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ReportHeader from '~/admin/abuse_report/components/report_header.vue';
+import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue';
+import { REPORT_HEADER_I18N } from '~/admin/abuse_report/constants';
+import { mockAbuseReport } from '../mock_data';
+
+describe('ReportHeader', () => {
+ let wrapper;
+
+ const { user, actions } = mockAbuseReport;
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findActions = () => wrapper.findComponent(AbuseReportActions);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ReportHeader, {
+ propsData: {
+ user,
+ actions,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the users avatar', () => {
+ expect(findAvatar().props('src')).toBe(user.avatarUrl);
+ });
+
+ it('renders the users name', () => {
+ expect(wrapper.html()).toContain(user.name);
+ });
+
+ it('renders a link to the users profile page', () => {
+ const link = findLink();
+
+ expect(link.attributes('href')).toBe(user.path);
+ expect(link.text()).toBe(`@${user.username}`);
+ });
+
+ it('renders a button with a link to the users admin path', () => {
+ const button = findButton();
+
+ expect(button.attributes('href')).toBe(user.adminPath);
+ expect(button.text()).toBe(REPORT_HEADER_I18N.adminProfile);
+ });
+
+ it('renders the actions', () => {
+ const actionsComponent = findActions();
+
+ expect(actionsComponent.props('report')).toMatchObject(actions);
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/reported_content_spec.js b/spec/frontend/admin/abuse_report/components/reported_content_spec.js
new file mode 100644
index 00000000000..ecc5ad6ad47
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/reported_content_spec.js
@@ -0,0 +1,193 @@
+import { GlSprintf, GlButton, GlModal, GlCard, GlAvatar, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import ReportedContent from '~/admin/abuse_report/components/reported_content.vue';
+import TruncatedText from '~/vue_shared/components/truncated_text/truncated_text.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { REPORTED_CONTENT_I18N } from '~/admin/abuse_report/constants';
+import { mockAbuseReport } from '../mock_data';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+
+const modalId = 'abuse-report-screenshot-modal';
+
+describe('ReportedContent', () => {
+ let wrapper;
+
+ const { report, reporter } = { ...mockAbuseReport };
+
+ const findScreenshotButton = () => wrapper.findByTestId('screenshot-button');
+ const findReportUrlButton = () => wrapper.findByTestId('report-url-button');
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findCard = () => wrapper.findComponent(GlCard);
+ const findCardHeader = () => findCard().find('.js-test-card-header');
+ const findTruncatedText = () => findCardHeader().findComponent(TruncatedText);
+ const findCardBody = () => findCard().find('.js-test-card-body');
+ const findCardFooter = () => findCard().find('.js-test-card-footer');
+ const findAvatar = () => findCardFooter().findComponent(GlAvatar);
+ const findProfileLink = () => findCardFooter().findComponent(GlLink);
+ const findTimeAgo = () => findCardFooter().findComponent(TimeAgoTooltip);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(ReportedContent, {
+ propsData: {
+ report,
+ reporter,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ GlButton,
+ GlCard,
+ TruncatedText,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the reported type', () => {
+ expect(wrapper.html()).toContain(sprintf(REPORTED_CONTENT_I18N.reportTypes[report.type]));
+ });
+
+ describe('when the type is unknown', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, type: null } });
+ });
+
+ it('renders a header with a generic text content', () => {
+ expect(wrapper.html()).toContain(sprintf(REPORTED_CONTENT_I18N.reportTypes.unknown));
+ });
+ });
+
+ describe('showing the screenshot', () => {
+ describe('when the report contains a screenshot', () => {
+ it('renders a button to show the screenshot', () => {
+ expect(findScreenshotButton().text()).toBe(REPORTED_CONTENT_I18N.viewScreenshot);
+ });
+
+ it('renders a modal with the corrrect id and title', () => {
+ const modal = findModal();
+
+ expect(modal.props('title')).toBe(REPORTED_CONTENT_I18N.screenshotTitle);
+ expect(modal.props('modalId')).toBe(modalId);
+ });
+
+ it('contains an image with the screenshot', () => {
+ expect(findModal().find('img').attributes('src')).toBe(report.screenshot);
+ expect(findModal().find('img').attributes('alt')).toBe(
+ REPORTED_CONTENT_I18N.screenshotTitle,
+ );
+ });
+
+ it('opens the modal when clicking the button', async () => {
+ const modal = findModal();
+
+ expect(modal.props('visible')).toBe(false);
+
+ await findScreenshotButton().trigger('click');
+
+ expect(modal.props('visible')).toBe(true);
+ });
+ });
+
+ describe('when the report does not contain a screenshot', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, screenshot: '' } });
+ });
+
+ it('does not render a button and a modal', () => {
+ expect(findScreenshotButton().exists()).toBe(false);
+ expect(findModal().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('showing a button to open the reported URL', () => {
+ describe('when the report contains a URL', () => {
+ it('renders a button with a link to the reported URL', () => {
+ expect(findReportUrlButton().text()).toBe(
+ sprintf(REPORTED_CONTENT_I18N.goToType[report.type]),
+ );
+ });
+ });
+
+ describe('when the report type is unknown', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, type: null } });
+ });
+
+ it('renders a button with a generic text content', () => {
+ expect(findReportUrlButton().text()).toBe(sprintf(REPORTED_CONTENT_I18N.goToType.unknown));
+ });
+ });
+
+ describe('when the report contains no URL', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, url: '' } });
+ });
+
+ it('does not render a button with a link to the reported URL', () => {
+ expect(findReportUrlButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('rendering the card header', () => {
+ describe('when the report contains the reported content', () => {
+ it('renders the content', () => {
+ const dummyElement = document.createElement('div');
+ dummyElement.innerHTML = report.content;
+ expect(findTruncatedText().text()).toBe(dummyElement.textContent);
+ });
+
+ it('renders gfm', () => {
+ expect(renderGFM).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the report does not contain the reported content', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, content: '' } });
+ });
+
+ it('does not render the card header', () => {
+ expect(findCardHeader().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('rendering the card body', () => {
+ it('renders the reported by', () => {
+ expect(findCardBody().text()).toBe(REPORTED_CONTENT_I18N.reportedBy);
+ });
+ });
+
+ describe('rendering the card footer', () => {
+ it('renders the reporters avatar', () => {
+ expect(findAvatar().props('src')).toBe(reporter.avatarUrl);
+ });
+
+ it('renders the users name', () => {
+ expect(findCardFooter().text()).toContain(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}`);
+ });
+
+ it('renders the time-ago tooltip', () => {
+ expect(findTimeAgo().props('time')).toBe(report.reportedAt);
+ });
+
+ it('renders the message', () => {
+ expect(findCardFooter().text()).toContain(report.message);
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/user_detail_spec.js b/spec/frontend/admin/abuse_report/components/user_detail_spec.js
new file mode 100644
index 00000000000..d9e02bc96e2
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/user_detail_spec.js
@@ -0,0 +1,66 @@
+import { shallowMount } from '@vue/test-utils';
+import UserDetail from '~/admin/abuse_report/components/user_detail.vue';
+
+describe('UserDetail', () => {
+ let wrapper;
+
+ const label = 'user detail label';
+ const value = 'user detail value';
+
+ const createComponent = (props = {}, slots = {}) => {
+ wrapper = shallowMount(UserDetail, {
+ propsData: {
+ label,
+ value,
+ ...props,
+ },
+ slots,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('UserDetail', () => {
+ it('renders the label', () => {
+ expect(wrapper.text()).toContain(label);
+ });
+
+ describe('rendering the value', () => {
+ const slots = {
+ default: ['slot provided user detail'],
+ };
+
+ describe('when `value` property and no default slot is provided', () => {
+ it('renders the `value` as content', () => {
+ expect(wrapper.text()).toContain(value);
+ });
+ });
+
+ describe('when default slot and no `value` property is provided', () => {
+ beforeEach(() => {
+ createComponent({ label, value: null }, slots);
+ });
+
+ it('renders the content provided via the default slot', () => {
+ expect(wrapper.text()).toContain(slots.default[0]);
+ });
+ });
+
+ describe('when `value` property and default slot are both provided', () => {
+ beforeEach(() => {
+ createComponent({ label, value }, slots);
+ });
+
+ it('does not render `value` as content', () => {
+ expect(wrapper.text()).not.toContain(value);
+ });
+
+ it('renders the content provided via the default slot', () => {
+ expect(wrapper.text()).toContain(slots.default[0]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_report/components/user_details_spec.js b/spec/frontend/admin/abuse_report/components/user_details_spec.js
new file mode 100644
index 00000000000..ca499fbaa6e
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/components/user_details_spec.js
@@ -0,0 +1,210 @@
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { sprintf } from '~/locale';
+import UserDetails from '~/admin/abuse_report/components/user_details.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { USER_DETAILS_I18N } from '~/admin/abuse_report/constants';
+import { mockAbuseReport } from '../mock_data';
+
+describe('UserDetails', () => {
+ let wrapper;
+
+ const { user } = mockAbuseReport;
+
+ const findUserDetail = (attribute) => wrapper.findByTestId(attribute);
+ const findUserDetailLabel = (attribute) => findUserDetail(attribute).props('label');
+ const findUserDetailValue = (attribute) => findUserDetail(attribute).props('value');
+ const findLinkIn = (component) => component.findComponent(GlLink);
+ 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 createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(UserDetails, {
+ propsData: {
+ user,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ 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);
+ });
+ });
+
+ describe('email', () => {
+ it('renders the users email with the correct label', () => {
+ expect(findUserDetailLabel('email')).toBe(USER_DETAILS_I18N.email);
+ expect(findLinkFor('email').attributes('href')).toBe(`mailto:${user.email}`);
+ expect(findLinkFor('email').text()).toBe(user.email);
+ });
+ });
+
+ describe('plan', () => {
+ it('renders the users plan with the correct label', () => {
+ expect(findUserDetailLabel('plan')).toBe(USER_DETAILS_I18N.plan);
+ expect(findUserDetailValue('plan')).toBe(user.plan);
+ });
+ });
+
+ describe('verification', () => {
+ it('renders the users verification with the correct label', () => {
+ expect(findUserDetailLabel('verification')).toBe(USER_DETAILS_I18N.verification);
+ expect(findUserDetailValue('verification')).toBe('Email, Credit card');
+ });
+ });
+
+ describe('creditCard', () => {
+ it('renders the correct label', () => {
+ expect(findUserDetailLabel('creditCard')).toBe(USER_DETAILS_I18N.creditCard);
+ });
+
+ it('renders the users name', () => {
+ expect(findUserDetail('creditCard').text()).toContain(
+ sprintf(USER_DETAILS_I18N.registeredWith, { ...user.creditCard }),
+ );
+
+ expect(findUserDetail('creditCard').text()).toContain(user.creditCard.name);
+ });
+
+ describe('similar credit cards', () => {
+ it('renders the number of similar records', () => {
+ expect(findUserDetail('creditCard').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('creditCard').text()).toBe(
+ sprintf('%{similarRecordsCount} accounts', { ...user.creditCard }),
+ );
+
+ expect(findLinkFor('creditCard').text()).toContain(
+ user.creditCard.similarRecordsCount.toString(),
+ );
+ });
+
+ describe('when the number of similar credit cards is less than 2', () => {
+ beforeEach(() => {
+ createComponent({
+ user: { ...user, creditCard: { ...user.creditCard, similarRecordsCount: 1 } },
+ });
+ });
+
+ it('does not render the number of similar records', () => {
+ expect(findUserDetail('creditCard').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);
+ });
+ });
+ });
+
+ describe('when the users creditCard is blank', () => {
+ beforeEach(() => {
+ createComponent({
+ user: { ...user, creditCard: undefined },
+ });
+ });
+
+ it('does not render the users creditCard', () => {
+ expect(findUserDetail('creditCard').exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('otherReports', () => {
+ it('renders the correct label', () => {
+ expect(findUserDetailLabel('otherReports')).toBe(USER_DETAILS_I18N.otherReports);
+ });
+
+ describe.each(user.otherReports)('renders a line for report %#', (otherReport) => {
+ const index = user.otherReports.indexOf(otherReport);
+
+ it('renders the category', () => {
+ expect(findOtherReport(index).text()).toContain(
+ sprintf('Reported for %{category}', { ...otherReport }),
+ );
+ });
+
+ it('renders a link to the report', () => {
+ expect(findLinkIn(findOtherReport(index)).attributes('href')).toBe(otherReport.reportPath);
+ });
+
+ it('renders the time it was created', () => {
+ expect(findTimeIn(findOtherReport(index))).toBe(otherReport.createdAt);
+ });
+ });
+
+ describe('when the users otherReports is empty', () => {
+ beforeEach(() => {
+ createComponent({
+ user: { ...user, otherReports: [] },
+ });
+ });
+
+ it('does not render the users otherReports', () => {
+ expect(findUserDetail('otherReports').exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('normalLocation', () => {
+ it('renders the correct label', () => {
+ expect(findUserDetailLabel('normalLocation')).toBe(USER_DETAILS_I18N.normalLocation);
+ });
+
+ describe('when the users mostUsedIp is blank', () => {
+ it('renders the users lastSignInIp', () => {
+ expect(findUserDetailValue('normalLocation')).toBe(user.lastSignInIp);
+ });
+ });
+
+ describe('when the users mostUsedIp is not blank', () => {
+ const mostUsedIp = '127.0.0.1';
+
+ beforeEach(() => {
+ createComponent({
+ user: { ...user, mostUsedIp },
+ });
+ });
+
+ it('renders the users mostUsedIp', () => {
+ expect(findUserDetailValue('normalLocation')).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);
+ });
+ });
+
+ 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(
+ 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
new file mode 100644
index 00000000000..ee0f0967735
--- /dev/null
+++ b/spec/frontend/admin/abuse_report/mock_data.js
@@ -0,0 +1,61 @@
+export const mockAbuseReport = {
+ user: {
+ username: 'spamuser417',
+ name: 'Sp4m User',
+ createdAt: '2023-03-29T09:30:23.885Z',
+ email: 'sp4m@spam.com',
+ lastActivityOn: '2023-04-02',
+ avatarUrl: 'https://www.gravatar.com/avatar/a2579caffc69ea5d7606f9dd9d8504ba?s=80&d=identicon',
+ path: '/spamuser417',
+ adminPath: '/admin/users/spamuser417',
+ plan: 'Free',
+ verificationState: { email: true, phone: false, creditCard: true },
+ creditCard: {
+ name: 'S. User',
+ similarRecordsCount: 2,
+ cardMatchesLink: '/admin/users/spamuser417/card_match',
+ },
+ otherReports: [
+ {
+ category: 'offensive',
+ createdAt: '2023-02-28T10:09:54.982Z',
+ reportPath: '/admin/abuse_reports/29',
+ },
+ {
+ category: 'crypto',
+ createdAt: '2023-03-31T11:57:11.849Z',
+ reportPath: '/admin/abuse_reports/31',
+ },
+ ],
+ mostUsedIp: null,
+ lastSignInIp: '::1',
+ 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',
+ },
+ report: {
+ message: 'This is obvious spam',
+ reportedAt: '2023-03-29T09:39:50.502Z',
+ category: 'spam',
+ type: 'comment',
+ content:
+ '<p data-sourcepos="1:1-1:772" dir="auto">Farmers Toy Sale ON NOW | SHOP CATALOGUE ... 50% off Kids\' Underwear by Hanes ... BUY 1 GET 1 HALF PRICE on Women\'s Clothing by Whistle, Ella Clothing Farmers Toy Sale ON <a href="http://www.farmers.com" rel="nofollow noreferrer noopener" target="_blank">www.farmers.com</a> | SHOP CATALOGUE ... 50% off Kids\' Underwear by Hanes ... BUY 1 GET 1 HALF PRICE on Women\'s Clothing by Whistle, Ella Clothing Farmers Toy Sale ON NOW | SHOP CATALOGUE ... 50% off Kids\' Underwear by Farmers Toy Sale ON NOW | SHOP CATALOGUE ... 50% off Kids\' Underwear by Hanes ... BUY 1 GET 1 HALF PRICE on Women\'s Clothing by Whistle, Ella Clothing Farmers Toy Sale ON <a href="http://www.farmers.com" rel="nofollow noreferrer noopener" target="_blank">www.farmers.com</a> | SHOP CATALOGUE ... 50% off Kids\' Underwear by Hanes ... BUY 1 GET 1 HALF PRICE on Women\'s Clothing by Whistle, Ella Clothing Farmers Toy Sale ON NOW | SHOP CATALOGUE ... 50% off Kids\' Underwear by.</p>',
+ url: 'http://localhost:3000/spamuser417/project/-/merge_requests/1#note_1375',
+ screenshot:
+ '/uploads/-/system/abuse_report/screenshot/27/Screenshot_2023-03-30_at_16.56.37.png',
+ },
+ actions: {
+ reportedUser: { name: 'Sp4m User', createdAt: '2023-03-29T09:30:23.885Z' },
+ userBlocked: false,
+ blockUserPath: '/admin/users/spamuser417/block',
+ removeReportPath: '/admin/abuse_reports/27',
+ removeUserAndReportPath: '/admin/abuse_reports/27?remove_user=true',
+ redirectPath: '/admin/abuse_reports',
+ },
+};
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js
new file mode 100644
index 00000000000..09b6b1edc44
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_actions_spec.js
@@ -0,0 +1,202 @@
+import { nextTick } from 'vue';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { sprintf } from '~/locale';
+import { ACTIONS_I18N } from '~/admin/abuse_reports/constants';
+import { mockAbuseReports } from '../mock_data';
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility');
+
+describe('AbuseReportActions', () => {
+ let wrapper;
+
+ const findRemoveUserAndReportButton = () => wrapper.findByText('Remove user & report');
+ const findBlockUserButton = () => wrapper.findByTestId('block-user-button');
+ const findRemoveReportButton = () => wrapper.findByText('Remove report');
+ const findConfirmationModal = () => wrapper.findComponent(GlModal);
+
+ const report = mockAbuseReports[0];
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(AbuseReportActions, {
+ propsData: {
+ report,
+ ...props,
+ },
+ stubs: {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays "Block user", "Remove user & report", and "Remove report" buttons', () => {
+ expect(findRemoveUserAndReportButton().text()).toBe(ACTIONS_I18N.removeUserAndReport);
+
+ const blockButton = findBlockUserButton();
+ expect(blockButton.text()).toBe(ACTIONS_I18N.blockUser);
+ expect(blockButton.attributes('disabled')).toBeUndefined();
+
+ expect(findRemoveReportButton().text()).toBe(ACTIONS_I18N.removeReport);
+ });
+
+ it('does not show the confirmation modal initially', () => {
+ expect(findConfirmationModal().props('visible')).toBe(false);
+ });
+ });
+
+ describe('block button when user is already blocked', () => {
+ it('is disabled and has the correct text', () => {
+ createComponent({ report: { ...report, userBlocked: true } });
+
+ const button = findBlockUserButton();
+ expect(button.text()).toBe(ACTIONS_I18N.alreadyBlocked);
+ expect(button.attributes('disabled')).toBeDefined();
+ });
+ });
+
+ describe('actions', () => {
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ createAlert.mockClear();
+ });
+
+ describe('on remove user and report', () => {
+ it('shows confirmation modal and reloads the page on success', async () => {
+ findRemoveUserAndReportButton().trigger('click');
+ await nextTick();
+
+ expect(findConfirmationModal().props()).toMatchObject({
+ visible: true,
+ title: sprintf(ACTIONS_I18N.removeUserAndReportConfirm, {
+ user: report.reportedUser.name,
+ }),
+ });
+
+ axiosMock.onDelete(report.removeUserAndReportPath).reply(HTTP_STATUS_OK);
+
+ findConfirmationModal().vm.$emit('primary');
+ await axios.waitForAll();
+
+ expect(refreshCurrentPage).toHaveBeenCalled();
+ });
+
+ describe('when a redirect path is present', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, redirectPath: '/redirect_path' } });
+ });
+
+ it('redirects to the given path', async () => {
+ findRemoveUserAndReportButton().trigger('click');
+ await nextTick();
+
+ axiosMock.onDelete(report.removeUserAndReportPath).reply(HTTP_STATUS_OK);
+
+ findConfirmationModal().vm.$emit('primary');
+ await axios.waitForAll();
+
+ expect(redirectTo).toHaveBeenCalledWith('/redirect_path'); // eslint-disable-line import/no-deprecated
+ });
+ });
+ });
+
+ describe('on block user', () => {
+ beforeEach(async () => {
+ findBlockUserButton().trigger('click');
+ await nextTick();
+ });
+
+ it('shows confirmation modal', () => {
+ expect(findConfirmationModal().props()).toMatchObject({
+ visible: true,
+ title: ACTIONS_I18N.blockUserConfirm,
+ });
+ });
+
+ describe.each([
+ {
+ responseData: { notice: 'Notice' },
+ createAlertArgs: { message: 'Notice', variant: VARIANT_SUCCESS },
+ blockButtonText: ACTIONS_I18N.alreadyBlocked,
+ blockButtonDisabled: 'disabled',
+ },
+ {
+ responseData: { error: 'Error' },
+ createAlertArgs: { message: 'Error' },
+ blockButtonText: ACTIONS_I18N.blockUser,
+ blockButtonDisabled: undefined,
+ },
+ ])(
+ 'when response JSON is $responseData',
+ ({ responseData, createAlertArgs, blockButtonText, blockButtonDisabled }) => {
+ beforeEach(async () => {
+ axiosMock.onPut(report.blockUserPath).reply(HTTP_STATUS_OK, responseData);
+
+ findConfirmationModal().vm.$emit('primary');
+ await axios.waitForAll();
+ });
+
+ it('updates the block button correctly', () => {
+ const button = findBlockUserButton();
+ expect(button.text()).toBe(blockButtonText);
+ expect(button.attributes('disabled')).toBe(blockButtonDisabled);
+ });
+
+ it('displays the returned message', () => {
+ expect(createAlert).toHaveBeenCalledWith(createAlertArgs);
+ });
+ },
+ );
+ });
+
+ describe('on remove report', () => {
+ it('reloads the page on success', async () => {
+ axiosMock.onDelete(report.removeReportPath).reply(HTTP_STATUS_OK);
+
+ findRemoveReportButton().trigger('click');
+
+ expect(findConfirmationModal().props('visible')).toBe(false);
+
+ await axios.waitForAll();
+
+ expect(refreshCurrentPage).toHaveBeenCalled();
+ });
+
+ describe('when a redirect path is present', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...report, redirectPath: '/redirect_path' } });
+ });
+
+ it('redirects to the given path', async () => {
+ axiosMock.onDelete(report.removeReportPath).reply(HTTP_STATUS_OK);
+
+ findRemoveReportButton().trigger('click');
+
+ await axios.waitForAll();
+
+ expect(redirectTo).toHaveBeenCalledWith('/redirect_path'); // eslint-disable-line import/no-deprecated
+ });
+ });
+ });
+ });
+});
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
new file mode 100644
index 00000000000..f3cced81478
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
@@ -0,0 +1,91 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import { SORT_UPDATED_AT } from '~/admin/abuse_reports/constants';
+import { mockAbuseReports } from '../mock_data';
+
+describe('AbuseReportRow', () => {
+ let wrapper;
+ const mockAbuseReport = mockAbuseReports[0];
+
+ const findListItem = () => wrapper.findComponent(ListItem);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findDisplayedDate = () => wrapper.findByTestId('abuse-report-date');
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(AbuseReportRow, {
+ propsData: {
+ report: mockAbuseReport,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a ListItem', () => {
+ expect(findListItem().exists()).toBe(true);
+ });
+
+ describe('title', () => {
+ const { reporter, reportedUser, category, reportPath } = mockAbuseReport;
+
+ it('displays correctly formatted title', () => {
+ expect(findTitle().text()).toMatchInterpolatedText(
+ `${reportedUser.name} reported for ${category} by ${reporter.name}`,
+ );
+ });
+
+ it('links to the details page', () => {
+ expect(findTitle().attributes('href')).toEqual(reportPath);
+ });
+
+ describe('when the reportedUser is missing', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...mockAbuseReport, reportedUser: null } });
+ });
+
+ it('displays correctly formatted title', () => {
+ expect(findTitle().text()).toMatchInterpolatedText(
+ `Deleted user reported for ${category} by ${reporter.name}`,
+ );
+ });
+ });
+
+ describe('when the reporter is missing', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...mockAbuseReport, reporter: null } });
+ });
+
+ it('displays correctly formatted title', () => {
+ expect(findTitle().text()).toMatchInterpolatedText(
+ `${reportedUser.name} reported for ${category} by Deleted user`,
+ );
+ });
+ });
+ });
+
+ describe('displayed date', () => {
+ it('displays correctly formatted created at', () => {
+ expect(findDisplayedDate().text()).toMatchInterpolatedText(
+ `Created ${getTimeago().format(mockAbuseReport.createdAt)}`,
+ );
+ });
+
+ describe('when sorted by updated_at', () => {
+ it('displays correctly formatted updated at', () => {
+ setWindowLocation(`?sort=${SORT_UPDATED_AT.sortDirection.ascending}`);
+
+ createComponent();
+
+ expect(findDisplayedDate().text()).toMatchInterpolatedText(
+ `Updated ${getTimeago().format(mockAbuseReport.updatedAt)}`,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
new file mode 100644
index 00000000000..1f3f2caa995
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
@@ -0,0 +1,225 @@
+import { shallowMount } from '@vue/test-utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { redirectTo, updateHistory } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import AbuseReportsFilteredSearchBar from '~/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue';
+import {
+ FILTERED_SEARCH_TOKENS,
+ FILTERED_SEARCH_TOKEN_USER,
+ FILTERED_SEARCH_TOKEN_REPORTER,
+ FILTERED_SEARCH_TOKEN_STATUS,
+ FILTERED_SEARCH_TOKEN_CATEGORY,
+ DEFAULT_SORT,
+ SORT_OPTIONS,
+} from '~/admin/abuse_reports/constants';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import { buildFilteredSearchCategoryToken } from '~/admin/abuse_reports/utils';
+
+jest.mock('~/lib/utils/url_utility', () => {
+ const urlUtility = jest.requireActual('~/lib/utils/url_utility');
+
+ return {
+ __esModule: true,
+ ...urlUtility,
+ redirectTo: jest.fn(),
+ updateHistory: jest.fn(),
+ };
+});
+
+describe('AbuseReportsFilteredSearchBar', () => {
+ let wrapper;
+
+ const CATEGORIES = ['spam', 'phishing'];
+
+ const createComponent = () => {
+ wrapper = shallowMount(AbuseReportsFilteredSearchBar, {
+ provide: { categories: CATEGORIES },
+ });
+ };
+
+ const findFilteredSearchBar = () => wrapper.findComponent(FilteredSearchBar);
+
+ beforeEach(() => {
+ setWindowLocation('https://localhost');
+ });
+
+ it('passes correct props to `FilteredSearchBar` component', () => {
+ createComponent();
+
+ const categoryToken = buildFilteredSearchCategoryToken(CATEGORIES);
+
+ expect(findFilteredSearchBar().props()).toMatchObject({
+ namespace: 'abuse_reports',
+ recentSearchesStorageKey: 'abuse_reports',
+ searchInputPlaceholder: 'Filter reports',
+ tokens: [...FILTERED_SEARCH_TOKENS, categoryToken],
+ initialSortBy: DEFAULT_SORT,
+ sortOptions: SORT_OPTIONS,
+ });
+ });
+
+ it.each([undefined, 'invalid'])(
+ 'sets status=open query when initial status query is %s',
+ (status) => {
+ if (status) {
+ setWindowLocation(`?status=${status}`);
+ }
+
+ createComponent();
+
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: 'https://localhost/?status=open',
+ replace: true,
+ });
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([
+ {
+ type: FILTERED_SEARCH_TOKEN_STATUS.type,
+ value: { data: 'open', operator: '=' },
+ },
+ ]);
+ },
+ );
+
+ it('parses and passes search param to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
+ setWindowLocation('?status=closed&user=mr_abuser&reporter=ms_nitch');
+
+ createComponent();
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([
+ {
+ type: FILTERED_SEARCH_TOKEN_USER.type,
+ value: { data: 'mr_abuser', operator: '=' },
+ },
+ {
+ type: FILTERED_SEARCH_TOKEN_REPORTER.type,
+ value: { data: 'ms_nitch', operator: '=' },
+ },
+ {
+ type: FILTERED_SEARCH_TOKEN_STATUS.type,
+ value: { data: 'closed', operator: '=' },
+ },
+ ]);
+ });
+
+ describe('initial sort', () => {
+ it.each(
+ SORT_OPTIONS.flatMap(({ sortDirection: { descending, ascending } }) => [
+ descending,
+ ascending,
+ ]),
+ )(
+ 'parses sort=%s query and passes it to `FilteredSearchBar` component as initialSortBy',
+ (sortBy) => {
+ setWindowLocation(`?sort=${sortBy}`);
+
+ createComponent();
+
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(sortBy);
+ },
+ );
+
+ it(`uses ${DEFAULT_SORT} as initialSortBy when sort query param is invalid`, () => {
+ setWindowLocation(`?sort=unknown`);
+
+ createComponent();
+
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(DEFAULT_SORT);
+ });
+ });
+
+ describe('onFilter', () => {
+ const USER_FILTER_TOKEN = {
+ type: FILTERED_SEARCH_TOKEN_USER.type,
+ value: { data: 'mr_abuser', operator: '=' },
+ };
+ const REPORTER_FILTER_TOKEN = {
+ type: FILTERED_SEARCH_TOKEN_REPORTER.type,
+ value: { data: 'ms_nitch', operator: '=' },
+ };
+ const STATUS_FILTER_TOKEN = {
+ type: FILTERED_SEARCH_TOKEN_STATUS.type,
+ value: { data: 'open', operator: '=' },
+ };
+ const CATEGORY_FILTER_TOKEN = {
+ type: FILTERED_SEARCH_TOKEN_CATEGORY.type,
+ value: { data: 'spam', operator: '=' },
+ };
+
+ const createComponentAndFilter = (filterTokens, initialLocation) => {
+ if (initialLocation) {
+ setWindowLocation(initialLocation);
+ }
+
+ createComponent();
+
+ findFilteredSearchBar().vm.$emit('onFilter', filterTokens);
+ };
+
+ it.each([USER_FILTER_TOKEN, REPORTER_FILTER_TOKEN, STATUS_FILTER_TOKEN, CATEGORY_FILTER_TOKEN])(
+ 'redirects with $type query param',
+ (filterToken) => {
+ createComponentAndFilter([filterToken]);
+ const { type, value } = filterToken;
+ expect(redirectTo).toHaveBeenCalledWith(`https://localhost/?${type}=${value.data}`); // eslint-disable-line import/no-deprecated
+ },
+ );
+
+ it('ignores search query param', () => {
+ const searchFilterToken = { type: FILTERED_SEARCH_TERM, value: { data: 'ignored' } };
+ createComponentAndFilter([USER_FILTER_TOKEN, searchFilterToken]);
+ expect(redirectTo).toHaveBeenCalledWith('https://localhost/?user=mr_abuser'); // eslint-disable-line import/no-deprecated
+ });
+
+ it('redirects without page query param', () => {
+ createComponentAndFilter([USER_FILTER_TOKEN], '?page=2');
+ expect(redirectTo).toHaveBeenCalledWith('https://localhost/?user=mr_abuser'); // eslint-disable-line import/no-deprecated
+ });
+
+ it('redirects with existing sort query param', () => {
+ createComponentAndFilter([USER_FILTER_TOKEN], `?sort=${DEFAULT_SORT}`);
+ // eslint-disable-next-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?user=mr_abuser&sort=${DEFAULT_SORT}`,
+ );
+ });
+ });
+
+ describe('onSort', () => {
+ const SORT_VALUE = 'updated_at_asc';
+ const EXISTING_QUERY = 'status=closed&user=mr_abuser';
+
+ const createComponentAndSort = (initialLocation) => {
+ setWindowLocation(initialLocation);
+ createComponent();
+ findFilteredSearchBar().vm.$emit('onSort', SORT_VALUE);
+ };
+
+ it('redirects to URL with existing query params and the sort query param', () => {
+ createComponentAndSort(`?${EXISTING_QUERY}`);
+
+ // eslint-disable-next-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`,
+ );
+ });
+
+ it('redirects without page query param', () => {
+ createComponentAndSort(`?${EXISTING_QUERY}&page=2`);
+
+ // eslint-disable-next-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`,
+ );
+ });
+
+ it('redirects with existing sort query param replaced with the new one', () => {
+ createComponentAndSort(`?${EXISTING_QUERY}&sort=created_at_desc`);
+
+ // eslint-disable-next-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?${EXISTING_QUERY}&sort=${SORT_VALUE}`,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/components/app_spec.js b/spec/frontend/admin/abuse_reports/components/app_spec.js
new file mode 100644
index 00000000000..41728baaf33
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/app_spec.js
@@ -0,0 +1,104 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState, GlPagination } from '@gitlab/ui';
+import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import AbuseReportsApp from '~/admin/abuse_reports/components/app.vue';
+import AbuseReportsFilteredSearchBar from '~/admin/abuse_reports/components/abuse_reports_filtered_search_bar.vue';
+import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
+import { mockAbuseReports } from '../mock_data';
+
+describe('AbuseReportsApp', () => {
+ let wrapper;
+
+ const findFilteredSearchBar = () => wrapper.findComponent(AbuseReportsFilteredSearchBar);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findAbuseReportRows = () => wrapper.findAllComponents(AbuseReportRow);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(AbuseReportsApp, {
+ propsData: {
+ abuseReports: mockAbuseReports,
+ pagination: { currentPage: 1, perPage: 20, totalItems: mockAbuseReports.length },
+ ...props,
+ },
+ });
+ };
+
+ it('renders AbuseReportsFilteredSearchBar', () => {
+ createComponent();
+
+ expect(findFilteredSearchBar().exists()).toBe(true);
+ });
+
+ it('renders one AbuseReportRow for each abuse report', () => {
+ createComponent();
+
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findAbuseReportRows().length).toBe(mockAbuseReports.length);
+ });
+
+ it('renders empty state when there are no reports', () => {
+ createComponent({
+ abuseReports: [],
+ pagination: { currentPage: 1, perPage: 20, totalItems: 0 },
+ });
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ describe('pagination', () => {
+ const pagination = {
+ currentPage: 1,
+ perPage: 1,
+ totalItems: mockAbuseReports.length,
+ };
+
+ it('renders GlPagination with the correct props when needed', () => {
+ createComponent({ pagination });
+
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toMatchObject({
+ value: pagination.currentPage,
+ perPage: pagination.perPage,
+ totalItems: pagination.totalItems,
+ prevText: 'Prev',
+ nextText: 'Next',
+ labelNextPage: 'Go to next page',
+ labelPrevPage: 'Go to previous page',
+ align: 'center',
+ });
+ });
+
+ it('does not render GlPagination when not needed', () => {
+ createComponent({ pagination: { currentPage: 1, perPage: 2, totalItems: 2 } });
+
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ describe('linkGen prop', () => {
+ const existingQuery = {
+ user: 'mr_okay',
+ status: 'closed',
+ };
+ const expectedGeneratedQuery = {
+ ...existingQuery,
+ page: '2',
+ };
+
+ beforeEach(() => {
+ setWindowLocation(`https://localhost?${objectToQuery(existingQuery)}`);
+ });
+
+ it('generates the correct page URL', () => {
+ createComponent({ pagination });
+
+ const linkGen = findPagination().props('linkGen');
+ const generatedUrl = linkGen(expectedGeneratedQuery.page);
+ const [, generatedQuery] = generatedUrl.split('?');
+
+ expect(queryToObject(generatedQuery)).toMatchObject(expectedGeneratedQuery);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/mock_data.js b/spec/frontend/admin/abuse_reports/mock_data.js
new file mode 100644
index 00000000000..1ea6ea7d131
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/mock_data.js
@@ -0,0 +1,18 @@
+export const mockAbuseReports = [
+ {
+ category: 'spam',
+ createdAt: '2018-10-03T05:46:38.977Z',
+ updatedAt: '2022-12-07T06:45:39.977Z',
+ reporter: { name: 'Ms. Admin' },
+ reportedUser: { name: 'Mr. Abuser' },
+ reportPath: '/admin/abuse_reports/1',
+ },
+ {
+ category: 'phishing',
+ createdAt: '2018-10-03T05:46:38.977Z',
+ updatedAt: '2022-12-07T06:45:39.977Z',
+ reporter: { name: 'Ms. Reporter' },
+ reportedUser: { name: 'Mr. Phisher' },
+ reportPath: '/admin/abuse_reports/2',
+ },
+];
diff --git a/spec/frontend/admin/abuse_reports/utils_spec.js b/spec/frontend/admin/abuse_reports/utils_spec.js
new file mode 100644
index 00000000000..3908edd3fdd
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/utils_spec.js
@@ -0,0 +1,31 @@
+import {
+ FILTERED_SEARCH_TOKEN_CATEGORY,
+ FILTERED_SEARCH_TOKEN_STATUS,
+} from '~/admin/abuse_reports/constants';
+import { buildFilteredSearchCategoryToken, isValidStatus } from '~/admin/abuse_reports/utils';
+
+describe('buildFilteredSearchCategoryToken', () => {
+ it('adds correctly formatted options to FILTERED_SEARCH_TOKEN_CATEGORY', () => {
+ const categories = ['tuxedo', 'tabby'];
+
+ expect(buildFilteredSearchCategoryToken(categories)).toMatchObject({
+ ...FILTERED_SEARCH_TOKEN_CATEGORY,
+ options: categories.map((c) => ({ value: c, title: c })),
+ });
+ });
+});
+
+describe('isValidStatus', () => {
+ const validStatuses = FILTERED_SEARCH_TOKEN_STATUS.options.map((o) => o.value);
+
+ it.each(validStatuses)(
+ 'returns true when status is an option value of FILTERED_SEARCH_TOKEN_STATUS',
+ (status) => {
+ expect(isValidStatus(status)).toBe(true);
+ },
+ );
+
+ it('return false when status is not an option value of FILTERED_SEARCH_TOKEN_STATUS', () => {
+ expect(isValidStatus('invalid')).toBe(false);
+ });
+});
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
index c9a899ab78b..06f9ffeffcd 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
@@ -19,10 +19,6 @@ describe('DevopsScoreCallout', () => {
const findBanner = () => wrapper.findComponent(GlBanner);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with no cookie set', () => {
beforeEach(() => {
utils.setCookie = jest.fn();
diff --git a/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js b/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js
index 2db997942a7..969844f981c 100644
--- a/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js
+++ b/spec/frontend/admin/application_settings/inactive_project_deletion/components/form_spec.js
@@ -29,10 +29,6 @@ describe('Form component', () => {
wrapper = mountFn(SettingsForm, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Enable inactive project deletion', () => {
it('has the checkbox', () => {
createComponent();
diff --git a/spec/frontend/admin/application_settings/network_outbound_spec.js b/spec/frontend/admin/application_settings/network_outbound_spec.js
new file mode 100644
index 00000000000..2c06a3fd67f
--- /dev/null
+++ b/spec/frontend/admin/application_settings/network_outbound_spec.js
@@ -0,0 +1,70 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+import initNetworkOutbound from '~/admin/application_settings/network_outbound';
+
+describe('initNetworkOutbound', () => {
+ const findAllowCheckboxes = () => document.querySelectorAll('.js-allow-local-requests');
+ const findDenyCheckbox = () => document.querySelector('.js-deny-all-requests');
+ const findWarningBanner = () => document.querySelector('.js-deny-all-requests-warning');
+ const clickDenyCheckbox = () => {
+ findDenyCheckbox().click();
+ };
+
+ const createFixture = (denyAll = false) => {
+ setHTMLFixture(`
+ <input class="js-deny-all-requests" type="checkbox" name="application_setting[deny_all_requests_except_allowed]" ${
+ denyAll ? 'checked="checked"' : ''
+ }/>
+ <div class="js-deny-all-requests-warning ${denyAll ? '' : 'gl-display-none'}"></div>
+ <input class="js-allow-local-requests" type="checkbox" name="application_setting[allow_local_requests_from_web_hooks_and_services]" />
+ <input class="js-allow-local-requests" type="checkbox" name="application_setting[allow_local_requests_from_system_hooks]" />
+ `);
+ };
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('when the checkbox is not checked', () => {
+ beforeEach(() => {
+ createFixture();
+ initNetworkOutbound();
+ });
+
+ it('shows banner and disables allow checkboxes on change', () => {
+ expect(findDenyCheckbox().checked).toBe(false);
+ expect(findWarningBanner().classList).toContain('gl-display-none');
+
+ clickDenyCheckbox();
+
+ expect(findDenyCheckbox().checked).toBe(true);
+ expect(findWarningBanner().classList).not.toContain('gl-display-none');
+ const allowCheckboxes = findAllowCheckboxes();
+ allowCheckboxes.forEach((checkbox) => {
+ expect(checkbox.checked).toBe(false);
+ expect(checkbox.disabled).toBe(true);
+ });
+ });
+ });
+
+ describe('when the checkbox is checked', () => {
+ beforeEach(() => {
+ createFixture(true);
+ initNetworkOutbound();
+ });
+
+ it('hides banner and enables allow checkboxes on change', () => {
+ expect(findDenyCheckbox().checked).toBe(true);
+ expect(findWarningBanner().classList).not.toContain('gl-display-none');
+
+ clickDenyCheckbox();
+
+ expect(findDenyCheckbox().checked).toBe(false);
+ expect(findWarningBanner().classList).toContain('gl-display-none');
+ const allowCheckboxes = findAllowCheckboxes();
+ allowCheckboxes.forEach((checkbox) => {
+ expect(checkbox.disabled).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/applications/components/delete_application_spec.js b/spec/frontend/admin/applications/components/delete_application_spec.js
index 1a400a101b5..315c38a2bbc 100644
--- a/spec/frontend/admin/applications/components/delete_application_spec.js
+++ b/spec/frontend/admin/applications/components/delete_application_spec.js
@@ -31,7 +31,6 @@ describe('DeleteApplication', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
index 212f4c0842c..d7b319a3d5e 100644
--- a/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
+++ b/spec/frontend/admin/background_migrations/components/database_listbox_spec.js
@@ -26,10 +26,6 @@ describe('BackgroundMigrationsDatabaseListbox', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
describe('template always', () => {
diff --git a/spec/frontend/admin/broadcast_messages/components/base_spec.js b/spec/frontend/admin/broadcast_messages/components/base_spec.js
index d69bf4a22bf..80577f86e3e 100644
--- a/spec/frontend/admin/broadcast_messages/components/base_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/base_spec.js
@@ -4,15 +4,15 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import { createAlert } from '~/flash';
+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 { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue';
import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
import { generateMockMessages, MOCK_MESSAGES } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('BroadcastMessagesBase', () => {
@@ -41,7 +41,6 @@ describe('BroadcastMessagesBase', () => {
afterEach(() => {
axiosMock.restore();
- wrapper.destroy();
});
it('renders the table and pagination when there are existing messages', () => {
@@ -108,6 +107,6 @@ describe('BroadcastMessagesBase', () => {
findTable().vm.$emit('delete-message', id);
await waitForPromises();
- expect(redirectTo).toHaveBeenCalledWith(`${TEST_HOST}/admin/broadcast_messages?page=1`);
+ expect(redirectTo).toHaveBeenCalledWith(`${TEST_HOST}/admin/broadcast_messages?page=1`); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
index 36c0ac303ba..212f26b8faf 100644
--- a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
@@ -1,21 +1,16 @@
import { mount } from '@vue/test-utils';
import { GlBroadcastMessage, GlForm } from '@gitlab/ui';
import AxiosMockAdapter from 'axios-mock-adapter';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
import MessageForm from '~/admin/broadcast_messages/components/message_form.vue';
-import {
- BROADCAST_MESSAGES_PATH,
- TYPE_BANNER,
- TYPE_NOTIFICATION,
- THEMES,
-} from '~/admin/broadcast_messages/constants';
+import { TYPE_BANNER, TYPE_NOTIFICATION, THEMES } from '~/admin/broadcast_messages/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('MessageForm', () => {
let wrapper;
@@ -32,6 +27,8 @@ describe('MessageForm', () => {
endsAt: new Date(),
};
+ const messagesPath = '_messages_path_';
+
const findPreview = () => extendedWrapper(wrapper.findComponent(GlBroadcastMessage));
const findThemeSelect = () => wrapper.findComponent('[data-testid=theme-select]');
const findDismissable = () => wrapper.findComponent('[data-testid=dismissable-checkbox]');
@@ -39,11 +36,12 @@ describe('MessageForm', () => {
const findSubmitButton = () => wrapper.findComponent('[data-testid=submit-button]');
const findForm = () => wrapper.findComponent(GlForm);
- function createComponent({ broadcastMessage = {}, glFeatures = {} }) {
+ function createComponent({ broadcastMessage = {} } = {}) {
wrapper = mount(MessageForm, {
provide: {
- glFeatures,
targetAccessLevelOptions: MOCK_TARGET_ACCESS_LEVELS,
+ messagesPath,
+ previewPath: '_preview_path_',
},
propsData: {
broadcastMessage: {
@@ -101,15 +99,10 @@ describe('MessageForm', () => {
});
describe('target roles checkboxes', () => {
- it('renders when roleTargetedBroadcastMessages feature is enabled', () => {
- createComponent({ glFeatures: { roleTargetedBroadcastMessages: true } });
+ it('renders target roles', () => {
+ createComponent();
expect(findTargetRoles().exists()).toBe(true);
});
-
- it('does not render when roleTargetedBroadcastMessages feature is disabled', () => {
- createComponent({ glFeatures: { roleTargetedBroadcastMessages: false } });
- expect(findTargetRoles().exists()).toBe(false);
- });
});
describe('form submit button', () => {
@@ -151,16 +144,16 @@ describe('MessageForm', () => {
findForm().vm.$emit('submit', { preventDefault: () => {} });
await waitForPromises();
- expect(axiosMock.history.post).toHaveLength(1);
- expect(axiosMock.history.post[0]).toMatchObject({
- url: BROADCAST_MESSAGES_PATH,
+ expect(axiosMock.history.post).toHaveLength(2);
+ expect(axiosMock.history.post[1]).toMatchObject({
+ url: messagesPath,
data: JSON.stringify(defaultPayload),
});
});
it('shows an error alert if the create request fails', async () => {
createComponent({ broadcastMessage: { id: undefined } });
- axiosMock.onPost(BROADCAST_MESSAGES_PATH).replyOnce(HTTP_STATUS_BAD_REQUEST);
+ axiosMock.onPost(messagesPath).replyOnce(HTTP_STATUS_BAD_REQUEST);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await waitForPromises();
@@ -179,7 +172,7 @@ describe('MessageForm', () => {
expect(axiosMock.history.patch).toHaveLength(1);
expect(axiosMock.history.patch[0]).toMatchObject({
- url: `${BROADCAST_MESSAGES_PATH}/${id}`,
+ url: `${messagesPath}/${id}`,
data: JSON.stringify(defaultPayload),
});
});
@@ -187,7 +180,7 @@ describe('MessageForm', () => {
it('shows an error alert if the update request fails', async () => {
const id = 1337;
createComponent({ broadcastMessage: { id } });
- axiosMock.onPost(`${BROADCAST_MESSAGES_PATH}/${id}`).replyOnce(HTTP_STATUS_BAD_REQUEST);
+ axiosMock.onPost(`${messagesPath}/${id}`).replyOnce(HTTP_STATUS_BAD_REQUEST);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await waitForPromises();
diff --git a/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js b/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js
index 349fab03853..6d536b2d0e4 100644
--- a/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/messages_table_spec.js
@@ -9,11 +9,8 @@ describe('MessagesTable', () => {
const findTargetRoles = () => wrapper.find('[data-testid="target-roles-th"]');
const findDeleteButton = (id) => wrapper.find(`[data-testid="delete-message-${id}"]`);
- function createComponent(props = {}, glFeatures = {}) {
+ function createComponent(props = {}) {
wrapper = mount(MessagesTable, {
- provide: {
- glFeatures,
- },
propsData: {
messages: MOCK_MESSAGES,
...props,
@@ -21,24 +18,16 @@ describe('MessagesTable', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a table row for each message', () => {
createComponent();
expect(findRows()).toHaveLength(MOCK_MESSAGES.length);
});
- it('renders the "Target Roles" column when roleTargetedBroadcastMessages is enabled', () => {
- createComponent({}, { roleTargetedBroadcastMessages: true });
- expect(findTargetRoles().exists()).toBe(true);
- });
-
- it('does not render the "Target Roles" column when roleTargetedBroadcastMessages is disabled', () => {
+ it('renders the "Target Roles" column', () => {
createComponent();
- expect(findTargetRoles().exists()).toBe(false);
+
+ expect(findTargetRoles().exists()).toBe(true);
});
it('emits a delete-message event when a delete button is clicked', () => {
diff --git a/spec/frontend/admin/broadcast_messages/mock_data.js b/spec/frontend/admin/broadcast_messages/mock_data.js
index 2e20b5cf638..54596fbf977 100644
--- a/spec/frontend/admin/broadcast_messages/mock_data.js
+++ b/spec/frontend/admin/broadcast_messages/mock_data.js
@@ -4,7 +4,10 @@ const generateMockMessage = (id) => ({
edit_path: `/admin/broadcast_messages/${id}/edit`,
starts_at: new Date().toISOString(),
ends_at: new Date().toISOString(),
- preview: '<div>YEET</div>',
+ broadcast_type: 'banner',
+ dismissable: true,
+ message: 'YEET',
+ theme: 'indigo',
status: 'Expired',
target_path: '*/welcome',
target_roles: 'Maintainer, Owner',
diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js
index 4d4a2caedde..a05654a1d25 100644
--- a/spec/frontend/admin/deploy_keys/components/table_spec.js
+++ b/spec/frontend/admin/deploy_keys/components/table_spec.js
@@ -9,10 +9,10 @@ import { stubComponent } from 'helpers/stub_component';
import DeployKeysTable from '~/admin/deploy_keys/components/table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Api, { DEFAULT_PER_PAGE } from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
jest.mock('~/api');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('DeployKeysTable', () => {
@@ -91,10 +91,6 @@ describe('DeployKeysTable', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders page title', () => {
createComponent();
@@ -242,7 +238,7 @@ describe('DeployKeysTable', () => {
itRendersTheEmptyState();
- it('displays flash', () => {
+ it('displays alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: DeployKeysTable.i18n.apiErrorMessage,
captureError: true,
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
index eecc21e206b..9e55716cc30 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_checkbox_spec.js
@@ -28,10 +28,6 @@ describe('Signup Form', () => {
const findCheckboxLabel = () => findByTestId('label');
const findHelpText = () => findByTestId('helpText');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Signup Checkbox', () => {
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
index f2a951bcc76..db8c33d01cb 100644
--- a/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
+++ b/spec/frontend/admin/signup_restrictions/components/signup_form_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlModal, GlLink } from '@gitlab/ui';
+import { GlButton, GlModal } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import { shallowMount, mount, createWrapper } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
@@ -36,12 +36,9 @@ describe('Signup Form', () => {
const findDenyListRawInputGroup = () => wrapper.findByTestId('domain-denylist-raw-input-group');
const findDenyListFileInputGroup = () => wrapper.findByTestId('domain-denylist-file-input-group');
const findUserCapInput = () => wrapper.findByTestId('user-cap-input');
- const findUserCapFormGroup = () => wrapper.findByTestId('user-cap-form-group');
const findModal = () => wrapper.findComponent(GlModal);
afterEach(() => {
- wrapper.destroy();
-
formSubmitSpy = null;
});
@@ -214,19 +211,4 @@ describe('Signup Form', () => {
});
});
});
-
- describe('rendering help links within user cap description', () => {
- beforeEach(() => {
- mountComponent({ mountFn: mount });
- });
-
- it('renders projectSharingHelpLink and groupSharingHelpLink', () => {
- const [projectSharingLink, groupSharingLink] = findUserCapFormGroup().findAllComponents(
- GlLink,
- ).wrappers;
-
- expect(projectSharingLink.attributes('href')).toBe(mockData.projectSharingHelpLink);
- expect(groupSharingLink.attributes('href')).toBe(mockData.groupSharingHelpLink);
- });
- });
});
diff --git a/spec/frontend/admin/signup_restrictions/mock_data.js b/spec/frontend/admin/signup_restrictions/mock_data.js
index ce5ec2248fe..3140d7be105 100644
--- a/spec/frontend/admin/signup_restrictions/mock_data.js
+++ b/spec/frontend/admin/signup_restrictions/mock_data.js
@@ -22,8 +22,6 @@ export const rawMockData = {
passwordLowercaseRequired: 'true',
passwordUppercaseRequired: 'true',
passwordSymbolRequired: 'true',
- projectSharingHelpLink: 'project-sharing/help/link',
- groupSharingHelpLink: 'group-sharing/help/link',
};
export const mockData = {
@@ -50,6 +48,4 @@ export const mockData = {
passwordLowercaseRequired: true,
passwordUppercaseRequired: true,
passwordSymbolRequired: true,
- projectSharingHelpLink: 'project-sharing/help/link',
- groupSharingHelpLink: 'group-sharing/help/link',
};
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js
index 4c362a31068..60e46cddd7e 100644
--- a/spec/frontend/admin/statistics_panel/components/app_spec.js
+++ b/spec/frontend/admin/statistics_panel/components/app_spec.js
@@ -30,10 +30,6 @@ describe('Admin statistics app', () => {
store = createStore();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findStats = (idx) => wrapper.findAll('.js-stats').at(idx);
describe('template', () => {
diff --git a/spec/frontend/admin/topics/components/remove_avatar_spec.js b/spec/frontend/admin/topics/components/remove_avatar_spec.js
index 97d257c682c..c069203d046 100644
--- a/spec/frontend/admin/topics/components/remove_avatar_spec.js
+++ b/spec/frontend/admin/topics/components/remove_avatar_spec.js
@@ -20,7 +20,7 @@ describe('RemoveAvatar', () => {
name,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
stubs: {
GlSprintf,
@@ -36,10 +36,6 @@ describe('RemoveAvatar', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('the button component', () => {
it('displays the remove button', () => {
const button = findButton();
diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js
index 738cbd88c4c..113a0e3d404 100644
--- a/spec/frontend/admin/topics/components/topic_select_spec.js
+++ b/spec/frontend/admin/topics/components/topic_select_spec.js
@@ -59,7 +59,6 @@ describe('TopicSelect', () => {
}
afterEach(() => {
- wrapper.destroy();
jest.clearAllMocks();
});
diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js
index 8e9652332c1..a5e7c6ebe21 100644
--- a/spec/frontend/admin/users/components/actions/actions_spec.js
+++ b/spec/frontend/admin/users/components/actions/actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Actions from '~/admin/users/components/actions';
import Delete from '~/admin/users/components/actions/delete.vue';
@@ -12,7 +12,7 @@ import { paths, userDeletionObstacles } from '../../mock_data';
describe('Action components', () => {
let wrapper;
- const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const initComponent = ({ component, props } = {}) => {
wrapper = shallowMount(component, {
@@ -22,11 +22,6 @@ describe('Action components', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('CONFIRMATION_ACTIONS', () => {
it.each(CONFIRMATION_ACTIONS)('renders a dropdown item for "%s"', (action) => {
initComponent({
@@ -37,7 +32,7 @@ describe('Action components', () => {
},
});
- expect(findDropdownItem().exists()).toBe(true);
+ expect(findDisclosureDropdownItem().exists()).toBe(true);
});
});
@@ -57,7 +52,7 @@ describe('Action components', () => {
},
});
- await findDropdownItem().vm.$emit('click');
+ await findDisclosureDropdownItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith(
EVENT_OPEN_DELETE_USER_MODAL,
diff --git a/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
index 64a88aab2c2..606a5c779fb 100644
--- a/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
+++ b/spec/frontend/admin/users/components/actions/delete_with_contributions_spec.js
@@ -1,5 +1,5 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import DeleteWithContributions from '~/admin/users/components/actions/delete_with_contributions.vue';
import eventHub, {
@@ -35,7 +35,7 @@ describe('DeleteWithContributions', () => {
};
const createComponent = () => {
- wrapper = mountExtended(DeleteWithContributions, { propsData: defaultPropsData });
+ wrapper = mount(DeleteWithContributions, { propsData: defaultPropsData });
};
describe('when action is clicked', () => {
@@ -47,10 +47,10 @@ describe('DeleteWithContributions', () => {
});
it('displays loading icon and disables button', async () => {
- await wrapper.trigger('click');
+ await wrapper.find('button').trigger('click');
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.findByRole('menuitem').attributes()).toMatchObject({
+ expect(wrapper.attributes()).toMatchObject({
disabled: 'disabled',
'aria-busy': 'true',
});
@@ -67,7 +67,7 @@ describe('DeleteWithContributions', () => {
});
it('emits event with association counts', async () => {
- await wrapper.trigger('click');
+ await wrapper.find('button').trigger('click');
await waitForPromises();
expect(associationsCount).toHaveBeenCalledWith(defaultPropsData.userId);
@@ -92,7 +92,7 @@ describe('DeleteWithContributions', () => {
});
it('emits event with error', async () => {
- await wrapper.trigger('click');
+ await wrapper.find('button').trigger('click');
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith(
diff --git a/spec/frontend/admin/users/components/app_spec.js b/spec/frontend/admin/users/components/app_spec.js
index 913732aae42..d40089edc82 100644
--- a/spec/frontend/admin/users/components/app_spec.js
+++ b/spec/frontend/admin/users/components/app_spec.js
@@ -17,11 +17,6 @@ describe('AdminUsersApp component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when initialized', () => {
beforeEach(() => {
initComponent();
diff --git a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap b/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap
deleted file mode 100644
index dc98d367af7..00000000000
--- a/spec/frontend/admin/users/components/associations/__snapshots__/associations_list_spec.js.snap
+++ /dev/null
@@ -1,34 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AssociationsList when counts are 0 does not render items 1`] = `""`;
-
-exports[`AssociationsList when counts are plural renders plural counts 1`] = `
-"<ul class=\\"gl-mb-5\\">
- <li><strong>2</strong> groups</li>
- <li><strong>3</strong> projects</li>
- <li><strong>4</strong> issues</li>
- <li><strong>5</strong> merge requests</li>
-</ul>"
-`;
-
-exports[`AssociationsList when counts are singular renders singular counts 1`] = `
-"<ul class=\\"gl-mb-5\\">
- <li><strong>1</strong> group</li>
- <li><strong>1</strong> project</li>
- <li><strong>1</strong> issue</li>
- <li><strong>1</strong> merge request</li>
-</ul>"
-`;
-
-exports[`AssociationsList when there is an error displays an alert 1`] = `
-"<div class=\\"gl-mb-5 gl-alert gl-alert-not-dismissible gl-alert-danger\\"><svg data-testid=\\"error-icon\\" role=\\"img\\" aria-hidden=\\"true\\" class=\\"gl-icon s16 gl-alert-icon gl-alert-icon-no-title\\">
- <use href=\\"#error\\"></use>
- </svg>
- <div role=\\"alert\\" aria-live=\\"assertive\\" class=\\"gl-alert-content\\">
- <!---->
- <div class=\\"gl-alert-body\\">An error occurred while fetching this user's contributions, and the request cannot return the number of issues, merge requests, groups, and projects linked to this user. If you proceed with deleting the user, all their contributions will still be deleted.</div>
- <!---->
- </div>
- <!---->
-</div>"
-`;
diff --git a/spec/frontend/admin/users/components/associations/associations_list_spec.js b/spec/frontend/admin/users/components/associations/associations_list_spec.js
index d77a645111f..21f9924b21a 100644
--- a/spec/frontend/admin/users/components/associations/associations_list_spec.js
+++ b/spec/frontend/admin/users/components/associations/associations_list_spec.js
@@ -1,3 +1,4 @@
+import { GlAlert } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import AssociationsList from '~/admin/users/components/associations/associations_list.vue';
@@ -13,6 +14,8 @@ describe('AssociationsList', () => {
},
};
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
const createComponent = ({ propsData = {} } = {}) => {
wrapper = mountExtended(AssociationsList, {
propsData: {
@@ -30,7 +33,18 @@ describe('AssociationsList', () => {
},
});
- expect(wrapper.html()).toMatchSnapshot();
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toContain(
+ "An error occurred while fetching this user's contributions",
+ );
+ });
+ });
+
+ describe('with no errors', () => {
+ it('does not display an alert', () => {
+ createComponent();
+
+ expect(findAlert().exists()).toBe(false);
});
});
@@ -38,24 +52,36 @@ describe('AssociationsList', () => {
it('renders singular counts', () => {
createComponent();
- expect(wrapper.html()).toMatchSnapshot();
+ expect(wrapper.text()).toContain(`${defaultPropsData.associationsCount.groups_count} group`);
+ expect(wrapper.text()).toContain(`${defaultPropsData.associationsCount.issues_count} issue`);
+ expect(wrapper.text()).toContain(
+ `${defaultPropsData.associationsCount.projects_count} project`,
+ );
+ expect(wrapper.text()).toContain(
+ `${defaultPropsData.associationsCount.merge_requests_count} merge request`,
+ );
});
});
describe('when counts are plural', () => {
it('renders plural counts', () => {
- createComponent({
- propsData: {
- associationsCount: {
- groups_count: 2,
- projects_count: 3,
- issues_count: 4,
- merge_requests_count: 5,
- },
+ const propsData = {
+ associationsCount: {
+ groups_count: 2,
+ projects_count: 3,
+ issues_count: 4,
+ merge_requests_count: 5,
},
- });
+ };
+
+ createComponent({ propsData });
- expect(wrapper.html()).toMatchSnapshot();
+ expect(wrapper.text()).toContain(`${propsData.associationsCount.groups_count} groups`);
+ expect(wrapper.text()).toContain(`${propsData.associationsCount.issues_count} issues`);
+ expect(wrapper.text()).toContain(`${propsData.associationsCount.projects_count} projects`);
+ expect(wrapper.text()).toContain(
+ `${propsData.associationsCount.merge_requests_count} merge requests`,
+ );
});
});
@@ -72,7 +98,7 @@ describe('AssociationsList', () => {
},
});
- expect(wrapper.html()).toMatchSnapshot();
+ expect(wrapper.html()).toBe('');
});
});
});
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index 2e892e292d7..b2a0c201893 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -73,11 +73,6 @@ describe('Delete user modal', () => {
formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders modal with form included', () => {
createComponent();
expect(findForm().element).toMatchSnapshot();
@@ -89,8 +84,8 @@ describe('Delete user modal', () => {
});
it('has disabled buttons', () => {
- expect(findPrimaryButton().attributes('disabled')).toBe('true');
- expect(findSecondaryButton().attributes('disabled')).toBe('true');
+ expect(findPrimaryButton().attributes('disabled')).toBeDefined();
+ expect(findSecondaryButton().attributes('disabled')).toBeDefined();
});
});
@@ -107,8 +102,8 @@ describe('Delete user modal', () => {
});
it('has disabled buttons', () => {
- expect(findPrimaryButton().attributes('disabled')).toBe('true');
- expect(findSecondaryButton().attributes('disabled')).toBe('true');
+ expect(findPrimaryButton().attributes('disabled')).toBeDefined();
+ expect(findSecondaryButton().attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
index 1b080b05c95..73d8c082bb9 100644
--- a/spec/frontend/admin/users/components/user_actions_spec.js
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownDivider } from '@gitlab/ui';
+import { GlDisclosureDropdownGroup } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Actions from '~/admin/users/components/actions';
@@ -19,7 +19,7 @@ describe('AdminUserActions component', () => {
const findEditButton = (id = user.id) => findUserActions(id).find('[data-testid="edit"]');
const findActionsDropdown = (id = user.id) =>
findUserActions(id).find('[data-testid="dropdown-toggle"]');
- const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
+ const findDisclosureGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
const initComponent = ({ actions = [], showButtonLabels } = {}) => {
wrapper = shallowMountExtended(AdminUserActions, {
@@ -32,16 +32,11 @@ describe('AdminUserActions component', () => {
showButtonLabels,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('edit button', () => {
describe('when the user has an edit action attached', () => {
beforeEach(() => {
@@ -109,8 +104,8 @@ describe('AdminUserActions component', () => {
initComponent({ actions: [LDAP, ...DELETE_ACTIONS] });
});
- it('renders a dropdown divider', () => {
- expect(findDropdownDivider().exists()).toBe(true);
+ it('renders a disclosure group', () => {
+ expect(findDisclosureGroup().exists()).toBe(true);
});
it('only renders delete dropdown items for actions containing the word "delete"', () => {
@@ -131,8 +126,8 @@ describe('AdminUserActions component', () => {
});
describe('when there are no delete actions', () => {
- it('does not render a dropdown divider', () => {
- expect(findDropdownDivider().exists()).toBe(false);
+ it('does not render a disclosure group', () => {
+ expect(findDisclosureGroup().exists()).toBe(false);
});
it('does not render a delete dropdown item', () => {
diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js
index 94fac875fbe..02e648d2b77 100644
--- a/spec/frontend/admin/users/components/user_avatar_spec.js
+++ b/spec/frontend/admin/users/components/user_avatar_spec.js
@@ -26,7 +26,7 @@ describe('AdminUserAvatar component', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
GlAvatarLabeled,
@@ -34,11 +34,6 @@ describe('AdminUserAvatar component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when initialized', () => {
beforeEach(() => {
initComponent();
diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js
index 73be33d5a9d..19c1cd38a50 100644
--- a/spec/frontend/admin/users/components/user_date_spec.js
+++ b/spec/frontend/admin/users/components/user_date_spec.js
@@ -17,11 +17,6 @@ describe('FormatDate component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
date | dateFormat | output
${mockDate} | ${undefined} | ${'Nov 13, 2020'}
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index a0aec347b6b..6f658fd2e59 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -10,12 +10,12 @@ import AdminUserActions from '~/admin/users/components/user_actions.vue';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
import AdminUsersTable from '~/admin/users/components/users_table.vue';
import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import AdminUserDate from '~/vue_shared/components/user_date.vue';
import { users, paths, createGroupCountResponse } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -57,11 +57,6 @@ describe('AdminUsersTable component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when there are users', () => {
beforeEach(() => {
initComponent();
@@ -134,7 +129,7 @@ describe('AdminUsersTable component', () => {
await waitForPromises();
});
- it('creates a flash message and captures the error', () => {
+ it('creates an alert message and captures the error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Could not load user group counts. Please refresh the page to try again.',
captureError: true,
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index b51858d5129..d8a94ee5e1d 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -19,8 +19,6 @@ describe('initAdminUsersApp', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
el = null;
});
@@ -47,8 +45,6 @@ describe('initAdminUserActions', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
el = null;
});
diff --git a/spec/frontend/admin/users/new_spec.js b/spec/frontend/admin/users/new_spec.js
index 5e5763822a8..eba5c87f470 100644
--- a/spec/frontend/admin/users/new_spec.js
+++ b/spec/frontend/admin/users/new_spec.js
@@ -1,20 +1,19 @@
+import newWithInternalUserRegex from 'test_fixtures/admin/users/new_with_internal_user_regex.html';
import {
setupInternalUserRegexHandler,
ID_USER_EMAIL,
ID_USER_EXTERNAL,
ID_WARNING,
} from '~/admin/users/new';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('admin/users/new', () => {
- const FIXTURE = 'admin/users/new_with_internal_user_regex.html';
-
let elExternal;
let elUserEmail;
let elWarningMessage;
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(newWithInternalUserRegex);
setupInternalUserRegexHandler();
elExternal = document.getElementById(ID_USER_EXTERNAL);
diff --git a/spec/frontend/airflow/dags/components/dags_spec.js b/spec/frontend/airflow/dags/components/dags_spec.js
deleted file mode 100644
index f9cf4fc87af..00000000000
--- a/spec/frontend/airflow/dags/components/dags_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { GlAlert, GlPagination, GlTableLite } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'helpers/test_constants';
-import AirflowDags from '~/airflow/dags/components/dags.vue';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import { mockDags } from './mock_data';
-
-describe('AirflowDags', () => {
- let wrapper;
-
- const createWrapper = (
- dags = [],
- pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
- ) => {
- wrapper = mountExtended(AirflowDags, {
- propsData: {
- dags,
- pagination,
- },
- });
- };
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findEmptyState = () => wrapper.findByText('There are no DAGs to show');
- const findPagination = () => wrapper.findComponent(GlPagination);
-
- describe('default (no dags)', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('shows incubation warning', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('shows empty state', () => {
- expect(findEmptyState().exists()).toBe(true);
- });
-
- it('does not show pagination', () => {
- expect(findPagination().exists()).toBe(false);
- });
- });
-
- describe('with dags', () => {
- const createWrapperWithDags = (pagination = {}) => {
- createWrapper(mockDags, {
- page: 1,
- isLastPage: false,
- per_page: 2,
- totalItems: 5,
- ...pagination,
- });
- };
-
- const findDagsData = () => {
- return wrapper
- .findComponent(GlTableLite)
- .findAll('tbody tr')
- .wrappers.map((tr) => {
- return tr.findAll('td').wrappers.map((td) => {
- const timeAgo = td.findComponent(TimeAgo);
-
- if (timeAgo.exists()) {
- return {
- type: 'time',
- value: timeAgo.props('time'),
- };
- }
-
- return {
- type: 'text',
- value: td.text(),
- };
- });
- });
- };
-
- it('renders the table of Dags with data', () => {
- createWrapperWithDags();
-
- expect(findDagsData()).toEqual(
- mockDags.map((x) => [
- { type: 'text', value: x.dag_name },
- { type: 'text', value: x.schedule },
- { type: 'time', value: x.next_run },
- { type: 'text', value: String(x.is_active) },
- { type: 'text', value: String(x.is_paused) },
- { type: 'text', value: x.fileloc },
- ]),
- );
- });
-
- describe('Pagination behaviour', () => {
- it.each`
- pagination | expected
- ${{}} | ${{ value: 1, prevPage: null, nextPage: 2 }}
- ${{ page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: 3 }}
- ${{ isLastPage: true, page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: null }}
- `('with $pagination, sets pagination props', ({ pagination, expected }) => {
- createWrapperWithDags({ ...pagination });
-
- expect(findPagination().props()).toMatchObject(expected);
- });
-
- it('generates link for each page', () => {
- createWrapperWithDags();
-
- const generateLink = findPagination().props('linkGen');
-
- expect(generateLink(3)).toBe(`${TEST_HOST}/?page=3`);
- });
- });
- });
-});
diff --git a/spec/frontend/airflow/dags/components/mock_data.js b/spec/frontend/airflow/dags/components/mock_data.js
deleted file mode 100644
index 9547282517d..00000000000
--- a/spec/frontend/airflow/dags/components/mock_data.js
+++ /dev/null
@@ -1,67 +0,0 @@
-export const mockDags = [
- {
- id: 1,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 1',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
- {
- id: 2,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 2',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
- {
- id: 3,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 3',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
- {
- id: 4,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 4',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
- {
- id: 5,
- project_id: 7,
- created_at: '2023-01-05T14:07:02.975Z',
- updated_at: '2023-01-05T14:07:02.975Z',
- has_import_errors: false,
- is_active: false,
- is_paused: true,
- next_run: '2023-01-05T14:07:02.975Z',
- dag_name: 'Dag number 5',
- schedule: 'Manual',
- fileloc: '/opt/dag.py',
- },
-];
diff --git a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
index 0d6bc1b74fb..b2889d429b1 100644
--- a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
@@ -19,12 +19,6 @@ describe('AlertManagementEmptyState', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
const EmptyState = () => wrapper.findComponent(GlEmptyState);
describe('Empty state', () => {
diff --git a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
index 3a5fb99fdf1..3cc2d59295c 100644
--- a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
@@ -20,12 +20,6 @@ describe('AlertManagementList', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('Alert List Wrapper', () => {
it('should show the empty state when alerts are not enabled', () => {
expect(wrapper.findComponent(AlertManagementEmptyState).exists()).toBe(true);
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index 7fb4f2d2463..afd88e1a6ac 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -68,7 +68,7 @@ describe('AlertManagementTable', () => {
},
stubs,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
}),
);
@@ -79,9 +79,6 @@ describe('AlertManagementTable', () => {
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
mock.restore();
});
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/alert_spec.js
index 17d6cea23df..1ae8373016b 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/alert_spec.js
@@ -1,6 +1,6 @@
import * as Sentry from '@sentry/browser';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/alert';
jest.mock('@sentry/browser');
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 0e402e61bcc..202a0a04192 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
@@ -51,36 +51,24 @@ exports[`Alert integration settings form default state should match the default
</gl-link-stub>
</label>
- <gl-dropdown-stub
+ <gl-collapsible-listbox-stub
block="true"
category="primary"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
data-qa-selector="incident_templates_dropdown"
headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
+ icon=""
id="alert-integration-settings-issue-template"
+ items="[object Object]"
+ noresultstext="No results found"
+ placement="left"
+ popperoptions="[object Object]"
+ resetbuttonlabel=""
+ searchplaceholder="Search"
+ selected="selecte_tmpl"
size="medium"
- text="selecte_tmpl"
+ toggletext=""
variant="default"
- >
- <gl-dropdown-item-stub
- avatarurl=""
- data-qa-selector="incident_templates_item"
- iconcolor=""
- iconname=""
- iconrightarialabel=""
- iconrightname=""
- ischeckitem="true"
- secondarytext=""
- >
-
- No template selected
-
- </gl-dropdown-item-stub>
- </gl-dropdown-stub>
+ />
</gl-form-group-stub>
<gl-form-group-stub
diff --git a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
index 1e125bdfd3a..04dc0fef5da 100644
--- a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
+++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
@@ -19,13 +19,6 @@ describe('AlertMappingBuilder', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
beforeEach(() => {
mountComponent();
});
@@ -49,7 +42,7 @@ describe('AlertMappingBuilder', () => {
const fallbackColumnIcon = findColumnInRow(0, 3).findComponent(GlIcon);
expect(fallbackColumnIcon.exists()).toBe(true);
- expect(fallbackColumnIcon.attributes('name')).toBe('question');
+ expect(fallbackColumnIcon.attributes('name')).toBe('question-o');
expect(fallbackColumnIcon.attributes('title')).toBe(i18n.fallbackTooltip);
});
diff --git a/spec/frontend/alerts_settings/components/alerts_form_spec.js b/spec/frontend/alerts_settings/components/alerts_form_spec.js
index 33098282bf8..c4e5598ed39 100644
--- a/spec/frontend/alerts_settings/components/alerts_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_form_spec.js
@@ -22,12 +22,6 @@ describe('Alert integration settings form', () => {
});
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('default state', () => {
it('should match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
index 9983af873c2..76d0c12e434 100644
--- a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
@@ -42,13 +42,6 @@ describe('AlertIntegrationsList', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
beforeEach(() => {
mountComponent();
});
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index e0075aa71d9..4a0c7f65493 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -63,12 +63,6 @@ describe('AlertsSettingsForm', () => {
const findActionBtn = () => wrapper.findByTestId('payload-action-btn');
const findTabs = () => wrapper.findAllComponents(GlTab);
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
const selectOptionAtIndex = async (index) => {
const options = findSelect().findAll('option');
await options.at(index).setSelected();
@@ -97,9 +91,9 @@ describe('AlertsSettingsForm', () => {
expect(findFormFields().at(0).isVisible()).toBe(true);
});
- it('disables the dropdown and shows help text when multi integrations are not supported', async () => {
+ it('disables the dropdown and shows help text when multi integrations are not supported', () => {
createComponent({ props: { canAddIntegration: false } });
- expect(findSelect().attributes('disabled')).toBe('disabled');
+ expect(findSelect().attributes('disabled')).toBeDefined();
expect(findMultiSupportText().exists()).toBe(true);
});
@@ -433,13 +427,13 @@ describe('AlertsSettingsForm', () => {
it('should not be able to submit when no integration type is selected', async () => {
await selectOptionAtIndex(0);
- expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ expect(findSubmitButton().attributes('disabled')).toBeDefined();
});
it('should not be able to submit when HTTP integration form is invalid', async () => {
await selectOptionAtIndex(1);
await findFormFields().at(0).vm.$emit('input', '');
- expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ expect(findSubmitButton().attributes('disabled')).toBeDefined();
});
it('should be able to submit when HTTP integration form is valid', async () => {
@@ -452,7 +446,7 @@ describe('AlertsSettingsForm', () => {
await selectOptionAtIndex(2);
await findFormFields().at(0).vm.$emit('input', '');
- expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ expect(findSubmitButton().attributes('disabled')).toBeDefined();
});
it('should be able to submit when Prometheus integration form is valid', async () => {
@@ -482,7 +476,7 @@ describe('AlertsSettingsForm', () => {
});
await nextTick();
- expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ expect(findSubmitButton().attributes('disabled')).toBeDefined();
});
it('should disable submit button after click on validation failure', async () => {
@@ -490,7 +484,7 @@ describe('AlertsSettingsForm', () => {
findSubmitButton().trigger('click');
await nextTick();
- expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ expect(findSubmitButton().attributes('disabled')).toBeDefined();
});
it('should scroll to invalid field on validation failure', async () => {
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 a15c78cc456..8c5df06042c 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -30,7 +30,7 @@ import {
INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
DELETE_INTEGRATION_ERROR,
} from '~/alerts_settings/utils/error_messages';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
@@ -48,7 +48,7 @@ import {
} from './mocks/apollo_mock';
import mockIntegrations from './mocks/integrations.json';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('AlertsSettingsWrapper', () => {
let wrapper;
@@ -128,10 +128,6 @@ describe('AlertsSettingsWrapper', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent({
@@ -433,7 +429,7 @@ describe('AlertsSettingsWrapper', () => {
});
describe('Test alert', () => {
- it('makes `updateTestAlert` service call', async () => {
+ it('makes `updateTestAlert` service call', () => {
jest.spyOn(alertsUpdateService, 'updateTestAlert').mockResolvedValueOnce();
const testPayload = '{"title":"test"}';
findAlertsSettingsForm().vm.$emit('test-alert-payload', testPayload);
@@ -478,7 +474,7 @@ describe('AlertsSettingsWrapper', () => {
expect(destroyIntegrationHandler).toHaveBeenCalled();
});
- it('displays flash if mutation had a recoverable error', async () => {
+ it('displays alert if mutation had a recoverable error', async () => {
createComponentWithApollo({
destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors),
});
@@ -489,7 +485,7 @@ describe('AlertsSettingsWrapper', () => {
expect(createAlert).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
});
- it('displays flash if mutation had a non-recoverable error', async () => {
+ it('displays alert if mutation had a non-recoverable error', async () => {
createComponentWithApollo({
destroyHandler: jest.fn().mockRejectedValue('Error'),
});
diff --git a/spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap b/spec/frontend/analytics/cycle_analytics/components/__snapshots__/total_time_spec.js.snap
index 92927ef16ec..92927ef16ec 100644
--- a/spec/frontend/analytics/cycle_analytics/__snapshots__/total_time_spec.js.snap
+++ b/spec/frontend/analytics/cycle_analytics/components/__snapshots__/total_time_spec.js.snap
diff --git a/spec/frontend/analytics/cycle_analytics/base_spec.js b/spec/frontend/analytics/cycle_analytics/components/base_spec.js
index 58588ff49ce..87f3117c7f9 100644
--- a/spec/frontend/analytics/cycle_analytics/base_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/base_spec.js
@@ -19,7 +19,7 @@ import {
currentGroup,
stageCounts,
initialPaginationState as pagination,
-} from './mock_data';
+} from '../mock_data';
const selectedStageEvents = issueEvents.events;
const noDataSvgPath = 'path/to/no/data';
@@ -31,13 +31,15 @@ Vue.use(Vuex);
let wrapper;
-const { id: groupId, path: groupPath } = currentGroup;
+const { path } = currentGroup;
+const groupPath = `groups/${path}`;
const defaultState = {
currentGroup,
createdBefore,
createdAfter,
stageCounts,
- endpoints: { fullPath, groupId, groupPath },
+ groupPath,
+ namespace: { fullPath },
};
function createStore({ initialState = {}, initialGetters = {} }) {
@@ -93,11 +95,6 @@ describe('Value stream analytics component', () => {
wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents, pagination } });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders the path navigation component', () => {
expect(findPathNavigation().exists()).toBe(true);
});
@@ -139,8 +136,8 @@ describe('Value stream analytics component', () => {
it('passes the paths to the filter bar', () => {
expect(findFilters().props()).toEqual({
- groupId,
groupPath,
+ namespacePath: groupPath,
endDate: createdBefore,
hasDateRangeFilter: true,
hasProjectFilter: false,
@@ -157,6 +154,10 @@ describe('Value stream analytics component', () => {
expect(findPagination().exists()).toBe(true);
});
+ it('does not render a link to the value streams dashboard', () => {
+ expect(findOverviewMetrics().props('dashboardsPath')).toBeNull();
+ });
+
describe('with `cycleAnalyticsForGroups=true` license', () => {
beforeEach(() => {
wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } });
@@ -167,6 +168,23 @@ describe('Value stream analytics component', () => {
});
});
+ describe('with `groupLevelAnalyticsDashboard=true` license', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialState: {
+ features: { groupLevelAnalyticsDashboard: true },
+ },
+ });
+ });
+
+ it('renders a link to the value streams dashboard', () => {
+ expect(findOverviewMetrics().props('dashboardsPath')).toBeDefined();
+ expect(findOverviewMetrics().props('dashboardsPath')).toBe(
+ '/groups/foo/-/analytics/dashboards/value_streams_dashboard?query=full/path/to/foo',
+ );
+ });
+ });
+
describe('isLoading = true', () => {
beforeEach(() => {
wrapper = createComponent({
diff --git a/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js b/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js
index 2b26b202882..f1b3af39199 100644
--- a/spec/frontend/analytics/cycle_analytics/filter_bar_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/filter_bar_spec.js
@@ -85,7 +85,7 @@ describe('Filter bar', () => {
return shallowMount(FilterBar, {
store: initialStore,
propsData: {
- groupPath: 'foo',
+ namespacePath: 'foo',
},
stubs: {
UrlSync,
@@ -98,7 +98,6 @@ describe('Filter bar', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js b/spec/frontend/analytics/cycle_analytics/components/formatted_stage_count_spec.js
index 9be92bb92bc..6dd7e2e6223 100644
--- a/spec/frontend/analytics/cycle_analytics/formatted_stage_count_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/formatted_stage_count_spec.js
@@ -16,10 +16,6 @@ describe('Formatted Stage Count', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
stageCount | expectedOutput
${null} | ${'-'}
diff --git a/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js b/spec/frontend/analytics/cycle_analytics/components/path_navigation_spec.js
index 107e62035c3..9084cec1c53 100644
--- a/spec/frontend/analytics/cycle_analytics/path_navigation_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/path_navigation_spec.js
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Component from '~/analytics/cycle_analytics/components/path_navigation.vue';
-import { transformedProjectStagePathData, selectedStage } from './mock_data';
+import { transformedProjectStagePathData, selectedStage } from '../mock_data';
describe('Project PathNavigation', () => {
let wrapper = null;
@@ -50,8 +50,6 @@ describe('Project PathNavigation', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
- wrapper = null;
});
describe('displays correctly', () => {
diff --git a/spec/frontend/analytics/cycle_analytics/stage_table_spec.js b/spec/frontend/analytics/cycle_analytics/components/stage_table_spec.js
index cfccce7eae9..494be641263 100644
--- a/spec/frontend/analytics/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/stage_table_spec.js
@@ -5,14 +5,15 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
import { PAGINATION_SORT_FIELD_DURATION } from '~/analytics/cycle_analytics/constants';
-import { issueEvents, issueStage, reviewStage, reviewEvents } from './mock_data';
+import { issueEvents, issueStage, reviewStage, reviewEvents } from '../mock_data';
let wrapper = null;
let trackingSpy = null;
const noDataSvgPath = 'path/to/no/data';
const emptyStateTitle = 'Too much data';
-const notEnoughDataError = "We don't have enough data to show this stage.";
+const notEnoughDataError =
+ 'There are 0 items to show in this stage, for these filters, within this time range.';
const issueEventItems = issueEvents.events;
const reviewEventItems = reviewEvents.events;
const [firstIssueEvent] = issueEventItems;
@@ -51,10 +52,6 @@ function createComponent(props = {}, shallow = false) {
}
describe('StageTable', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('is loaded with data', () => {
beforeEach(() => {
wrapper = createComponent();
@@ -258,7 +255,6 @@ describe('StageTable', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
});
it('will display the pagination component', () => {
@@ -305,7 +301,6 @@ describe('StageTable', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
});
it('can sort the end event or duration', () => {
diff --git a/spec/frontend/analytics/cycle_analytics/total_time_spec.js b/spec/frontend/analytics/cycle_analytics/components/total_time_spec.js
index 47ee7aad8c4..6597b6fa3d5 100644
--- a/spec/frontend/analytics/cycle_analytics/total_time_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/total_time_spec.js
@@ -10,10 +10,6 @@ describe('TotalTime', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with a valid time object', () => {
it.each`
time
diff --git a/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js b/spec/frontend/analytics/cycle_analytics/components/value_stream_filters_spec.js
index 4f333e95d89..e3bcb0ab557 100644
--- a/spec/frontend/analytics/cycle_analytics/value_stream_filters_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/value_stream_filters_spec.js
@@ -8,14 +8,17 @@ import {
createdBefore as endDate,
currentGroup,
selectedProjects,
-} from './mock_data';
+} from '../mock_data';
+
+const { path } = currentGroup;
+const groupPath = `groups/${path}`;
function createComponent(props = {}) {
return shallowMount(ValueStreamFilters, {
propsData: {
selectedProjects,
- groupId: currentGroup.id,
- groupPath: currentGroup.fullPath,
+ groupPath,
+ namespacePath: currentGroup.fullPath,
startDate,
endDate,
...props,
@@ -34,11 +37,6 @@ describe('ValueStreamFilters', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('will render the filter bar', () => {
expect(findFilterBar().exists()).toBe(true);
});
diff --git a/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/analytics/cycle_analytics/components/value_stream_metrics_spec.js
index 948dc5c9be2..e1e955cec2c 100644
--- a/spec/frontend/analytics/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/value_stream_metrics_spec.js
@@ -8,10 +8,11 @@ import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
-import { createAlert } from '~/flash';
-import { group } from './mock_data';
+import ValueStreamsDashboardLink from '~/analytics/shared/components/value_streams_dashboard_link.vue';
+import { createAlert } from '~/alert';
+import { group } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('ValueStreamMetrics', () => {
let wrapper;
@@ -37,6 +38,7 @@ describe('ValueStreamMetrics', () => {
});
};
+ const findVSDLink = () => wrapper.findComponent(ValueStreamsDashboardLink);
const findMetrics = () => wrapper.findAllComponents(MetricTile);
const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group');
@@ -48,10 +50,6 @@ describe('ValueStreamMetrics', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with successful requests', () => {
beforeEach(() => {
mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
@@ -168,6 +166,28 @@ describe('ValueStreamMetrics', () => {
});
});
+ describe('Value Streams Dashboard Link', () => {
+ it('will render when a dashboardsPath is set', async () => {
+ wrapper = createComponent({
+ groupBy: VSA_METRICS_GROUPS,
+ dashboardsPath: 'fake-group-path',
+ });
+ await waitForPromises();
+
+ const vsdLink = findVSDLink();
+
+ expect(vsdLink.exists()).toBe(true);
+ expect(vsdLink.props()).toEqual({ requestPath: 'fake-group-path' });
+ });
+
+ it('does not render without a dashboardsPath', async () => {
+ wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
+ await waitForPromises();
+
+ expect(findVSDLink().exists()).toBe(false);
+ });
+ });
+
describe('with a request failing', () => {
beforeEach(async () => {
mockGetValueStreamSummaryMetrics = jest.fn().mockRejectedValue();
diff --git a/spec/frontend/analytics/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js
index f820f755400..f9587bf1967 100644
--- a/spec/frontend/analytics/cycle_analytics/mock_data.js
+++ b/spec/frontend/analytics/cycle_analytics/mock_data.js
@@ -214,11 +214,13 @@ export const group = {
id: 1,
name: 'foo',
path: 'foo',
- full_path: 'foo',
+ full_path: 'groups/foo',
avatar_url: `${TEST_HOST}/images/home/nasa.svg`,
};
export const currentGroup = convertObjectPropsToCamelCase(group, { deep: true });
+export const groupNamespace = { id: currentGroup.id, fullPath: `groups/${currentGroup.path}` };
+export const projectNamespace = { fullPath: 'some/cool/path' };
export const selectedProjects = [
{
diff --git a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
index 3030fca126b..b2ce8596c22 100644
--- a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
@@ -13,21 +13,13 @@ import {
createdBefore,
initialPaginationState,
reviewEvents,
+ projectNamespace as namespace,
} from '../mock_data';
-const { id: groupId, path: groupPath } = currentGroup;
-const mockMilestonesPath = 'mock-milestones.json';
-const mockLabelsPath = 'mock-labels.json';
-const mockRequestPath = 'some/cool/path';
+const { path: groupPath } = currentGroup;
+const mockMilestonesPath = `/${namespace.fullPath}/-/milestones.json`;
+const mockLabelsPath = `/${namespace.fullPath}/-/labels.json`;
const mockFullPath = '/namespace/-/analytics/value_stream_analytics/value_streams';
-const mockEndpoints = {
- fullPath: mockFullPath,
- requestPath: mockRequestPath,
- labelsPath: mockLabelsPath,
- milestonesPath: mockMilestonesPath,
- groupId,
- groupPath,
-};
const mockSetDateActionCommit = {
payload: { createdAfter, createdBefore },
type: 'SET_DATE_RANGE',
@@ -35,6 +27,7 @@ const mockSetDateActionCommit = {
const defaultState = {
...getters,
+ namespace,
selectedValueStream,
createdAfter,
createdBefore,
@@ -81,7 +74,8 @@ describe('Project Value Stream Analytics actions', () => {
const selectedAssigneeList = ['Assignee 1', 'Assignee 2'];
const selectedLabelList = ['Label 1', 'Label 2'];
const payload = {
- endpoints: mockEndpoints,
+ namespace,
+ groupPath,
selectedAuthor,
selectedMilestone,
selectedAssigneeList,
@@ -92,7 +86,7 @@ describe('Project Value Stream Analytics actions', () => {
groupEndpoint: 'foo',
labelsEndpoint: mockLabelsPath,
milestonesEndpoint: mockMilestonesPath,
- projectEndpoint: '/namespace/-/analytics/value_stream_analytics/value_streams',
+ projectEndpoint: namespace.fullPath,
};
it('will dispatch fetchValueStreams actions and commit SET_LOADING and INITIALIZE_VSA', () => {
@@ -193,7 +187,6 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
...defaultState,
- endpoints: mockEndpoints,
selectedStage,
};
mock = new MockAdapter(axios);
@@ -219,7 +212,6 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
...defaultState,
- endpoints: mockEndpoints,
selectedStage,
};
mock = new MockAdapter(axios);
@@ -243,7 +235,6 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
...defaultState,
- endpoints: mockEndpoints,
selectedStage,
};
mock = new MockAdapter(axios);
@@ -265,9 +256,7 @@ describe('Project Value Stream Analytics actions', () => {
const mockValueStreamPath = /\/analytics\/value_stream_analytics\/value_streams/;
beforeEach(() => {
- state = {
- endpoints: mockEndpoints,
- };
+ state = { namespace };
mock = new MockAdapter(axios);
mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_OK);
});
@@ -333,7 +322,7 @@ describe('Project Value Stream Analytics actions', () => {
beforeEach(() => {
state = {
- endpoints: mockEndpoints,
+ namespace,
selectedValueStream,
};
mock = new MockAdapter(axios);
diff --git a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
index 567fac81e1f..70b7454f4a0 100644
--- a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
@@ -17,12 +17,14 @@ import {
rawStageCounts,
stageCounts,
initialPaginationState as pagination,
+ projectNamespace as mockNamespace,
} from '../mock_data';
let state;
const rawEvents = rawIssueEvents.events;
const convertedEvents = issueEvents.events;
-const mockRequestPath = 'fake/request/path';
+const mockGroupPath = 'groups/path';
+const mockFeatures = { some: 'feature' };
const mockCreatedAfter = '2020-06-18';
const mockCreatedBefore = '2020-07-18';
@@ -64,19 +66,22 @@ describe('Project Value Stream Analytics mutations', () => {
const mockSetDatePayload = { createdAfter: mockCreatedAfter, createdBefore: mockCreatedBefore };
const mockInitialPayload = {
- endpoints: { requestPath: mockRequestPath },
currentGroup: { title: 'cool-group' },
id: 1337,
+ groupPath: mockGroupPath,
+ namespace: mockNamespace,
+ features: mockFeatures,
...mockSetDatePayload,
};
const mockInitializedObj = {
- endpoints: { requestPath: mockRequestPath },
...mockSetDatePayload,
};
it.each`
mutation | stateKey | value
- ${types.INITIALIZE_VSA} | ${'endpoints'} | ${{ requestPath: mockRequestPath }}
+ ${types.INITIALIZE_VSA} | ${'features'} | ${mockFeatures}
+ ${types.INITIALIZE_VSA} | ${'namespace'} | ${mockNamespace}
+ ${types.INITIALIZE_VSA} | ${'groupPath'} | ${mockGroupPath}
${types.INITIALIZE_VSA} | ${'createdAfter'} | ${mockCreatedAfter}
${types.INITIALIZE_VSA} | ${'createdBefore'} | ${mockCreatedBefore}
`('$mutation will set $stateKey', ({ mutation, stateKey, value }) => {
diff --git a/spec/frontend/analytics/cycle_analytics/utils_spec.js b/spec/frontend/analytics/cycle_analytics/utils_spec.js
index fe412bf7498..ab5d78bde51 100644
--- a/spec/frontend/analytics/cycle_analytics/utils_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/utils_spec.js
@@ -91,9 +91,9 @@ describe('Value stream analytics utils', () => {
const projectId = '5';
const createdAfter = '2021-09-01';
const createdBefore = '2021-11-06';
- const groupId = '146';
- const groupPath = 'fake-group';
- const fullPath = 'fake-group/fake-project';
+ const groupPath = 'groups/fake-group';
+ const namespaceName = 'Fake project';
+ const namespaceFullPath = 'fake-group/fake-project';
const labelsPath = '/fake-group/fake-project/-/labels.json';
const milestonesPath = '/fake-group/fake-project/-/milestones.json';
const requestPath = '/fake-group/fake-project/-/value_stream_analytics';
@@ -102,11 +102,11 @@ describe('Value stream analytics utils', () => {
projectId,
createdBefore,
createdAfter,
- fullPath,
+ namespaceName,
+ namespaceFullPath,
requestPath,
labelsPath,
milestonesPath,
- groupId,
groupPath,
};
@@ -124,14 +124,13 @@ describe('Value stream analytics utils', () => {
expect(res.createdAfter).toEqual(new Date(createdAfter));
});
+ it('sets the namespace', () => {
+ expect(res.namespace.name).toBe(namespaceName);
+ expect(res.namespace.fullPath).toBe(namespaceFullPath);
+ });
+
it('sets the endpoints', () => {
- const { endpoints } = res;
- expect(endpoints.fullPath).toBe(fullPath);
- expect(endpoints.requestPath).toBe(requestPath);
- expect(endpoints.labelsPath).toBe(labelsPath);
- expect(endpoints.milestonesPath).toBe(milestonesPath);
- expect(endpoints.groupId).toBe(parseInt(groupId, 10));
- expect(endpoints.groupPath).toBe(groupPath);
+ expect(res.groupPath).toBe(groupPath);
});
it('returns null when there is no stage', () => {
@@ -159,12 +158,15 @@ describe('Value stream analytics utils', () => {
describe('with features set', () => {
const fakeFeatures = { cycleAnalyticsForGroups: true };
+ beforeEach(() => {
+ window.gon = { licensed_features: fakeFeatures };
+ });
+
it('sets the feature flags', () => {
res = buildCycleAnalyticsInitialData({
...rawData,
- gon: { licensed_features: fakeFeatures },
});
- expect(res.features).toEqual(fakeFeatures);
+ expect(res.features).toMatchObject(fakeFeatures);
});
});
});
diff --git a/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
index c62bfb11f7b..70bfce41c82 100644
--- a/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
+++ b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
@@ -6,10 +6,6 @@ import ServicePingDisabled from '~/analytics/devops_reports/components/service_p
describe('~/analytics/devops_reports/components/service_ping_disabled.vue', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const createWrapper = ({ isAdmin = false } = {}) => {
wrapper = mountExtended(ServicePingDisabled, {
provide: {
diff --git a/spec/frontend/analytics/components/activity_chart_spec.js b/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js
index c26407f5c1d..4f8126aaacf 100644
--- a/spec/frontend/analytics/components/activity_chart_spec.js
+++ b/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js
@@ -13,11 +13,6 @@ describe('Activity Chart Bundle', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findChart = () => wrapper.findComponent(GlColumnChart);
const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]');
diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js
index 562e86529ee..5f0847e0db6 100644
--- a/spec/frontend/analytics/shared/components/daterange_spec.js
+++ b/spec/frontend/analytics/shared/components/daterange_spec.js
@@ -22,10 +22,6 @@ describe('Daterange component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDaterangePicker = () => wrapper.findComponent(GlDaterangePicker);
const findDateRangeIndicator = () => wrapper.findByTestId('daterange-picker-indicator');
@@ -90,18 +86,19 @@ describe('Daterange component', () => {
});
describe('set', () => {
- it('emits the change event with an object containing startDate and endDate', () => {
+ it('emits the change event with an object containing startDate and endDate', async () => {
const startDate = new Date('2019-10-01');
const endDate = new Date('2019-10-05');
- wrapper.vm.dateRange = { startDate, endDate };
- expect(wrapper.emitted().change).toEqual([[{ startDate, endDate }]]);
+ await findDaterangePicker().vm.$emit('input', { startDate, endDate });
+
+ expect(wrapper.emitted('change')).toEqual([[{ startDate, endDate }]]);
});
});
describe('get', () => {
- it("returns value of dateRange from state's startDate and endDate", () => {
- expect(wrapper.vm.dateRange).toEqual({
+ it("datepicker to have default of dateRange from state's startDate and endDate", () => {
+ expect(findDaterangePicker().props('value')).toEqual({
startDate: defaultProps.startDate,
endDate: defaultProps.endDate,
});
diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js
index e0bfff3e664..d7e6606cdc6 100644
--- a/spec/frontend/analytics/shared/components/metric_popover_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js
@@ -34,10 +34,6 @@ describe('MetricPopover', () => {
const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon);
const findMetricDetailsIcon = () => findMetricLink().findComponent(GlIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the metric label', () => {
wrapper = createComponent({ metric: MOCK_METRIC });
expect(findMetricLabel().text()).toBe(MOCK_METRIC.label);
diff --git a/spec/frontend/analytics/shared/components/metric_tile_spec.js b/spec/frontend/analytics/shared/components/metric_tile_spec.js
index 980dfad9eb0..9da5ed0fb07 100644
--- a/spec/frontend/analytics/shared/components/metric_tile_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_tile_spec.js
@@ -2,7 +2,7 @@ import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import MetricPopover from '~/analytics/shared/components/metric_popover.vue';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
jest.mock('~/lib/utils/url_utility');
@@ -21,10 +21,6 @@ describe('MetricTile', () => {
const findSingleStat = () => wrapper.findComponent(GlSingleStat);
const findPopover = () => wrapper.findComponent(MetricPopover);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe('links', () => {
it('when the metric has links, it redirects the user on click', () => {
@@ -38,7 +34,7 @@ describe('MetricTile', () => {
const singleStat = findSingleStat();
singleStat.vm.$emit('click');
- expect(redirectTo).toHaveBeenCalledWith('foo/bar');
+ expect(redirectTo).toHaveBeenCalledWith('foo/bar'); // eslint-disable-line import/no-deprecated
});
it("when the metric doesn't have links, it won't the user on click", () => {
@@ -47,7 +43,7 @@ describe('MetricTile', () => {
const singleStat = findSingleStat();
singleStat.vm.$emit('click');
- expect(redirectTo).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 3871fd530d8..33801fb8552 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlTruncate } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlTruncate, GlSearchBoxByType } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
@@ -31,6 +31,7 @@ const projects = [
const MockGlDropdown = stubComponent(GlDropdown, {
template: `
<div>
+ <slot name="header"></slot>
<div data-testid="vsa-highlighted-items">
<slot name="highlighted-items"></slot>
</div>
@@ -70,10 +71,6 @@ describe('ProjectsDropdownFilter component', () => {
return waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findHighlightedItems = () => wrapper.findByTestId('vsa-highlighted-items');
const findUnhighlightedItems = () => wrapper.findByTestId('vsa-default-items');
const findClearAllButton = () => wrapper.findByText('Clear all');
@@ -116,6 +113,8 @@ describe('ProjectsDropdownFilter component', () => {
const selectedIds = () => wrapper.vm.selectedProjects.map(({ id }) => id);
+ const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
+
describe('queryParams are applied when fetching data', () => {
beforeEach(() => {
createComponent({
@@ -127,9 +126,7 @@ describe('ProjectsDropdownFilter component', () => {
});
it('applies the correct queryParams when making an api call', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ searchTerm: 'gitlab' });
+ findSearchBoxByType().vm.$emit('input', 'gitlab');
expect(spyQuery).toHaveBeenCalledTimes(1);
@@ -148,6 +145,7 @@ describe('ProjectsDropdownFilter component', () => {
describe('highlighted items', () => {
const blockDefaultProps = { multiSelect: true };
+
beforeEach(() => {
createComponent(blockDefaultProps);
});
@@ -155,6 +153,7 @@ describe('ProjectsDropdownFilter component', () => {
describe('with no project selected', () => {
it('does not render the highlighted items', async () => {
await createWithMockDropdown(blockDefaultProps);
+
expect(findSelectedDropdownItems().length).toBe(0);
});
@@ -192,8 +191,7 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedProjectsLabel().text()).toBe('2 projects selected');
- findClearAllButton().trigger('click');
- await nextTick();
+ await findClearAllButton().trigger('click');
expect(findSelectedProjectsLabel().text()).toBe('Select projects');
});
@@ -205,16 +203,14 @@ describe('ProjectsDropdownFilter component', () => {
await createWithMockDropdown({ multiSelect: true });
selectDropdownItemAtIndex(0);
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ searchTerm: 'this is a very long search string' });
+ findSearchBoxByType().vm.$emit('input', 'this is a very long search string');
});
- it('renders the highlighted items', async () => {
+ it('renders the highlighted items', () => {
expect(findUnhighlightedItems().findAll('li').length).toBe(1);
});
- it('hides the unhighlighted items that do not match the string', async () => {
+ it('hides the unhighlighted items that do not match the string', () => {
expect(findUnhighlightedItems().findAll('li').length).toBe(1);
expect(findUnhighlightedItems().text()).toContain('No matching results');
});
@@ -355,17 +351,19 @@ describe('ProjectsDropdownFilter component', () => {
it('should remove from selection when clicked again', () => {
selectDropdownItemAtIndex(0);
+
expect(selectedIds()).toEqual([projects[0].id]);
selectDropdownItemAtIndex(0);
+
expect(selectedIds()).toEqual([]);
});
it('renders the correct placeholder text when multiple projects are selected', async () => {
selectDropdownItemAtIndex(0);
selectDropdownItemAtIndex(1);
-
await nextTick();
+
expect(findDropdownButton().text()).toBe('2 projects selected');
});
});
diff --git a/spec/frontend/analytics/shared/utils_spec.js b/spec/frontend/analytics/shared/utils_spec.js
index b48e2d971b5..24af7b836d5 100644
--- a/spec/frontend/analytics/shared/utils_spec.js
+++ b/spec/frontend/analytics/shared/utils_spec.js
@@ -5,6 +5,7 @@ import {
extractPaginationQueryParameters,
getDataZoomOption,
prepareTimeMetricsData,
+ generateValueStreamsDashboardLink,
} from '~/analytics/shared/utils';
import { slugify } from '~/lib/utils/text_utility';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -212,3 +213,30 @@ describe('prepareTimeMetricsData', () => {
]);
});
});
+
+describe('generateValueStreamsDashboardLink', () => {
+ it.each`
+ groupPath | projectPaths | result
+ ${''} | ${[]} | ${''}
+ ${'groups/fake-group'} | ${[]} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard'}
+ ${'groups/fake-group'} | ${['fake-path/project_1']} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard?query=fake-path/project_1'}
+ ${'groups/fake-group'} | ${['fake-path/project_1', 'fake-path/project_2']} | ${'/groups/fake-group/-/analytics/dashboards/value_streams_dashboard?query=fake-path/project_1,fake-path/project_2'}
+ `(
+ 'generates the dashboard link when groupPath=$groupPath and projectPaths=$projectPaths',
+ ({ groupPath, projectPaths, result }) => {
+ expect(generateValueStreamsDashboardLink(groupPath, projectPaths)).toBe(result);
+ },
+ );
+
+ describe('with a relative url rool set', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('with includes a relative path if one is set', () => {
+ expect(generateValueStreamsDashboardLink('groups/fake-path', ['project_1'])).toBe(
+ '/foobar/groups/fake-path/-/analytics/dashboards/value_streams_dashboard?query=project_1',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/analytics/usage_trends/components/app_spec.js b/spec/frontend/analytics/usage_trends/components/app_spec.js
index c732dc22322..f9338661ebf 100644
--- a/spec/frontend/analytics/usage_trends/components/app_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/app_spec.js
@@ -15,11 +15,6 @@ describe('UsageTrendsApp', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays the usage counts component', () => {
expect(wrapper.findComponent(UsageCounts).exists()).toBe(true);
});
diff --git a/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
index f4cbc56be5c..a71ce090955 100644
--- a/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/usage_counts_spec.js
@@ -26,10 +26,6 @@ describe('UsageCounts', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat);
diff --git a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
index ad6089f74b5..322d05e663a 100644
--- a/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/usage_trends_count_chart_spec.js
@@ -45,11 +45,6 @@ describe('UsageTrendsCountChart', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findLoader = () => wrapper.findComponent(ChartSkeletonLoader);
const findChart = () => wrapper.findComponent(GlLineChart);
const findAlert = () => wrapper.findComponent(GlAlert);
diff --git a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
index e7abd4d4323..20836d7cc70 100644
--- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
@@ -42,11 +42,6 @@ describe('UsersChart', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findLoader = () => wrapper.findComponent(ChartSkeletonLoader);
const findAlert = () => wrapper.findComponent(GlAlert);
const findChart = () => wrapper.findComponent(GlAreaChart);
diff --git a/spec/frontend/api/alert_management_alerts_api_spec.js b/spec/frontend/api/alert_management_alerts_api_spec.js
index 507f659a170..86052a05b76 100644
--- a/spec/frontend/api/alert_management_alerts_api_spec.js
+++ b/spec/frontend/api/alert_management_alerts_api_spec.js
@@ -9,7 +9,6 @@ import {
describe('~/api/alert_management_alerts_api.js', () => {
let mock;
- let originalGon;
const projectId = 1;
const alertIid = 2;
@@ -19,13 +18,11 @@ describe('~/api/alert_management_alerts_api.js', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = { api_version: 'v4' };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('fetchAlertMetricImages', () => {
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
index 0315db02cf2..642edb33624 100644
--- a/spec/frontend/api/groups_api_spec.js
+++ b/spec/frontend/api/groups_api_spec.js
@@ -10,23 +10,18 @@ const mockUrlRoot = '/gitlab';
const mockGroupId = '99';
describe('GroupsApi', () => {
- let originalGon;
let mock;
- const dummyGon = {
- api_version: mockApiVersion,
- relative_url_root: mockUrlRoot,
- };
-
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: mockApiVersion,
+ relative_url_root: mockUrlRoot,
+ };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('updateGroup', () => {
diff --git a/spec/frontend/api/packages_api_spec.js b/spec/frontend/api/packages_api_spec.js
index 5f517bcf358..37c4b926ec2 100644
--- a/spec/frontend/api/packages_api_spec.js
+++ b/spec/frontend/api/packages_api_spec.js
@@ -6,22 +6,18 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Api', () => {
const dummyApiVersion = 'v3000';
const dummyUrlRoot = '/gitlab';
- const dummyGon = {
- api_version: dummyApiVersion,
- relative_url_root: dummyUrlRoot,
- };
- let originalGon;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+ };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('packages', () => {
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 2d4ed39dad0..4ceed885e6e 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -7,23 +7,17 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/projects_api.js', () => {
let mock;
- let originalGon;
const projectId = 1;
- const setfullPathProjectSearch = (value) => {
- window.gon.features.fullPathProjectSearch = value;
- };
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
- window.gon = { api_version: 'v7', features: { fullPathProjectSearch: true } };
+ window.gon = { api_version: 'v7' };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('getProjects', () => {
@@ -71,17 +65,18 @@ describe('~/api/projects_api.js', () => {
expect(data.data).toEqual(expectedProjects);
});
});
+ });
- it('does not search namespaces if fullPathProjectSearch is disabled', () => {
- setfullPathProjectSearch(false);
- const expectedParams = { params: { per_page: 20, search: 'group/project1', simple: true } };
- const query = 'group/project1';
+ describe('createProject', () => {
+ it('posts to the correct URL and returns the data', () => {
+ const body = { name: 'test project' };
+ const expectedUrl = '/api/v7/projects.json';
+ const expectedRes = { id: 999, name: 'test project' };
- mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
+ mock.onPost(expectedUrl, body).replyOnce(HTTP_STATUS_OK, { data: expectedRes });
- return projectsApi.getProjects(query, options).then(({ data }) => {
- expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
- expect(data.data).toEqual(expectedProjects);
+ return projectsApi.createProject(body).then(({ data }) => {
+ expect(data).toStrictEqual(expectedRes);
});
});
});
diff --git a/spec/frontend/api/tags_api_spec.js b/spec/frontend/api/tags_api_spec.js
index af3533f52b7..0a1177d4f60 100644
--- a/spec/frontend/api/tags_api_spec.js
+++ b/spec/frontend/api/tags_api_spec.js
@@ -5,20 +5,17 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/tags_api.js', () => {
let mock;
- let originalGon;
const projectId = 1;
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = { api_version: 'v7' };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('getTag', () => {
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index 4d0252aad23..a879c229581 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -1,6 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
-import { followUser, unfollowUser, associationsCount, updateUserStatus } from '~/api/user_api';
+import projects from 'test_fixtures/api/users/projects/get.json';
+import {
+ followUser,
+ unfollowUser,
+ associationsCount,
+ updateUserStatus,
+ getUserProjects,
+} from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
@@ -12,19 +19,16 @@ import { timeRanges } from '~/vue_shared/constants';
describe('~/api/user_api', () => {
let axiosMock;
- let originalGon;
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = { api_version: 'v4' };
});
afterEach(() => {
axiosMock.restore();
axiosMock.resetHistory();
- window.gon = originalGon;
});
describe('followUser', () => {
@@ -94,4 +98,18 @@ describe('~/api/user_api', () => {
expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual(expectedData);
});
});
+
+ describe('getUserProjects', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const expectedUrl = '/api/v4/users/1/projects';
+ const expectedResponse = { data: projects };
+
+ axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
+
+ await expect(getUserProjects(1)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.get[0].url).toBe(expectedUrl);
+ });
+ });
});
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 6fd106502c4..4ef37311e51 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -10,27 +10,22 @@ import {
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
-jest.mock('~/flash');
-
describe('Api', () => {
const dummyApiVersion = 'v3000';
const dummyUrlRoot = '/gitlab';
- const dummyGon = {
- api_version: dummyApiVersion,
- relative_url_root: dummyUrlRoot,
- };
- let originalGon;
+
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- originalGon = window.gon;
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+ };
});
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('buildUrl', () => {
@@ -1423,7 +1418,7 @@ describe('Api', () => {
describe('when service data increment counter is called with feature flag disabled', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: false };
+ gon.features = { usageDataApi: false };
});
it('returns null', () => {
@@ -1437,7 +1432,7 @@ describe('Api', () => {
describe('when service data increment counter is called', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: true };
+ gon.features = { usageDataApi: true };
});
it('resolves the Promise', () => {
@@ -1468,7 +1463,7 @@ describe('Api', () => {
describe('when service data increment unique users is called with feature flag disabled', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: false };
+ gon.features = { usageDataApi: false };
});
it('returns null and does not call the endpoint', () => {
@@ -1483,7 +1478,7 @@ describe('Api', () => {
describe('when service data increment unique users is called', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: true };
+ gon.features = { usageDataApi: true };
});
it('resolves the Promise', () => {
@@ -1500,7 +1495,7 @@ describe('Api', () => {
describe('when user is not set and feature flag enabled', () => {
beforeEach(() => {
- gon.features = { ...gon.features, usageDataApi: true };
+ gon.features = { usageDataApi: true };
});
it('returns null and does not call the endpoint', () => {
diff --git a/spec/frontend/approvals/mock_data.js b/spec/frontend/approvals/mock_data.js
new file mode 100644
index 00000000000..e0e90c09791
--- /dev/null
+++ b/spec/frontend/approvals/mock_data.js
@@ -0,0 +1,10 @@
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
+
+export const createCanApproveResponse = () => {
+ const response = JSON.parse(JSON.stringify(approvedByCurrentUser));
+ response.data.project.mergeRequest.userPermissions.canApprove = true;
+ response.data.project.mergeRequest.approved = false;
+ response.data.project.mergeRequest.approvedBy.nodes = [];
+
+ return response;
+};
diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/artifacts/components/artifact_row_spec.js
deleted file mode 100644
index 2a7156bf480..00000000000
--- a/spec/frontend/artifacts/components/artifact_row_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import { GlBadge, GlButton, GlFriendlyWrap } from '@gitlab/ui';
-import mockGetJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import ArtifactRow from '~/artifacts/components/artifact_row.vue';
-
-describe('ArtifactRow component', () => {
- let wrapper;
-
- const artifact = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0];
-
- const findName = () => wrapper.findByTestId('job-artifact-row-name');
- const findBadge = () => wrapper.findComponent(GlBadge);
- const findSize = () => wrapper.findByTestId('job-artifact-row-size');
- const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button');
- const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
-
- const createComponent = ({ canDestroyArtifacts = true } = {}) => {
- wrapper = shallowMountExtended(ArtifactRow, {
- propsData: {
- artifact,
- isLoading: false,
- isLastRow: false,
- },
- provide: { canDestroyArtifacts },
- stubs: { GlBadge, GlButton, GlFriendlyWrap },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('artifact details', () => {
- beforeEach(async () => {
- createComponent();
-
- await waitForPromises();
- });
-
- it('displays the artifact name and type', () => {
- expect(findName().text()).toContain(artifact.name);
- expect(findBadge().text()).toBe(artifact.fileType.toLowerCase());
- });
-
- it('displays the artifact size', () => {
- expect(findSize().text()).toBe(numberToHumanSize(artifact.size));
- });
-
- it('displays the download button as a link to the download path', () => {
- expect(findDownloadButton().attributes('href')).toBe(artifact.downloadPath);
- });
- });
-
- describe('delete button', () => {
- it('does not show when user does not have permission', () => {
- createComponent({ canDestroyArtifacts: false });
-
- expect(findDeleteButton().exists()).toBe(false);
- });
-
- it('shows when user has permission', () => {
- createComponent();
-
- expect(findDeleteButton().exists()).toBe(true);
- });
-
- it('emits the delete event when clicked', async () => {
- createComponent();
-
- expect(wrapper.emitted('delete')).toBeUndefined();
-
- findDeleteButton().trigger('click');
- await waitForPromises();
-
- expect(wrapper.emitted('delete')).toBeDefined();
- });
- });
-});
diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
deleted file mode 100644
index dbe4598f599..00000000000
--- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js
+++ /dev/null
@@ -1,363 +0,0 @@
-import { GlLoadingIcon, GlTable, GlLink, GlBadge, GlPagination, GlModal } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
-import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
-import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
-import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ARCHIVE_FILE_TYPE, JOBS_PER_PAGE, I18N_FETCH_ERROR } from '~/artifacts/constants';
-import { totalArtifactsSizeForJob } from '~/artifacts/utils';
-import { createAlert } from '~/flash';
-
-jest.mock('~/flash');
-
-Vue.use(VueApollo);
-
-describe('JobArtifactsTable component', () => {
- let wrapper;
- let requestHandlers;
-
- const findBanner = () => wrapper.findComponent(FeedbackBanner);
-
- const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
- const findTable = () => wrapper.findComponent(GlTable);
- const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails);
- const findDetailsInRow = (i) =>
- findTable().findAll('tbody tr').at(i).findComponent(ArtifactsTableRowDetails);
-
- const findCount = () => wrapper.findByTestId('job-artifacts-count');
- const findCountAt = (i) => wrapper.findAllByTestId('job-artifacts-count').at(i);
-
- const findModal = () => wrapper.findComponent(GlModal);
-
- const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
- const findSuccessfulJobStatus = () => findStatuses().at(0);
- const findFailedJobStatus = () => findStatuses().at(1);
-
- const findLinks = () => wrapper.findAllComponents(GlLink);
- const findJobLink = () => findLinks().at(0);
- const findPipelineLink = () => findLinks().at(1);
- const findRefLink = () => findLinks().at(2);
- const findCommitLink = () => findLinks().at(3);
-
- const findSize = () => wrapper.findByTestId('job-artifacts-size');
- const findCreated = () => wrapper.findByTestId('job-artifacts-created');
-
- const findDownloadButton = () => wrapper.findByTestId('job-artifacts-download-button');
- const findBrowseButton = () => wrapper.findByTestId('job-artifacts-browse-button');
- const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button');
- const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
-
- const findPagination = () => wrapper.findComponent(GlPagination);
- const setPage = async (page) => {
- findPagination().vm.$emit('input', page);
- await waitForPromises();
- };
-
- let enoughJobsToPaginate = [...getJobArtifactsResponse.data.project.jobs.nodes];
- while (enoughJobsToPaginate.length <= JOBS_PER_PAGE) {
- enoughJobsToPaginate = [
- ...enoughJobsToPaginate,
- ...getJobArtifactsResponse.data.project.jobs.nodes,
- ];
- }
- const getJobArtifactsResponseThatPaginates = {
- data: { project: { jobs: { nodes: enoughJobsToPaginate } } },
- };
-
- const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
- const archiveArtifact = job.artifacts.nodes.find(
- (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
- );
-
- const createComponent = (
- handlers = {
- getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
- },
- data = {},
- canDestroyArtifacts = true,
- ) => {
- requestHandlers = handlers;
- wrapper = mountExtended(JobArtifactsTable, {
- apolloProvider: createMockApollo([
- [getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
- ]),
- provide: {
- projectPath: 'project/path',
- canDestroyArtifacts,
- artifactsManagementFeedbackImagePath: 'banner/image/path',
- },
- data() {
- return data;
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders feedback banner', () => {
- createComponent();
-
- expect(findBanner().exists()).toBe(true);
- });
-
- it('when loading, shows a loading state', () => {
- createComponent();
-
- expect(findLoadingState().exists()).toBe(true);
- });
-
- it('on error, shows an alert', async () => {
- createComponent({
- getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')),
- });
-
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({ message: I18N_FETCH_ERROR });
- });
-
- it('with data, renders the table', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(findTable().exists()).toBe(true);
- });
-
- describe('job details', () => {
- beforeEach(async () => {
- createComponent();
-
- await waitForPromises();
- });
-
- it('shows the artifact count', () => {
- expect(findCount().text()).toBe(`${job.artifacts.nodes.length} files`);
- });
-
- it('shows the job status as an icon for a successful job', () => {
- expect(findSuccessfulJobStatus().findComponent(CiIcon).exists()).toBe(true);
- expect(findSuccessfulJobStatus().findComponent(GlBadge).exists()).toBe(false);
- });
-
- it('shows the job status as a badge for other job statuses', () => {
- expect(findFailedJobStatus().findComponent(GlBadge).exists()).toBe(true);
- expect(findFailedJobStatus().findComponent(CiIcon).exists()).toBe(false);
- });
-
- it('shows links to the job, pipeline, ref, and commit', () => {
- expect(findJobLink().text()).toBe(job.name);
- expect(findJobLink().attributes('href')).toBe(job.webPath);
-
- expect(findPipelineLink().text()).toBe(`#${getIdFromGraphQLId(job.pipeline.id)}`);
- expect(findPipelineLink().attributes('href')).toBe(job.pipeline.path);
-
- expect(findRefLink().text()).toBe(job.refName);
- expect(findRefLink().attributes('href')).toBe(job.refPath);
-
- expect(findCommitLink().text()).toBe(job.shortSha);
- expect(findCommitLink().attributes('href')).toBe(job.commitPath);
- });
-
- it('shows the total size of artifacts', () => {
- expect(findSize().text()).toBe(totalArtifactsSizeForJob(job));
- });
-
- it('shows the created time', () => {
- expect(findCreated().text()).toBe('5 years ago');
- });
-
- describe('row expansion', () => {
- it('toggles the visibility of the row details', async () => {
- expect(findDetailsRows().length).toBe(0);
-
- findCount().trigger('click');
- await waitForPromises();
-
- expect(findDetailsRows().length).toBe(1);
-
- findCount().trigger('click');
- await waitForPromises();
-
- expect(findDetailsRows().length).toBe(0);
- });
-
- it('expands and collapses jobs', async () => {
- // both jobs start collapsed
- expect(findDetailsInRow(0).exists()).toBe(false);
- expect(findDetailsInRow(1).exists()).toBe(false);
-
- findCountAt(0).trigger('click');
- await waitForPromises();
-
- // first job is expanded, second row has its details
- expect(findDetailsInRow(0).exists()).toBe(false);
- expect(findDetailsInRow(1).exists()).toBe(true);
- expect(findDetailsInRow(2).exists()).toBe(false);
-
- findCountAt(1).trigger('click');
- await waitForPromises();
-
- // both jobs are expanded, each has details below it
- expect(findDetailsInRow(0).exists()).toBe(false);
- expect(findDetailsInRow(1).exists()).toBe(true);
- expect(findDetailsInRow(2).exists()).toBe(false);
- expect(findDetailsInRow(3).exists()).toBe(true);
-
- findCountAt(0).trigger('click');
- await waitForPromises();
-
- // first job collapsed, second job expanded
- expect(findDetailsInRow(0).exists()).toBe(false);
- expect(findDetailsInRow(1).exists()).toBe(false);
- expect(findDetailsInRow(2).exists()).toBe(true);
- });
-
- it('keeps the job expanded when an artifact is deleted', async () => {
- findCount().trigger('click');
- await waitForPromises();
-
- expect(findDetailsInRow(0).exists()).toBe(false);
- expect(findDetailsInRow(1).exists()).toBe(true);
-
- findArtifactDeleteButton().trigger('click');
- await waitForPromises();
-
- expect(findModal().props('visible')).toBe(true);
-
- wrapper.findComponent(ArtifactDeleteModal).vm.$emit('primary');
- await waitForPromises();
-
- expect(findDetailsInRow(0).exists()).toBe(false);
- expect(findDetailsInRow(1).exists()).toBe(true);
- });
- });
- });
-
- describe('download button', () => {
- it('is a link to the download path for the archive artifact', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(findDownloadButton().attributes('href')).toBe(archiveArtifact.downloadPath);
- });
-
- it('is disabled when there is no download path', async () => {
- const jobWithoutDownloadPath = {
- ...job,
- archive: { downloadPath: null },
- };
-
- createComponent(
- { getJobArtifactsQuery: jest.fn() },
- { jobArtifacts: [jobWithoutDownloadPath] },
- );
-
- await waitForPromises();
-
- expect(findDownloadButton().attributes('disabled')).toBe('disabled');
- });
- });
-
- describe('browse button', () => {
- it('is a link to the browse path for the job', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(findBrowseButton().attributes('href')).toBe(job.browseArtifactsPath);
- });
-
- it('is disabled when there is no browse path', async () => {
- const jobWithoutBrowsePath = {
- ...job,
- browseArtifactsPath: null,
- };
-
- createComponent(
- { getJobArtifactsQuery: jest.fn() },
- { jobArtifacts: [jobWithoutBrowsePath] },
- );
-
- await waitForPromises();
-
- expect(findBrowseButton().attributes('disabled')).toBe('disabled');
- });
- });
-
- describe('delete button', () => {
- it('does not show when user does not have permission', async () => {
- createComponent({}, {}, false);
-
- await waitForPromises();
-
- expect(findDeleteButton().exists()).toBe(false);
- });
-
- it('shows a disabled delete button for now (coming soon)', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(findDeleteButton().attributes('disabled')).toBe('disabled');
- });
- });
-
- describe('pagination', () => {
- const { pageInfo } = getJobArtifactsResponse.data.project.jobs;
-
- beforeEach(async () => {
- createComponent(
- {
- getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates),
- },
- {
- count: enoughJobsToPaginate.length,
- pageInfo,
- },
- );
-
- await waitForPromises();
- });
-
- it('renders pagination and passes page props', () => {
- expect(findPagination().exists()).toBe(true);
- expect(findPagination().props()).toMatchObject({
- value: wrapper.vm.pagination.currentPage,
- prevPage: wrapper.vm.prevPage,
- nextPage: wrapper.vm.nextPage,
- });
- });
-
- it('updates query variables when going to previous page', () => {
- return setPage(1).then(() => {
- expect(wrapper.vm.queryVariables).toMatchObject({
- projectPath: 'project/path',
- nextPageCursor: undefined,
- prevPageCursor: pageInfo.startCursor,
- });
- });
- });
-
- it('updates query variables when going to next page', () => {
- return setPage(2).then(() => {
- expect(wrapper.vm.queryVariables).toMatchObject({
- lastPageSize: null,
- nextPageCursor: pageInfo.endCursor,
- prevPageCursor: '',
- });
- });
- });
- });
-});
diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
index ca94acfa444..8dafff350f2 100644
--- a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
+++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
@@ -1,7 +1,8 @@
import { GlFormCheckbox, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import UpdateKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
import GetKeepLatestArtifactApplicationSetting from '~/artifacts_settings/graphql/queries/get_keep_latest_artifact_application_setting.query.graphql';
@@ -28,7 +29,9 @@ const keepLatestArtifactApplicationMock = {
};
const keepLatestArtifactMockResponse = {
- data: { ciCdSettingsUpdate: { errors: [], __typename: 'CiCdSettingsUpdatePayload' } },
+ data: {
+ projectCiCdSettingsUpdate: { errors: [], __typename: 'ProjectCiCdSettingsUpdatePayload' },
+ },
};
describe('Keep latest artifact checkbox', () => {
@@ -78,8 +81,6 @@ describe('Keep latest artifact checkbox', () => {
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
apolloProvider = null;
});
@@ -104,19 +105,16 @@ describe('Keep latest artifact checkbox', () => {
});
describe('when application keep latest artifact setting is enabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
+ await waitForPromises();
});
- it('sets correct setting value in checkbox with query result', async () => {
- await nextTick();
-
+ it('sets correct setting value in checkbox with query result', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('checkbox is enabled when application setting is enabled', async () => {
- await nextTick();
-
+ it('checkbox is enabled when application setting is enabled', () => {
expect(findCheckbox().attributes('disabled')).toBeUndefined();
});
});
diff --git a/spec/frontend/authentication/password/components/password_input_spec.js b/spec/frontend/authentication/password/components/password_input_spec.js
new file mode 100644
index 00000000000..5b2a9da993b
--- /dev/null
+++ b/spec/frontend/authentication/password/components/password_input_spec.js
@@ -0,0 +1,64 @@
+import { GlFormInput, GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import PasswordInput from '~/authentication/password/components/password_input.vue';
+import { SHOW_PASSWORD, HIDE_PASSWORD } from '~/authentication/password/constants';
+
+describe('PasswordInput', () => {
+ let wrapper;
+ const propsData = {
+ title: 'This field is required',
+ id: 'new_user_password',
+ minimumPasswordLength: '8',
+ qaSelector: 'new_user_password_field',
+ testid: 'new_user_password',
+ autocomplete: 'new-password',
+ name: 'new_user',
+ };
+
+ const findPasswordInput = () => wrapper.findComponent(GlFormInput);
+ const findToggleButton = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ return shallowMount(PasswordInput, {
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ it('sets password input attributes correctly', () => {
+ expect(findPasswordInput().attributes('id')).toBe(propsData.id);
+ expect(findPasswordInput().attributes('autocomplete')).toBe(propsData.autocomplete);
+ expect(findPasswordInput().attributes('name')).toBe(propsData.name);
+ expect(findPasswordInput().attributes('minlength')).toBe(propsData.minimumPasswordLength);
+ expect(findPasswordInput().attributes('data-qa-selector')).toBe(propsData.qaSelector);
+ expect(findPasswordInput().attributes('data-testid')).toBe(propsData.testid);
+ expect(findPasswordInput().attributes('title')).toBe(propsData.title);
+ });
+
+ describe('when the show password button is clicked', () => {
+ beforeEach(() => {
+ findToggleButton().vm.$emit('click');
+ });
+
+ it('displays hide password button', () => {
+ expect(findPasswordInput().attributes('type')).toBe('text');
+ expect(findToggleButton().attributes('icon')).toBe('eye-slash');
+ expect(findToggleButton().attributes('aria-label')).toBe(HIDE_PASSWORD);
+ });
+
+ describe('when the hide password button is clicked', () => {
+ beforeEach(() => {
+ findToggleButton().vm.$emit('click');
+ });
+
+ it('displays show password button', () => {
+ expect(findPasswordInput().attributes('type')).toBe('password');
+ expect(findToggleButton().attributes('icon')).toBe('eye');
+ expect(findToggleButton().attributes('aria-label')).toBe(SHOW_PASSWORD);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
index 694c16a85c4..8ecef710e03 100644
--- a/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/manage_two_factor_form_spec.js
@@ -19,7 +19,6 @@ describe('ManageTwoFactorForm', () => {
wrapper = mountExtended(ManageTwoFactorForm, {
provide: {
...defaultProvide,
- webauthnEnabled: options?.webauthnEnabled ?? false,
isCurrentPasswordRequired: options?.currentPasswordRequired ?? true,
},
stubs: {
@@ -41,16 +40,6 @@ describe('ManageTwoFactorForm', () => {
const findRegenerateCodesButton = () => wrapper.findByTestId('test-2fa-regenerate-codes-button');
const findConfirmationModal = () => wrapper.findComponent(GlModal);
- const itShowsConfirmationModal = (confirmText) => {
- it('shows confirmation modal', async () => {
- await wrapper.findByLabelText('Current password').setValue('foo bar');
- await findDisableButton().trigger('click');
-
- expect(findConfirmationModal().props('visible')).toBe(true);
- expect(findConfirmationModal().html()).toContain(confirmText);
- });
- };
-
const itShowsValidationMessageIfCurrentPasswordFieldIsEmpty = (findButtonFunction) => {
it('shows validation message if `Current password` is empty', async () => {
await findButtonFunction().trigger('click');
@@ -91,16 +80,12 @@ describe('ManageTwoFactorForm', () => {
describe('when clicked', () => {
itShowsValidationMessageIfCurrentPasswordFieldIsEmpty(findDisableButton);
- itShowsConfirmationModal(i18n.confirm);
-
- describe('when webauthnEnabled', () => {
- beforeEach(() => {
- createComponent({
- webauthnEnabled: true,
- });
- });
+ it('shows confirmation modal', async () => {
+ await wrapper.findByLabelText('Current password').setValue('foo bar');
+ await findDisableButton().trigger('click');
- itShowsConfirmationModal(i18n.confirmWebAuthn);
+ expect(findConfirmationModal().props('visible')).toBe(true);
+ expect(findConfirmationModal().html()).toContain(i18n.confirmWebAuthn);
});
it('modifies the form action and method when submitted through the button', async () => {
diff --git a/spec/frontend/authentication/u2f/authenticate_spec.js b/spec/frontend/authentication/u2f/authenticate_spec.js
deleted file mode 100644
index 3ae7fcf1c49..00000000000
--- a/spec/frontend/authentication/u2f/authenticate_spec.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import U2FAuthenticate from '~/authentication/u2f/authenticate';
-import 'vendor/u2f';
-import MockU2FDevice from './mock_u2f_device';
-
-describe('U2FAuthenticate', () => {
- let u2fDevice;
- let container;
- let component;
-
- beforeEach(() => {
- loadHTMLFixture('u2f/authenticate.html');
- u2fDevice = new MockU2FDevice();
- container = $('#js-authenticate-token-2fa');
- component = new U2FAuthenticate(
- container,
- '#js-login-token-2fa-form',
- {
- sign_requests: [],
- },
- document.querySelector('#js-login-2fa-device'),
- document.querySelector('.js-2fa-form'),
- );
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('with u2f unavailable', () => {
- let oldu2f;
-
- beforeEach(() => {
- jest.spyOn(component, 'switchToFallbackUI').mockImplementation(() => {});
- oldu2f = window.u2f;
- window.u2f = null;
- });
-
- afterEach(() => {
- window.u2f = oldu2f;
- });
-
- it('falls back to normal 2fa', async () => {
- await component.start();
- expect(component.switchToFallbackUI).toHaveBeenCalled();
- });
- });
-
- describe('with u2f available', () => {
- beforeEach(() => {
- // bypass automatic form submission within renderAuthenticated
- jest.spyOn(component, 'renderAuthenticated').mockReturnValue(true);
- u2fDevice = new MockU2FDevice();
-
- return component.start();
- });
-
- it('allows authenticating via a U2F device', () => {
- const inProgressMessage = container.find('p');
-
- expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
- u2fDevice.respondToAuthenticateRequest({
- deviceData: 'this is data from the device',
- });
-
- expect(component.renderAuthenticated).toHaveBeenCalledWith(
- '{"deviceData":"this is data from the device"}',
- );
- });
-
- describe('errors', () => {
- it('displays an error message', () => {
- const setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('There was a problem communicating with your device');
- });
-
- it('allows retrying authentication after an error', () => {
- let setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const retryButton = container.find('#js-token-2fa-try-again');
- retryButton.trigger('click');
- setupButton = container.find('#js-login-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToAuthenticateRequest({
- deviceData: 'this is data from the device',
- });
-
- expect(component.renderAuthenticated).toHaveBeenCalledWith(
- '{"deviceData":"this is data from the device"}',
- );
- });
- });
- });
-});
diff --git a/spec/frontend/authentication/u2f/mock_u2f_device.js b/spec/frontend/authentication/u2f/mock_u2f_device.js
deleted file mode 100644
index ec8425a4e3e..00000000000
--- a/spec/frontend/authentication/u2f/mock_u2f_device.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable no-unused-expressions */
-
-export default class MockU2FDevice {
- constructor() {
- this.respondToAuthenticateRequest = this.respondToAuthenticateRequest.bind(this);
- this.respondToRegisterRequest = this.respondToRegisterRequest.bind(this);
- window.u2f || (window.u2f = {});
- window.u2f.register = (appId, registerRequests, signRequests, callback) => {
- this.registerCallback = callback;
- };
- window.u2f.sign = (appId, challenges, signRequests, callback) => {
- this.authenticateCallback = callback;
- };
- }
-
- respondToRegisterRequest(params) {
- return this.registerCallback(params);
- }
-
- respondToAuthenticateRequest(params) {
- return this.authenticateCallback(params);
- }
-}
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
deleted file mode 100644
index 23d1e5c7dee..00000000000
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { trimText } from 'helpers/text_helper';
-import U2FRegister from '~/authentication/u2f/register';
-import 'vendor/u2f';
-import MockU2FDevice from './mock_u2f_device';
-
-describe('U2FRegister', () => {
- let u2fDevice;
- let container;
- let component;
-
- beforeEach(() => {
- loadHTMLFixture('u2f/register.html');
- u2fDevice = new MockU2FDevice();
- container = $('#js-register-token-2fa');
- component = new U2FRegister(container, {});
- return component.start();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('allows registering a U2F device', () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
-
- expect(trimText(setupButton.text())).toBe('Set up new device');
- setupButton.trigger('click');
- const inProgressMessage = container.children('p');
-
- expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
- u2fDevice.respondToRegisterRequest({
- deviceData: 'this is data from the device',
- });
- const registeredMessage = container.find('p');
- const deviceResponse = container.find('#js-device-response');
-
- expect(registeredMessage.text()).toContain('Your device was successfully set up!');
- expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
- });
-
- describe('errors', () => {
- it("doesn't allow the same device to be registered twice (for the same user", () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 4,
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('already been registered with us');
- });
-
- it('displays an error message for other errors', () => {
- const setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 'error!',
- });
- const errorMessage = container.find('p');
-
- expect(errorMessage.text()).toContain('There was a problem communicating with your device');
- });
-
- it('allows retrying registration after an error', () => {
- let setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- errorCode: 'error!',
- });
- const retryButton = container.find('#js-token-2fa-try-again');
- retryButton.trigger('click');
- setupButton = container.find('#js-setup-token-2fa-device');
- setupButton.trigger('click');
- u2fDevice.respondToRegisterRequest({
- deviceData: 'this is data from the device',
- });
- const registeredMessage = container.find('p');
-
- expect(registeredMessage.text()).toContain('Your device was successfully set up!');
- });
- });
-});
diff --git a/spec/frontend/authentication/u2f/util_spec.js b/spec/frontend/authentication/u2f/util_spec.js
deleted file mode 100644
index 67fd4c73243..00000000000
--- a/spec/frontend/authentication/u2f/util_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { canInjectU2fApi } from '~/authentication/u2f/util';
-
-describe('U2F Utils', () => {
- describe('canInjectU2fApi', () => {
- it('returns false for Chrome < 41', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.28 Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns true for Chrome >= 41', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(true);
- });
-
- it('returns false for Opera < 40', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.25';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns true for Opera >= 40', () => {
- const userAgent =
- 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991';
-
- expect(canInjectU2fApi(userAgent)).toBe(true);
- });
-
- it('returns false for Safari', () => {
- const userAgent =
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Chrome on Android', () => {
- const userAgent =
- 'Mozilla/5.0 (Linux; Android 7.0; VS988 Build/NRD90U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3145.0 Mobile Safari/537.36';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Chrome on iOS', () => {
- const userAgent =
- 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
-
- it('returns false for Safari on iOS', () => {
- const userAgent =
- 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1';
-
- expect(canInjectU2fApi(userAgent)).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/authentication/webauthn/authenticate_spec.js b/spec/frontend/authentication/webauthn/authenticate_spec.js
index b1f4e43e56d..b3a634fb072 100644
--- a/spec/frontend/authentication/webauthn/authenticate_spec.js
+++ b/spec/frontend/authentication/webauthn/authenticate_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlWebauthnAuthenticate from 'test_fixtures/webauthn/authenticate.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import WebAuthnAuthenticate from '~/authentication/webauthn/authenticate';
import MockWebAuthnDevice from './mock_webauthn_device';
@@ -35,7 +36,7 @@ describe('WebAuthnAuthenticate', () => {
};
beforeEach(() => {
- loadHTMLFixture('webauthn/authenticate.html');
+ setHTMLFixture(htmlWebauthnAuthenticate);
fallbackElement = document.createElement('div');
fallbackElement.classList.add('js-2fa-form');
webAuthnDevice = new MockWebAuthnDevice();
diff --git a/spec/frontend/authentication/webauthn/components/registration_spec.js b/spec/frontend/authentication/webauthn/components/registration_spec.js
new file mode 100644
index 00000000000..e4ca1ac8c38
--- /dev/null
+++ b/spec/frontend/authentication/webauthn/components/registration_spec.js
@@ -0,0 +1,255 @@
+import { nextTick } from 'vue';
+import { GlAlert, GlButton, GlForm, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import Registration from '~/authentication/webauthn/components/registration.vue';
+import {
+ I18N_BUTTON_REGISTER,
+ I18N_BUTTON_SETUP,
+ I18N_BUTTON_TRY_AGAIN,
+ I18N_ERROR_HTTP,
+ I18N_ERROR_UNSUPPORTED_BROWSER,
+ I18N_INFO_TEXT,
+ I18N_STATUS_SUCCESS,
+ I18N_STATUS_WAITING,
+ STATE_ERROR,
+ STATE_READY,
+ STATE_SUCCESS,
+ STATE_UNSUPPORTED,
+ STATE_WAITING,
+ WEBAUTHN_REGISTER,
+} from '~/authentication/webauthn/constants';
+import * as WebAuthnUtils from '~/authentication/webauthn/util';
+import WebAuthnError from '~/authentication/webauthn/error';
+
+const csrfToken = 'mock-csrf-token';
+jest.mock('~/lib/utils/csrf', () => ({ token: csrfToken }));
+jest.mock('~/authentication/webauthn/util');
+jest.mock('~/authentication/webauthn/error');
+
+describe('Registration', () => {
+ const initialError = null;
+ const passwordRequired = true;
+ const targetPath = '/-/profile/two_factor_auth/create_webauthn';
+ let wrapper;
+
+ const createComponent = (provide = {}) => {
+ wrapper = shallowMountExtended(Registration, {
+ provide: { initialError, passwordRequired, targetPath, ...provide },
+ });
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ describe(`when ${STATE_UNSUPPORTED} state`, () => {
+ it('shows an error if using unsecure scheme (HTTP)', () => {
+ // `supported` function returns false for HTTP because `navigator.credentials` is undefined.
+ WebAuthnUtils.supported.mockReturnValue(false);
+ WebAuthnUtils.isHTTPS.mockReturnValue(false);
+ createComponent();
+
+ const alert = wrapper.findComponent(GlAlert);
+ expect(alert.props('variant')).toBe('danger');
+ expect(alert.text()).toBe(I18N_ERROR_HTTP);
+ });
+
+ it('shows an error if using unsupported browser', () => {
+ WebAuthnUtils.supported.mockReturnValue(false);
+ WebAuthnUtils.isHTTPS.mockReturnValue(true);
+ createComponent();
+
+ const alert = wrapper.findComponent(GlAlert);
+ expect(alert.props('variant')).toBe('danger');
+ expect(alert.text()).toBe(I18N_ERROR_UNSUPPORTED_BROWSER);
+ });
+ });
+
+ describe('when scheme or browser are supported', () => {
+ const mockCreate = jest.fn();
+
+ const clickSetupDeviceButton = () => {
+ findButton().vm.$emit('click');
+ return nextTick();
+ };
+
+ const setupDevice = () => {
+ clickSetupDeviceButton();
+ return waitForPromises();
+ };
+
+ beforeEach(() => {
+ WebAuthnUtils.isHTTPS.mockReturnValue(true);
+ WebAuthnUtils.supported.mockReturnValue(true);
+ global.navigator.credentials = { create: mockCreate };
+ gon.webauthn = { options: {} };
+ });
+
+ afterEach(() => {
+ global.navigator.credentials = undefined;
+ });
+
+ describe(`when ${STATE_READY} state`, () => {
+ it('shows button and explanation text', () => {
+ createComponent();
+
+ expect(findButton().text()).toBe(I18N_BUTTON_SETUP);
+ expect(wrapper.text()).toContain(I18N_INFO_TEXT);
+ });
+ });
+
+ describe(`when ${STATE_WAITING} state`, () => {
+ it('shows loading icon and message after pressing the button', async () => {
+ createComponent();
+
+ await clickSetupDeviceButton();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.text()).toContain(I18N_STATUS_WAITING);
+ });
+ });
+
+ describe(`when ${STATE_SUCCESS} state`, () => {
+ const credentials = 1;
+
+ const findCurrentPasswordInput = () => wrapper.findByTestId('current-password-input');
+ const findDeviceNameInput = () => wrapper.findByTestId('device-name-input');
+
+ beforeEach(() => {
+ mockCreate.mockResolvedValueOnce(true);
+ WebAuthnUtils.convertCreateResponse.mockReturnValue(credentials);
+ });
+
+ describe('registration form', () => {
+ it('has correct action', async () => {
+ createComponent();
+
+ await setupDevice();
+
+ expect(wrapper.findComponent(GlForm).attributes('action')).toBe(targetPath);
+ });
+
+ describe('when password is required', () => {
+ it('shows device name and password fields', async () => {
+ createComponent();
+
+ await setupDevice();
+
+ expect(wrapper.text()).toContain(I18N_STATUS_SUCCESS);
+
+ // Visible inputs
+ expect(findCurrentPasswordInput().attributes('name')).toBe('current_password');
+ expect(findDeviceNameInput().attributes('name')).toBe('device_registration[name]');
+
+ // Hidden inputs
+ expect(
+ wrapper
+ .find('input[name="device_registration[device_response]"]')
+ .attributes('value'),
+ ).toBe(`${credentials}`);
+ expect(wrapper.find('input[name=authenticity_token]').attributes('value')).toBe(
+ csrfToken,
+ );
+
+ expect(findButton().text()).toBe(I18N_BUTTON_REGISTER);
+ });
+
+ it('enables the register device button when device name and password are filled', async () => {
+ createComponent();
+
+ await setupDevice();
+
+ expect(findButton().props('disabled')).toBe(true);
+
+ // Visible inputs
+ findCurrentPasswordInput().vm.$emit('input', 'my current password');
+ findDeviceNameInput().vm.$emit('input', 'my device name');
+ await nextTick();
+
+ expect(findButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('when password is not required', () => {
+ it('shows a device name field', async () => {
+ createComponent({ passwordRequired: false });
+
+ await setupDevice();
+
+ expect(wrapper.text()).toContain(I18N_STATUS_SUCCESS);
+
+ // Visible inputs
+ expect(findCurrentPasswordInput().exists()).toBe(false);
+ expect(findDeviceNameInput().attributes('name')).toBe('device_registration[name]');
+
+ // Hidden inputs
+ expect(
+ wrapper
+ .find('input[name="device_registration[device_response]"]')
+ .attributes('value'),
+ ).toBe(`${credentials}`);
+ expect(wrapper.find('input[name=authenticity_token]').attributes('value')).toBe(
+ csrfToken,
+ );
+
+ expect(findButton().text()).toBe(I18N_BUTTON_REGISTER);
+ });
+
+ it('enables the register device button when device name is filled', async () => {
+ createComponent({ passwordRequired: false });
+
+ await setupDevice();
+
+ expect(findButton().props('disabled')).toBe(true);
+
+ findDeviceNameInput().vm.$emit('input', 'my device name');
+ await nextTick();
+
+ expect(findButton().props('disabled')).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe(`when ${STATE_ERROR} state`, () => {
+ it('shows an initial error message and a retry button', () => {
+ const myError = 'my error';
+ createComponent({ initialError: myError });
+
+ const alert = wrapper.findComponent(GlAlert);
+ expect(alert.props()).toMatchObject({
+ variant: 'danger',
+ secondaryButtonText: I18N_BUTTON_TRY_AGAIN,
+ });
+ expect(alert.text()).toContain(myError);
+ });
+
+ it('shows an error message and a retry button', async () => {
+ createComponent();
+ const error = new Error();
+ mockCreate.mockRejectedValueOnce(error);
+
+ await setupDevice();
+
+ expect(WebAuthnError).toHaveBeenCalledWith(error, WEBAUTHN_REGISTER);
+ expect(wrapper.findComponent(GlAlert).props()).toMatchObject({
+ variant: 'danger',
+ secondaryButtonText: I18N_BUTTON_TRY_AGAIN,
+ });
+ });
+
+ it('recovers after an error (error to success state)', async () => {
+ createComponent();
+ mockCreate.mockRejectedValueOnce(new Error()).mockResolvedValueOnce(true);
+
+ await setupDevice();
+
+ expect(wrapper.findComponent(GlAlert).props('variant')).toBe('danger');
+
+ wrapper.findComponent(GlAlert).vm.$emit('secondaryAction');
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlAlert).props('variant')).toBe('info');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/authentication/webauthn/error_spec.js b/spec/frontend/authentication/webauthn/error_spec.js
index 9b71f77dde2..b979173edc6 100644
--- a/spec/frontend/authentication/webauthn/error_spec.js
+++ b/spec/frontend/authentication/webauthn/error_spec.js
@@ -1,16 +1,17 @@
import setWindowLocation from 'helpers/set_window_location_helper';
import WebAuthnError from '~/authentication/webauthn/error';
+import { WEBAUTHN_AUTHENTICATE, WEBAUTHN_REGISTER } from '~/authentication/webauthn/constants';
describe('WebAuthnError', () => {
it.each([
[
'NotSupportedError',
'Your device is not compatible with GitLab. Please try another device',
- 'authenticate',
+ WEBAUTHN_AUTHENTICATE,
],
- ['InvalidStateError', 'This device has not been registered with us.', 'authenticate'],
- ['InvalidStateError', 'This device has already been registered with us.', 'register'],
- ['UnknownError', 'There was a problem communicating with your device.', 'register'],
+ ['InvalidStateError', 'This device has not been registered with us.', WEBAUTHN_AUTHENTICATE],
+ ['InvalidStateError', 'This device has already been registered with us.', WEBAUTHN_REGISTER],
+ ['UnknownError', 'There was a problem communicating with your device.', WEBAUTHN_REGISTER],
])('exception %s will have message %s, flow type: %s', (exception, expectedMessage, flowType) => {
expect(new WebAuthnError(new DOMException('', exception), flowType).message()).toEqual(
expectedMessage,
@@ -24,7 +25,7 @@ describe('WebAuthnError', () => {
const expectedMessage =
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.';
expect(
- new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(),
+ new WebAuthnError(new DOMException('', 'SecurityError'), WEBAUTHN_AUTHENTICATE).message(),
).toEqual(expectedMessage);
});
@@ -33,7 +34,7 @@ describe('WebAuthnError', () => {
const expectedMessage = 'There was a problem communicating with your device.';
expect(
- new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(),
+ new WebAuthnError(new DOMException('', 'SecurityError'), WEBAUTHN_AUTHENTICATE).message(),
).toEqual(expectedMessage);
});
});
diff --git a/spec/frontend/authentication/webauthn/register_spec.js b/spec/frontend/authentication/webauthn/register_spec.js
index 773481346fc..5f0691782a7 100644
--- a/spec/frontend/authentication/webauthn/register_spec.js
+++ b/spec/frontend/authentication/webauthn/register_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlWebauthnRegister from 'test_fixtures/webauthn/register.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { trimText } from 'helpers/text_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -25,7 +26,7 @@ describe('WebAuthnRegister', () => {
let component;
beforeEach(() => {
- loadHTMLFixture('webauthn/register.html');
+ setHTMLFixture(htmlWebauthnRegister);
webAuthnDevice = new MockWebAuthnDevice();
container = $('#js-register-token-2fa');
component = new WebAuthnRegister(container, {
diff --git a/spec/frontend/authentication/webauthn/util_spec.js b/spec/frontend/authentication/webauthn/util_spec.js
index bc44b47d0ba..831d1636b8c 100644
--- a/spec/frontend/authentication/webauthn/util_spec.js
+++ b/spec/frontend/authentication/webauthn/util_spec.js
@@ -1,4 +1,9 @@
-import { base64ToBuffer, bufferToBase64, base64ToBase64Url } from '~/authentication/webauthn/util';
+import {
+ base64ToBuffer,
+ bufferToBase64,
+ base64ToBase64Url,
+ supported,
+} from '~/authentication/webauthn/util';
const encodedString = 'SGVsbG8gd29ybGQh';
const stringBytes = [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33];
@@ -31,4 +36,28 @@ describe('Webauthn utils', () => {
expect(base64ToBase64Url(argument)).toBe(expectedResult);
});
});
+
+ describe('supported', () => {
+ afterEach(() => {
+ global.navigator.credentials = undefined;
+ window.PublicKeyCredential = undefined;
+ });
+
+ it.each`
+ credentials | PublicKeyCredential | expected
+ ${undefined} | ${undefined} | ${false}
+ ${{}} | ${undefined} | ${false}
+ ${{ create: true }} | ${undefined} | ${false}
+ ${{ create: true, get: true }} | ${undefined} | ${false}
+ ${{ create: true, get: true }} | ${true} | ${true}
+ `(
+ 'returns $expected when credentials is $credentials and PublicKeyCredential is $PublicKeyCredential',
+ ({ credentials, PublicKeyCredential, expected }) => {
+ global.navigator.credentials = credentials;
+ window.PublicKeyCredential = PublicKeyCredential;
+
+ expect(supported()).toBe(expected);
+ },
+ );
+ });
});
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index 1a54b9909ba..c2b7906d0d6 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -1,15 +1,14 @@
import $ from 'jquery';
+import htmlSnippetsShow from 'test_fixtures/snippets/show.html';
import Cookies from '~/lib/utils/cookies';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import loadAwardsHandler from '~/awards_handler';
window.gl = window.gl || {};
-window.gon = window.gon || {};
let awardsHandler = null;
-const urlRoot = gon.relative_url_root;
describe('AwardsHandler', () => {
useFakeRequestAnimationFrame();
@@ -88,16 +87,13 @@ describe('AwardsHandler', () => {
beforeEach(async () => {
await initEmojiMock(emojiData);
- loadHTMLFixture('snippets/show.html');
+ setHTMLFixture(htmlSnippetsShow);
awardsHandler = await loadAwardsHandler(true);
jest.spyOn(awardsHandler, 'postEmoji').mockImplementation((button, url, emoji, cb) => cb());
});
afterEach(() => {
- // restore original url root value
- gon.relative_url_root = urlRoot;
-
clearEmojiMock();
// Undo what we did to the shared <body>
diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index 0a736df7075..d7519f1f80d 100644
--- a/spec/frontend/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -43,7 +43,6 @@ describe('BadgeForm component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
diff --git a/spec/frontend/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js
index ee7ccac974a..cbbeb36ff33 100644
--- a/spec/frontend/badges/components/badge_list_row_spec.js
+++ b/spec/frontend/badges/components/badge_list_row_spec.js
@@ -43,7 +43,6 @@ describe('BadgeListRow component', () => {
};
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index 606b1bc9cce..374b7b50af4 100644
--- a/spec/frontend/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -38,10 +38,6 @@ describe('BadgeList component', () => {
wrapper = mount(BadgeList, { store });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('for project badges', () => {
it('renders a header with the badge count', () => {
createComponent({
diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js
index bddb6d3801c..7ad2c99869c 100644
--- a/spec/frontend/badges/components/badge_settings_spec.js
+++ b/spec/frontend/badges/components/badge_settings_spec.js
@@ -32,10 +32,6 @@ describe('BadgeSettings component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays modal if button for deleting a badge is clicked', async () => {
const button = wrapper.find('[data-testid="delete-badge"]');
diff --git a/spec/frontend/badges/components/badge_spec.js b/spec/frontend/badges/components/badge_spec.js
index b468e38f19e..c933c1b5434 100644
--- a/spec/frontend/badges/components/badge_spec.js
+++ b/spec/frontend/badges/components/badge_spec.js
@@ -24,10 +24,6 @@ describe('Badge component', () => {
wrapper = mount(Badge, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
return createComponent({ ...dummyProps }, '#dummy-element');
});
diff --git a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
index c922d6a9809..f667ebc0fcb 100644
--- a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
+++ b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js
@@ -28,10 +28,6 @@ describe('Batch comments diff file drafts component', () => {
});
}
- afterEach(() => {
- vm.destroy();
- });
-
it('renders list of draft notes', () => {
factory();
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 924d88866ee..159e36c1364 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -49,10 +49,6 @@ describe('Batch comments draft note component', () => {
draft = createDraft();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders template', () => {
createComponent();
expect(wrapper.findComponent(GlBadge).exists()).toBe(true);
diff --git a/spec/frontend/batch_comments/components/drafts_count_spec.js b/spec/frontend/batch_comments/components/drafts_count_spec.js
index c3a7946c85c..850a7efb4ed 100644
--- a/spec/frontend/batch_comments/components/drafts_count_spec.js
+++ b/spec/frontend/batch_comments/components/drafts_count_spec.js
@@ -15,10 +15,6 @@ describe('Batch comments drafts count component', () => {
wrapper = mount(DraftsCount, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders count', () => {
expect(wrapper.text()).toContain('1');
});
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index f86e003ab5f..3a28bf4ade8 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -1,7 +1,6 @@
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { GlDisclosureDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { visitUrl } from '~/lib/utils/url_utility';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
@@ -46,9 +45,11 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
},
});
- wrapper = shallowMount(PreviewDropdown, {
+ wrapper = mount(PreviewDropdown, {
store,
- stubs: { GlDisclosureDropdown },
+ stubs: {
+ PreviewItem: true,
+ },
});
}
@@ -59,12 +60,12 @@ describe('Batch comments preview dropdown', () => {
viewDiffsFileByFile: true,
sortedDrafts: [{ id: 1, file_hash: 'hash' }],
});
-
- findPreviewItem().vm.$emit('click');
-
+ findPreviewItem().trigger('click');
await nextTick();
expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash');
+
+ await nextTick();
expect(scrollToDraft).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ id: 1, file_hash: 'hash' }),
@@ -77,7 +78,7 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1 }],
});
- findPreviewItem().vm.$emit('click');
+ findPreviewItem().trigger('click');
await nextTick();
@@ -93,7 +94,7 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }],
});
- findPreviewItem().vm.$emit('click');
+ findPreviewItem().trigger('click');
await nextTick();
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index 6a99294f855..a19a72af813 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -26,10 +26,6 @@ describe('Batch comments draft preview item component', () => {
wrapper = mount(PreviewItem, { store, propsData: { draft, isLast } });
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders text content', () => {
createComponent(false, { note_html: '<img src="" /><p>Hello world</p>' });
diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js
index 0a4c9ff62e4..ea4b015ea39 100644
--- a/spec/frontend/batch_comments/components/review_bar_spec.js
+++ b/spec/frontend/batch_comments/components/review_bar_spec.js
@@ -20,11 +20,7 @@ describe('Batch comments review bar component', () => {
document.body.className = '';
});
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('adds review-bar-visible class to body when review bar is mounted', async () => {
+ it('adds review-bar-visible class to body when review bar is mounted', () => {
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false);
createComponent();
@@ -32,7 +28,7 @@ describe('Batch comments review bar component', () => {
expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true);
});
- it('removes review-bar-visible class to body when review bar is destroyed', async () => {
+ it('removes review-bar-visible class to body when review bar is destroyed', () => {
createComponent();
wrapper.destroy();
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 003a6d86371..5c33df882bf 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -1,3 +1,4 @@
+import { GlDropdown } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -10,7 +11,7 @@ Vue.use(Vuex);
let wrapper;
let publishReview;
-function factory({ canApprove = true } = {}) {
+function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
publishReview = jest.fn();
const store = new Vuex.Store({
@@ -30,6 +31,7 @@ function factory({ canApprove = true } = {}) {
modules: {
batchComments: {
namespaced: true,
+ state: { shouldAnimateReviewButton },
actions: {
publishReview,
},
@@ -44,10 +46,10 @@ function factory({ canApprove = true } = {}) {
const findCommentTextarea = () => wrapper.findByTestId('comment-textarea');
const findSubmitButton = () => wrapper.findByTestId('submit-review-button');
const findForm = () => wrapper.findByTestId('submit-gl-form');
+const findSubmitDropdown = () => wrapper.findComponent(GlDropdown);
describe('Batch comments submit dropdown', () => {
afterEach(() => {
- wrapper.destroy();
window.mrTabs = null;
});
@@ -99,4 +101,19 @@ describe('Batch comments submit dropdown', () => {
expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists);
});
+
+ it.each`
+ shouldAnimateReviewButton | animationClassApplied | classText
+ ${true} | ${true} | ${'applies'}
+ ${false} | ${false} | ${'does not apply'}
+ `(
+ '$classText animation class to `Finish review` button if `shouldAnimateReviewButton` is $shouldAnimateReviewButton',
+ ({ shouldAnimateReviewButton, animationClassApplied }) => {
+ factory({ shouldAnimateReviewButton });
+
+ expect(findSubmitDropdown().classes('submit-review-dropdown-animated')).toBe(
+ animationClassApplied,
+ );
+ },
+ );
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 20eedcbb25b..57bafb51cd6 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -317,4 +317,10 @@ describe('Batch comments store actions', () => {
expect(window.mrTabs.tabShown).toHaveBeenCalledWith('diffs');
});
});
+
+ describe('clearDrafts', () => {
+ it('commits CLEAR_DRAFTS', () => {
+ return testAction(actions.clearDrafts, null, null, [{ type: 'CLEAR_DRAFTS' }], []);
+ });
+ });
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
index fe01de638c2..fc00083987e 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/mutations_spec.js
@@ -10,9 +10,8 @@ describe('Batch comments mutations', () => {
});
describe(types.ADD_NEW_DRAFT, () => {
+ const draft = { id: 1, note: 'test' };
it('adds processed object into drafts array', () => {
- const draft = { id: 1, note: 'test' };
-
mutations[types.ADD_NEW_DRAFT](state, draft);
expect(state.drafts).toEqual([
@@ -22,6 +21,19 @@ describe('Batch comments mutations', () => {
},
]);
});
+
+ it('sets `shouldAnimateReviewButton` to true if it is a first draft', () => {
+ mutations[types.ADD_NEW_DRAFT](state, draft);
+
+ expect(state.shouldAnimateReviewButton).toBe(true);
+ });
+
+ it('does not set `shouldAnimateReviewButton` to true if it is not a first draft', () => {
+ state.drafts.push({ id: 1 }, { id: 2 });
+ mutations[types.ADD_NEW_DRAFT](state, { id: 2, note: 'test2' });
+
+ expect(state.shouldAnimateReviewButton).toBe(false);
+ });
});
describe(types.DELETE_DRAFT, () => {
@@ -104,4 +116,14 @@ describe('Batch comments mutations', () => {
]);
});
});
+
+ describe(types.CLEAR_DRAFTS, () => {
+ it('clears drafts array', () => {
+ state.drafts.push({ id: 1 });
+
+ mutations[types.CLEAR_DRAFTS](state);
+
+ expect(state.drafts).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/behaviors/components/diagram_performance_warning_spec.js b/spec/frontend/behaviors/components/diagram_performance_warning_spec.js
index c58c2bc55a9..7e6b20da4d4 100644
--- a/spec/frontend/behaviors/components/diagram_performance_warning_spec.js
+++ b/spec/frontend/behaviors/components/diagram_performance_warning_spec.js
@@ -11,10 +11,6 @@ describe('DiagramPerformanceWarning component', () => {
wrapper = shallowMount(DiagramPerformanceWarning);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders warning alert with button', () => {
expect(findAlert().props()).toMatchObject({
primaryButtonText: DiagramPerformanceWarning.i18n.buttonText,
diff --git a/spec/frontend/behaviors/components/json_table_spec.js b/spec/frontend/behaviors/components/json_table_spec.js
index 42b4a051d4d..ae62d28d6c0 100644
--- a/spec/frontend/behaviors/components/json_table_spec.js
+++ b/spec/frontend/behaviors/components/json_table_spec.js
@@ -12,6 +12,7 @@ const TEST_FIELDS = [
label: 'Second',
sortable: true,
other: 'foo',
+ class: 'someClass',
},
{
key: 'C',
@@ -59,10 +60,6 @@ describe('behaviors/components/json_table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTable = () => wrapper.findComponent(GlTable);
const findTableCaption = () => wrapper.findByTestId('slot-table-caption');
const findFilterInput = () => wrapper.findComponent(GlFormInput);
@@ -131,11 +128,13 @@ describe('behaviors/components/json_table', () => {
key: 'B',
label: 'Second',
sortable: true,
+ class: 'someClass',
},
{
key: 'C',
label: 'Third',
sortable: false,
+ class: [],
},
'D',
],
diff --git a/spec/frontend/behaviors/copy_to_clipboard_spec.js b/spec/frontend/behaviors/copy_to_clipboard_spec.js
index c5beaa0ba5d..74a396eb8cb 100644
--- a/spec/frontend/behaviors/copy_to_clipboard_spec.js
+++ b/spec/frontend/behaviors/copy_to_clipboard_spec.js
@@ -31,7 +31,7 @@ describe('initCopyToClipboard', () => {
const defaultButtonAttributes = {
'data-clipboard-text': 'foo bar',
title,
- 'data-title': title,
+ 'data-original-title': title,
};
const createButton = (attributes = {}) => {
const combinedAttributes = { ...defaultButtonAttributes, ...attributes };
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 722327e94ba..995e4219ae3 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -51,13 +51,13 @@ describe('gl_emoji', () => {
'bomb emoji just with name attribute',
'<gl-emoji data-name="bomb"></gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
],
[
'bomb emoji with name attribute and unicode version',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
'<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
- `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
],
[
'bomb emoji with sprite fallback',
@@ -69,19 +69,19 @@ describe('gl_emoji', () => {
'bomb emoji with image fallback',
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
'<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>',
],
[
'invalid emoji',
'<gl-emoji data-name="invalid_emoji"></gl-emoji>',
'<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
- `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>`,
+ `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
],
[
'custom emoji with image fallback',
'<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="20" height="20" align="absmiddle"></gl-emoji>',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="20" height="20" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
],
])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
it(`renders correctly with emoji support`, async () => {
@@ -111,7 +111,7 @@ describe('gl_emoji', () => {
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(
- '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" width="20" height="20" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>',
);
});
diff --git a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
index 38d19ac3808..ad70efdf7c3 100644
--- a/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
+++ b/spec/frontend/behaviors/markdown/highlight_current_user_spec.js
@@ -22,14 +22,9 @@ describe('highlightCurrentUser', () => {
describe('without current user', () => {
beforeEach(() => {
- window.gon = window.gon || {};
window.gon.current_user_id = null;
});
- afterEach(() => {
- delete window.gon.current_user_id;
- });
-
it('does not highlight the user', () => {
const initialHtml = rootElement.outerHTML;
@@ -41,14 +36,9 @@ describe('highlightCurrentUser', () => {
describe('with current user', () => {
beforeEach(() => {
- window.gon = window.gon || {};
window.gon.current_user_id = 2;
});
- afterEach(() => {
- delete window.gon.current_user_id;
- });
-
it('highlights current user', () => {
highlightCurrentUser(elements);
diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js
index 0bbb92282e5..220ad874b47 100644
--- a/spec/frontend/behaviors/markdown/render_gfm_spec.js
+++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js
@@ -1,4 +1,7 @@
import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import renderMetrics from '~/behaviors/markdown/render_metrics';
+
+jest.mock('~/behaviors/markdown/render_metrics');
describe('renderGFM', () => {
it('handles a missing element', () => {
@@ -6,4 +9,27 @@ describe('renderGFM', () => {
renderGFM();
}).not.toThrow();
});
+
+ describe('remove_monitor_metrics flag', () => {
+ let metricsElement;
+
+ beforeEach(() => {
+ window.gon = { features: { removeMonitorMetrics: true } };
+ metricsElement = document.createElement('div');
+ metricsElement.setAttribute('class', '.js-render-metrics');
+ });
+
+ it('renders metrics when the flag is disabled', () => {
+ window.gon.features = { features: { removeMonitorMetrics: false } };
+ renderGFM(metricsElement);
+
+ expect(renderMetrics).toHaveBeenCalled();
+ });
+
+ it('does not render metrics when the flag is enabled', () => {
+ renderGFM(metricsElement);
+
+ expect(renderMetrics).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js
index 03a0cb2fcc2..f464c01ac15 100644
--- a/spec/frontend/behaviors/markdown/render_observability_spec.js
+++ b/spec/frontend/behaviors/markdown/render_observability_spec.js
@@ -1,46 +1,43 @@
+import Vue from 'vue';
+import { createWrapper } from '@vue/test-utils';
import renderObservability from '~/behaviors/markdown/render_observability';
-import * as ColorUtils from '~/lib/utils/color_utils';
+import { INLINE_EMBED_DIMENSIONS, SKELETON_VARIANT_EMBED } from '~/observability/constants';
+import ObservabilityApp from '~/observability/components/observability_app.vue';
-describe('Observability iframe renderer', () => {
- const findObservabilityIframes = (theme = 'light') =>
- document.querySelectorAll(`iframe[src="https://observe.gitlab.com/?theme=${theme}&kiosk"]`);
-
- const renderEmbeddedObservability = () => {
- renderObservability([...document.querySelectorAll('.js-render-observability')]);
- jest.runAllTimers();
- };
+describe('renderObservability', () => {
+ let subject;
beforeEach(() => {
- document.body.dataset.page = '';
- document.body.innerHTML = '';
+ subject = document.createElement('div');
+ subject.classList.add('js-render-observability');
+ subject.dataset.frameUrl = 'https://observe.gitlab.com/';
+ document.body.appendChild(subject);
});
- it('renders an observability iframe', () => {
- document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/" data-observability-url="https://observe.gitlab.com/" ></div>`;
-
- expect(findObservabilityIframes()).toHaveLength(0);
-
- renderEmbeddedObservability();
-
- expect(findObservabilityIframes()).toHaveLength(1);
+ afterEach(() => {
+ subject.remove();
});
- it('renders iframe with dark param when GL has dark theme', () => {
- document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://observe.gitlab.com/" data-observability-url="https://observe.gitlab.com/"></div>`;
- jest.spyOn(ColorUtils, 'darkModeEnabled').mockImplementation(() => true);
-
- expect(findObservabilityIframes('dark')).toHaveLength(0);
-
- renderEmbeddedObservability();
-
- expect(findObservabilityIframes('dark')).toHaveLength(1);
+ it('should return an array of Vue instances', () => {
+ const vueInstances = renderObservability([
+ ...document.querySelectorAll('.js-render-observability'),
+ ]);
+ expect(vueInstances).toEqual([expect.any(Vue)]);
});
- it('does not render if url is different from observability url', () => {
- document.body.innerHTML = `<div class="js-render-observability" data-frame-url="https://example.com/" data-observability-url="https://observe.gitlab.com/"></div>`;
+ it('should correctly pass props to the ObservabilityApp component', () => {
+ const vueInstances = renderObservability([
+ ...document.querySelectorAll('.js-render-observability'),
+ ]);
- renderEmbeddedObservability();
+ const wrapper = createWrapper(vueInstances[0]);
- expect(findObservabilityIframes()).toHaveLength(0);
+ expect(wrapper.findComponent(ObservabilityApp).props()).toMatchObject({
+ observabilityIframeSrc: 'https://observe.gitlab.com/',
+ skeletonVariant: SKELETON_VARIANT_EMBED,
+ inlineEmbed: true,
+ height: INLINE_EMBED_DIMENSIONS.HEIGHT,
+ width: INLINE_EMBED_DIMENSIONS.WIDTH,
+ });
});
});
diff --git a/spec/frontend/behaviors/quick_submit_spec.js b/spec/frontend/behaviors/quick_submit_spec.js
index 317c671cd2b..81eeb3f153e 100644
--- a/spec/frontend/behaviors/quick_submit_spec.js
+++ b/spec/frontend/behaviors/quick_submit_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlSnippetsShow from 'test_fixtures/snippets/show.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import '~/behaviors/quick_submit';
describe('Quick Submit behavior', () => {
@@ -8,7 +9,7 @@ describe('Quick Submit behavior', () => {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
beforeEach(() => {
- loadHTMLFixture('snippets/show.html');
+ setHTMLFixture(htmlSnippetsShow);
testContext = {};
@@ -60,22 +61,15 @@ describe('Quick Submit behavior', () => {
expect(testContext.spies.submit).not.toHaveBeenCalled();
});
- it('disables input of type submit', () => {
- const submitButton = $('.js-quick-submit input[type=submit]');
- testContext.textarea.trigger(keydownEvent());
-
- expect(submitButton).toBeDisabled();
- });
-
- it('disables button of type submit', () => {
- const submitButton = $('.js-quick-submit input[type=submit]');
+ it('disables submit', () => {
+ const submitButton = $('.js-quick-submit [type=submit]');
testContext.textarea.trigger(keydownEvent());
expect(submitButton).toBeDisabled();
});
it('only clicks one submit', () => {
- const existingSubmit = $('.js-quick-submit input[type=submit]');
+ const existingSubmit = $('.js-quick-submit [type=submit]');
// Add an extra submit button
const newSubmit = $('<button type="submit">Submit it</button>');
newSubmit.insertAfter(testContext.textarea);
diff --git a/spec/frontend/behaviors/requires_input_spec.js b/spec/frontend/behaviors/requires_input_spec.js
index f2f68f17d1c..68fa980216a 100644
--- a/spec/frontend/behaviors/requires_input_spec.js
+++ b/spec/frontend/behaviors/requires_input_spec.js
@@ -1,12 +1,13 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlNewBranch from 'test_fixtures/branches/new_branch.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import '~/behaviors/requires_input';
describe('requiresInput', () => {
let submitButton;
beforeEach(() => {
- loadHTMLFixture('branches/new_branch.html');
+ setHTMLFixture(htmlNewBranch);
submitButton = $('button[type="submit"]');
});
diff --git a/spec/frontend/behaviors/shortcuts/keybindings_spec.js b/spec/frontend/behaviors/shortcuts/keybindings_spec.js
index 1f7e1b24e78..65ef6a18864 100644
--- a/spec/frontend/behaviors/shortcuts/keybindings_spec.js
+++ b/spec/frontend/behaviors/shortcuts/keybindings_spec.js
@@ -7,7 +7,7 @@ import {
TOGGLE_PERFORMANCE_BAR,
HIDE_APPEARING_CONTENT,
LOCAL_STORAGE_KEY,
- WEB_IDE_COMMIT,
+ BOLD_TEXT,
} from '~/behaviors/shortcuts/keybindings';
describe('~/behaviors/shortcuts/keybindings', () => {
@@ -67,11 +67,11 @@ describe('~/behaviors/shortcuts/keybindings', () => {
const customization = ['mod+shift+c'];
beforeEach(() => {
- setupCustomizations(JSON.stringify({ [WEB_IDE_COMMIT.id]: customization }));
+ setupCustomizations(JSON.stringify({ [BOLD_TEXT.id]: customization }));
});
it('returns the default keybinding for the command', () => {
- expect(keysFor(WEB_IDE_COMMIT)).toEqual(['mod+enter']);
+ expect(keysFor(BOLD_TEXT)).toEqual(['mod+b']);
});
});
diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
index e6e587ff44b..ae7f5416c0c 100644
--- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlSnippetsShow from 'test_fixtures/snippets/show.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import initCopyAsGFM, { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
@@ -11,8 +12,6 @@ jest.mock('~/lib/utils/common_utils', () => ({
}));
describe('ShortcutsIssuable', () => {
- const snippetShowFixtureName = 'snippets/show.html';
-
beforeAll(() => {
initCopyAsGFM();
@@ -24,7 +23,7 @@ describe('ShortcutsIssuable', () => {
const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
beforeEach(() => {
- loadHTMLFixture(snippetShowFixtureName);
+ setHTMLFixture(htmlSnippetsShow);
$('body').append(
`<div class="js-main-target-form">
<textarea class="js-vue-comment-form"></textarea>
diff --git a/spec/frontend/blame/blame_redirect_spec.js b/spec/frontend/blame/blame_redirect_spec.js
index beb10139b3a..5cd91ec5f1f 100644
--- a/spec/frontend/blame/blame_redirect_spec.js
+++ b/spec/frontend/blame/blame_redirect_spec.js
@@ -1,12 +1,11 @@
import redirectToCorrectPage from '~/blame/blame_redirect';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Blame page redirect', () => {
beforeEach(() => {
- global.window = Object.create(window);
const url = 'https://gitlab.com/flightjs/Flight/-/blame/master/file.json';
Object.defineProperty(window, 'location', {
writable: true,
diff --git a/spec/frontend/blame/streaming/index_spec.js b/spec/frontend/blame/streaming/index_spec.js
new file mode 100644
index 00000000000..e048ce3f70e
--- /dev/null
+++ b/spec/frontend/blame/streaming/index_spec.js
@@ -0,0 +1,110 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { renderBlamePageStreams } from '~/blame/streaming';
+import { setHTMLFixture } from 'helpers/fixtures';
+import { renderHtmlStreams } from '~/streaming/render_html_streams';
+import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
+import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { toPolyfillReadable } from '~/streaming/polyfills';
+import { createAlert } from '~/alert';
+
+jest.mock('~/streaming/render_html_streams');
+jest.mock('~/streaming/rate_limit_stream_requests');
+jest.mock('~/streaming/handle_streamed_anchor_link');
+jest.mock('~/streaming/polyfills');
+jest.mock('~/sentry');
+jest.mock('~/alert');
+
+global.fetch = jest.fn();
+
+describe('renderBlamePageStreams', () => {
+ let stopAnchor;
+ const PAGES_URL = 'https://example.com/';
+ const findStreamContainer = () => document.querySelector('#blame-stream-container');
+ const findStreamLoadingIndicator = () => document.querySelector('#blame-stream-loading');
+
+ const setupHtml = (totalExtraPages = 0) => {
+ setHTMLFixture(`
+ <div id="blob-content-holder"
+ data-total-extra-pages="${totalExtraPages}"
+ data-pages-url="${PAGES_URL}"
+ ></div>
+ <div id="blame-stream-container"></div>
+ <div id="blame-stream-loading"></div>
+ `);
+ };
+
+ handleStreamedAnchorLink.mockImplementation(() => stopAnchor);
+ rateLimitStreamRequests.mockImplementation(({ factory, total }) => {
+ return Array.from({ length: total }, (_, i) => {
+ return Promise.resolve(factory(i));
+ });
+ });
+ toPolyfillReadable.mockImplementation((obj) => obj);
+
+ beforeEach(() => {
+ stopAnchor = jest.fn();
+ fetch.mockClear();
+ });
+
+ it('does nothing for an empty page', async () => {
+ await renderBlamePageStreams();
+
+ expect(handleStreamedAnchorLink).not.toHaveBeenCalled();
+ expect(renderHtmlStreams).not.toHaveBeenCalled();
+ });
+
+ it('renders a single stream', async () => {
+ let res;
+ const stream = new Promise((resolve) => {
+ res = resolve;
+ });
+ renderHtmlStreams.mockImplementationOnce(() => stream);
+ setupHtml();
+
+ renderBlamePageStreams(stream);
+
+ expect(handleStreamedAnchorLink).toHaveBeenCalledTimes(1);
+ expect(stopAnchor).toHaveBeenCalledTimes(0);
+ expect(renderHtmlStreams).toHaveBeenCalledWith([stream], findStreamContainer());
+ expect(findStreamLoadingIndicator()).not.toBe(null);
+
+ res();
+ await waitForPromises();
+
+ expect(stopAnchor).toHaveBeenCalledTimes(1);
+ expect(findStreamLoadingIndicator()).toBe(null);
+ });
+
+ it('renders rest of the streams', async () => {
+ const stream = Promise.resolve();
+ const stream2 = Promise.resolve({ body: null });
+ fetch.mockImplementationOnce(() => stream2);
+ setupHtml(1);
+
+ await renderBlamePageStreams(stream);
+
+ expect(fetch.mock.calls[0][0].toString()).toBe(`${PAGES_URL}?page=3`);
+ expect(renderHtmlStreams).toHaveBeenCalledWith([stream, stream2], findStreamContainer());
+ });
+
+ it('shows an error message when failed', async () => {
+ const stream = Promise.resolve();
+ const error = new Error();
+ renderHtmlStreams.mockImplementationOnce(() => Promise.reject(error));
+ setupHtml();
+
+ try {
+ await renderBlamePageStreams(stream);
+ } catch (err) {
+ expect(err).toBe(error);
+ }
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Blame could not be loaded as a single page.',
+ primaryButton: {
+ text: 'View blame as separate pages',
+ clickHandler: expect.any(Function),
+ },
+ });
+ });
+});
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 a5690844053..1733c4d4bb4 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
@@ -10,7 +10,7 @@ exports[`Blob Header Editing rendering matches the snapshot 1`] = `
<gl-form-input-stub
class="form-control js-snippet-file-name"
name="snippet_file_name"
- placeholder="Give your file a name to add code highlighting, e.g. example.rb for Ruby"
+ placeholder="File name (e.g. test.rb)"
type="text"
value="foo.md"
/>
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 fdbb9bdd0d0..4ae55f34e4c 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
@@ -22,7 +22,7 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
<clipboard-button-stub
category="tertiary"
- cssclass="btn-clipboard btn-transparent lh-100 position-static"
+ cssclass="gl-mr-2"
gfm="\`foo/bar/dummy.md\`"
size="medium"
text="foo/bar/dummy.md"
diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js
index 0f5885c2acf..203fab94a5c 100644
--- a/spec/frontend/blob/components/blob_content_error_spec.js
+++ b/spec/frontend/blob/components/blob_content_error_spec.js
@@ -18,10 +18,6 @@ describe('Blob Content Error component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('collapsed and too large blobs', () => {
it.each`
error | reason | options
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index f7b819b6e94..91af5f7bfed 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -29,10 +29,6 @@ describe('Blob Content component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('renders loader if `loading: true`', () => {
createComponent({ loading: true });
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index c84b5896348..b0ce5f40d95 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -1,6 +1,5 @@
import { GlFormInput, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import BlobEditHeader from '~/blob/components/blob_edit_header.vue';
describe('Blob Header Editing', () => {
@@ -15,24 +14,22 @@ describe('Blob Header Editing', () => {
},
});
};
+
const findDeleteButton = () =>
wrapper.findAllComponents(GlButton).wrappers.find((x) => x.text() === 'Delete file');
+ const findFormInput = () => wrapper.findComponent(GlFormInput);
beforeEach(() => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('contains a form input field', () => {
- expect(wrapper.findComponent(GlFormInput).exists()).toBe(true);
+ expect(findFormInput().exists()).toBe(true);
});
it('does not show delete button', () => {
@@ -41,19 +38,16 @@ describe('Blob Header Editing', () => {
});
describe('functionality', () => {
- it('emits input event when the blob name is changed', async () => {
- const inputComponent = wrapper.findComponent(GlFormInput);
+ it('emits input event when the blob name is changed', () => {
+ const inputComponent = findFormInput();
const newValue = 'bar.txt';
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- name: newValue,
- });
+ // update `name` with `newValue`
+ inputComponent.vm.$emit('input', newValue);
+ // trigger change event which emits input event on wrapper
inputComponent.vm.$emit('change');
- await nextTick();
- expect(wrapper.emitted().input[0]).toEqual([newValue]);
+ expect(wrapper.emitted().input).toEqual([[newValue]]);
});
});
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index 0f015715dc2..4c8c256121f 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -34,10 +34,6 @@ describe('Blob Header Default Actions', () => {
buttons = wrapper.findAllComponents(GlButton);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders', () => {
const findCopyButton = () => wrapper.findByTestId('copyContentsButton');
const findViewRawButton = () => wrapper.findByTestId('viewRawButton');
@@ -49,7 +45,7 @@ describe('Blob Header Default Actions', () => {
it('exactly 3 buttons with predefined actions', () => {
expect(buttons.length).toBe(3);
[BTN_COPY_CONTENTS_TITLE, BTN_RAW_TITLE, BTN_DOWNLOAD_TITLE].forEach((title, i) => {
- expect(buttons.at(i).vm.$el.title).toBe(title);
+ expect(buttons.at(i).attributes('title')).toBe(title);
});
});
@@ -71,7 +67,7 @@ describe('Blob Header Default Actions', () => {
});
buttons = wrapper.findAllComponents(GlButton);
- expect(buttons.at(0).attributes('disabled')).toBe('true');
+ expect(buttons.at(0).attributes('disabled')).toBeDefined();
});
it('does not render the copy button if a rendering error is set', () => {
@@ -91,10 +87,9 @@ describe('Blob Header Default Actions', () => {
it('emits a copy event if overrideCopy is set to true', () => {
createComponent({ overrideCopy: true });
- jest.spyOn(wrapper.vm, '$emit');
findCopyButton().vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('copy');
+ expect(wrapper.emitted('copy')).toHaveLength(1);
});
});
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index 8c32cba1ba4..be49146ff8a 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -21,10 +21,6 @@ describe('Blob Header Filepath', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findBadge = () => wrapper.findComponent(GlBadge);
describe('rendering', () => {
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index 46740958090..47e09bb38bc 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -1,9 +1,14 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+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';
import BlobFilepath from '~/blob/components/blob_header_filepath.vue';
import ViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
+import {
+ RICH_BLOB_VIEWER_TITLE,
+ SIMPLE_BLOB_VIEWER,
+ SIMPLE_BLOB_VIEWER_TITLE,
+} from '~/blob/components/constants';
import TableContents from '~/blob/components/table_contents.vue';
import { Blob } from './mock_data';
@@ -11,12 +16,26 @@ import { Blob } from './mock_data';
describe('Blob Header Default Actions', () => {
let wrapper;
- function createComponent(blobProps = {}, options = {}, propsData = {}, shouldMount = false) {
- const method = shouldMount ? mount : shallowMount;
- const blobHash = 'foo-bar';
- wrapper = method.call(this, BlobHeader, {
+ const defaultProvide = {
+ blobHash: 'foo-bar',
+ };
+
+ const findDefaultActions = () => wrapper.findComponent(DefaultActions);
+ const findTableContents = () => wrapper.findComponent(TableContents);
+ const findViewSwitcher = () => wrapper.findComponent(ViewerSwitcher);
+ const findBlobFilePath = () => wrapper.findComponent(BlobFilepath);
+ const findRichTextEditorBtn = () => wrapper.findByLabelText(RICH_BLOB_VIEWER_TITLE);
+ const findSimpleTextEditorBtn = () => wrapper.findByLabelText(SIMPLE_BLOB_VIEWER_TITLE);
+
+ function createComponent({
+ blobProps = {},
+ options = {},
+ propsData = {},
+ mountFn = shallowMount,
+ } = {}) {
+ wrapper = mountFn(BlobHeader, {
provide: {
- blobHash,
+ ...defaultProvide,
},
propsData: {
blob: { ...Blob, ...blobProps },
@@ -26,143 +45,123 @@ describe('Blob Header Default Actions', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
- const findDefaultActions = () => wrapper.findComponent(DefaultActions);
-
- const slots = {
- prepend: 'Foo Prepend',
- actions: 'Actions Bar',
- };
-
it('matches the snapshot', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
- it('renders all components', () => {
- createComponent();
- expect(wrapper.findComponent(TableContents).exists()).toBe(true);
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(true);
- expect(findDefaultActions().exists()).toBe(true);
- expect(wrapper.findComponent(BlobFilepath).exists()).toBe(true);
+ describe('default render', () => {
+ it.each`
+ findComponent | componentName
+ ${findTableContents} | ${'TableContents'}
+ ${findViewSwitcher} | ${'ViewSwitcher'}
+ ${findDefaultActions} | ${'DefaultActions'}
+ ${findBlobFilePath} | ${'BlobFilePath'}
+ `('renders $componentName component by default', ({ findComponent }) => {
+ createComponent();
+
+ expect(findComponent().exists()).toBe(true);
+ });
});
it('does not render viewer switcher if the blob has only the simple viewer', () => {
createComponent({
- richViewer: null,
+ blobProps: {
+ richViewer: null,
+ },
});
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('does not render viewer switcher if a corresponding prop is passed', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hideViewerSwitcher: true,
},
- );
- expect(wrapper.findComponent(ViewerSwitcher).exists()).toBe(false);
+ });
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('does not render default actions is corresponding prop is passed', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hideDefaultActions: true,
},
- );
- expect(wrapper.findComponent(DefaultActions).exists()).toBe(false);
+ });
+ expect(findDefaultActions().exists()).toBe(false);
});
- Object.keys(slots).forEach((slot) => {
- it('renders the slots', () => {
- const slotContent = slots[slot];
- createComponent(
- {},
- {
- scopedSlots: {
- [slot]: `<span>${slotContent}</span>`,
- },
+ it.each`
+ slotContent | key
+ ${'Foo Prepend'} | ${'prepend'}
+ ${'Actions Bar'} | ${'actions'}
+ `('renders the slot $key', ({ key, slotContent }) => {
+ createComponent({
+ options: {
+ scopedSlots: {
+ [key]: `<span>${slotContent}</span>`,
},
- {},
- true,
- );
- expect(wrapper.text()).toContain(slotContent);
+ },
+ mountFn: mount,
});
+ expect(wrapper.text()).toContain(slotContent);
});
it('passes information about render error down to default actions', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ propsData: {
hasRenderError: true,
},
- );
+ });
expect(findDefaultActions().props('hasRenderError')).toBe(true);
});
it('passes the correct isBinary value to default actions when viewing a binary file', () => {
- createComponent({}, {}, { isBinary: true });
+ createComponent({ propsData: { isBinary: true } });
expect(findDefaultActions().props('isBinary')).toBe(true);
});
});
describe('functionality', () => {
- const newViewer = 'Foo Bar';
- const activeViewerType = 'Alpha Beta';
-
const factory = (hideViewerSwitcher = false) => {
- createComponent(
- {},
- {},
- {
- activeViewerType,
+ createComponent({
+ propsData: {
+ activeViewerType: SIMPLE_BLOB_VIEWER,
hideViewerSwitcher,
},
- );
+ mountFn: mountExtended,
+ });
};
- it('by default sets viewer data based on activeViewerType', () => {
+ it('shows the correctly selected view by default', () => {
factory();
- expect(wrapper.vm.viewer).toBe(activeViewerType);
+
+ expect(findViewSwitcher().exists()).toBe(true);
+ expect(findRichTextEditorBtn().props().selected).toBe(false);
+ expect(findSimpleTextEditorBtn().props().selected).toBe(true);
});
- it('sets viewer to null if the viewer switcher should be hidden', () => {
+ it('Does not show the viewer switcher should be hidden', () => {
factory(true);
- expect(wrapper.vm.viewer).toBe(null);
+
+ expect(findViewSwitcher().exists()).toBe(false);
});
it('watches the changes in viewer data and emits event when the change is registered', async () => {
factory();
- jest.spyOn(wrapper.vm, '$emit');
- wrapper.vm.viewer = newViewer;
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('viewer-changed', newViewer);
- });
-
- it('does not emit event if the switcher is not rendered', async () => {
- factory(true);
-
- expect(wrapper.vm.showViewerSwitcher).toBe(false);
- jest.spyOn(wrapper.vm, '$emit');
- wrapper.vm.viewer = newViewer;
+ await findRichTextEditorBtn().trigger('click');
- await nextTick();
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('viewer-changed')).toBeDefined();
});
it('sets different icons depending on the blob file type', async () => {
factory();
- expect(wrapper.vm.blobSwitcherDocIcon).toBe('document');
+
+ expect(findViewSwitcher().props('docIcon')).toBe('document');
+
await wrapper.setProps({
blob: {
...Blob,
@@ -172,7 +171,8 @@ describe('Blob Header Default Actions', () => {
},
},
});
- expect(wrapper.vm.blobSwitcherDocIcon).toBe('table');
+
+ expect(findViewSwitcher().props('docIcon')).toBe('table');
});
});
});
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index 1eac0733646..2ef87f6664b 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -18,14 +18,14 @@ describe('Blob Header Viewer Switcher', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
+ const findSimpleViewerButton = () => wrapper.findComponent('[data-viewer="simple"]');
+ const findRichViewerButton = () => wrapper.findComponent('[data-viewer="rich"]');
describe('intiialization', () => {
it('is initialized with simple viewer as active', () => {
createComponent();
- expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
+ expect(findSimpleViewerButton().props('selected')).toBe(true);
+ expect(findRichViewerButton().props('selected')).toBe(false);
});
});
@@ -52,45 +52,34 @@ describe('Blob Header Viewer Switcher', () => {
});
describe('viewer changes', () => {
- let buttons;
- let simpleBtn;
- let richBtn;
+ it('does not switch the viewer if the selected one is already active', async () => {
+ createComponent();
+ expect(findSimpleViewerButton().props('selected')).toBe(true);
- function factory(propsData = {}) {
- createComponent(propsData);
- buttons = wrapper.findAllComponents(GlButton);
- simpleBtn = buttons.at(0);
- richBtn = buttons.at(1);
-
- jest.spyOn(wrapper.vm, '$emit');
- }
-
- it('does not switch the viewer if the selected one is already active', () => {
- factory();
- expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
- simpleBtn.vm.$emit('click');
- expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ findSimpleViewerButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findSimpleViewerButton().props('selected')).toBe(true);
+ expect(wrapper.emitted('input')).toBe(undefined);
});
it('emits an event when a Rich Viewer button is clicked', async () => {
- factory();
- expect(wrapper.vm.value).toBe(SIMPLE_BLOB_VIEWER);
-
- richBtn.vm.$emit('click');
+ createComponent();
+ expect(findSimpleViewerButton().props('selected')).toBe(true);
+ findRichViewerButton().vm.$emit('click');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', RICH_BLOB_VIEWER);
+
+ expect(wrapper.emitted('input')).toEqual([[RICH_BLOB_VIEWER]]);
});
it('emits an event when a Simple Viewer button is clicked', async () => {
- factory({
- value: RICH_BLOB_VIEWER,
- });
- simpleBtn.vm.$emit('click');
+ createComponent({ value: RICH_BLOB_VIEWER });
+ findSimpleViewerButton().vm.$emit('click');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', SIMPLE_BLOB_VIEWER);
+
+ expect(wrapper.emitted('input')).toEqual([[SIMPLE_BLOB_VIEWER]]);
});
});
});
diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js
index b5803bf0cbc..6ecf5091591 100644
--- a/spec/frontend/blob/components/mock_data.js
+++ b/spec/frontend/blob/components/mock_data.js
@@ -47,11 +47,13 @@ export const BinaryBlob = {
};
export const RichBlobContentMock = {
+ __typename: 'Blob',
path: 'foo.md',
richData: '<h1>Rich</h1>',
};
export const SimpleBlobContentMock = {
+ __typename: 'Blob',
path: 'foo.js',
plainData: 'Plain',
};
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 6af9cdcae7d..acfcef9704c 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -31,7 +31,6 @@ describe('Markdown table of contents component', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/blob/csv/csv_viewer_spec.js b/spec/frontend/blob/csv/csv_viewer_spec.js
index 9364f76da5e..8f105f04aa7 100644
--- a/spec/frontend/blob/csv/csv_viewer_spec.js
+++ b/spec/frontend/blob/csv/csv_viewer_spec.js
@@ -29,10 +29,6 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(PapaParseAlert);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render loading spinner', () => {
createComponent();
diff --git a/spec/frontend/blob/file_template_selector_spec.js b/spec/frontend/blob/file_template_selector_spec.js
index 65444e86efd..123475f8d62 100644
--- a/spec/frontend/blob/file_template_selector_spec.js
+++ b/spec/frontend/blob/file_template_selector_spec.js
@@ -53,7 +53,7 @@ describe('FileTemplateSelector', () => {
expect(subject.wrapper.classList.contains('hidden')).toBe(false);
});
- it('sets the focus on the dropdown', async () => {
+ it('sets the focus on the dropdown', () => {
subject.show();
jest.spyOn(subject.dropdown, 'focus');
jest.runAllTimers();
diff --git a/spec/frontend/blob/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js
index 21d4e8db503..b2e1a29b84f 100644
--- a/spec/frontend/blob/line_highlighter_spec.js
+++ b/spec/frontend/blob/line_highlighter_spec.js
@@ -1,7 +1,7 @@
/* eslint-disable no-return-assign, no-new, no-underscore-dangle */
-
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LineHighlighter from '~/blob/line_highlighter';
import * as utils from '~/lib/utils/common_utils';
@@ -17,7 +17,7 @@ describe('LineHighlighter', () => {
};
beforeEach(() => {
- loadHTMLFixture('static/line_highlighter.html');
+ setHTMLFixture(htmlStaticLineHighlighter);
testContext.class = new LineHighlighter();
testContext.css = testContext.class.highlightLineClass;
return (testContext.spies = {
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index 2e7eadc912d..97b32a42afe 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -42,8 +42,6 @@ describe('iPython notebook renderer', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mock.restore();
});
diff --git a/spec/frontend/blob/pdf/pdf_viewer_spec.js b/spec/frontend/blob/pdf/pdf_viewer_spec.js
index 23227df6357..19d404f504b 100644
--- a/spec/frontend/blob/pdf/pdf_viewer_spec.js
+++ b/spec/frontend/blob/pdf/pdf_viewer_spec.js
@@ -26,11 +26,6 @@ describe('PDF renderer', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('shows loading icon', () => {
expect(findLoading().exists()).toBe(true);
});
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index 81b38cfc278..84efa6041e4 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -38,7 +38,6 @@ describe('PipelineTourSuccessModal', () => {
});
afterEach(() => {
- wrapper.destroy();
unmockTracking();
Cookies.remove(modalProps.commitCookie);
});
diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js
index 4b6cb79791c..64b6152a07d 100644
--- a/spec/frontend/blob/sketch/index_spec.js
+++ b/spec/frontend/blob/sketch/index_spec.js
@@ -1,10 +1,11 @@
import SketchLoader from '~/blob/sketch';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
+import htmlSketchViewer from 'test_fixtures_static/sketch_viewer.html';
describe('Sketch viewer', () => {
beforeEach(() => {
- loadHTMLFixture('static/sketch_viewer.html');
+ setHTMLFixture(htmlSketchViewer);
});
afterEach(() => {
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
index 6b329dc078a..b30b0287a34 100644
--- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -36,11 +36,6 @@ describe('Suggest gitlab-ci.yml Popover', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when no dismiss cookie is set', () => {
beforeEach(() => {
createWrapper(defaultTrackLabel);
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index ed42322b0e6..6a7ca3288cb 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -5,12 +5,19 @@ import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
import SourceEditor from '~/blob_edit/edit_blob';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
jest.mock('~/blob_edit/edit_blob');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('BlobBundle', () => {
+ beforeAll(() => {
+ // HACK: Workaround readonly property in Jest
+ Object.defineProperty(window, 'onbeforeunload', {
+ writable: true,
+ });
+ });
+
it('does not load SourceEditor by default', () => {
blobBundle();
expect(SourceEditor).not.toHaveBeenCalled();
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index dda46e97b85..9ab20fc2cd7 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -20,9 +20,9 @@ jest.mock('~/editor/extensions/source_editor_toolbar_ext');
const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown';
const defaultExtensions = [
+ { definition: ToolbarExtension },
{ definition: SourceEditorExtension },
{ definition: FileTemplateExtension },
- { definition: ToolbarExtension },
];
const markdownExtensions = [
{ definition: EditorMarkdownExtension },
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 1e823e3321a..a925f752f5e 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -84,7 +84,7 @@ describe('Board card component', () => {
BoardCardMoveToPosition: true,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
provide: {
rootPath: '/',
@@ -110,8 +110,6 @@ describe('Board card component', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
store = null;
jest.clearAllMocks();
});
@@ -170,7 +168,7 @@ describe('Board card component', () => {
});
describe('blocked', () => {
- it('renders blocked icon if issue is blocked', async () => {
+ it('renders blocked icon if issue is blocked', () => {
createWrapper({
props: {
item: {
@@ -314,10 +312,6 @@ describe('Board card component', () => {
});
});
- afterEach(() => {
- global.gon.default_avatar_url = null;
- });
-
it('displays defaults avatar if users avatar is null', () => {
expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
@@ -493,7 +487,7 @@ describe('Board card component', () => {
});
describe('loading', () => {
- it('renders loading icon', async () => {
+ it('renders loading icon', () => {
createWrapper({
props: {
item: {
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index d882ff071b7..43cf6ead1c1 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -92,6 +92,7 @@ export default function createComponent({
boardItems: [issue],
canAdminList: true,
boardId: 'gid://gitlab/Board/1',
+ filterParams: {},
...componentProps,
},
provide: {
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index fc8dbf8dc3a..e0a110678b1 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,3 +1,4 @@
+import { GlIntersectionObserver } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { nextTick } from 'vue';
import { DraggableItemTypes, ListType } from 'ee_else_ce/boards/constants';
@@ -8,15 +9,18 @@ import BoardCard from '~/boards/components/board_card.vue';
import eventHub from '~/boards/eventhub';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
-import { mockIssues } from './mock_data';
+import { mockIssues, mockList, mockIssuesMore } from './mock_data';
describe('Board list component', () => {
let wrapper;
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
- const findIssueCountLoadingIcon = () => wrapper.find('[data-testid="count-loading-icon"]');
const findDraggable = () => wrapper.findComponent(Draggable);
const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findBoardListCount = () => wrapper.find('.board-list-count');
+
+ const triggerInfiniteScroll = () => findIntersectionObserver().vm.$emit('appear');
const startDrag = (
params = {
@@ -36,10 +40,6 @@ describe('Board list component', () => {
useFakeRequestAnimationFrame();
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When Expanded', () => {
beforeEach(() => {
wrapper = createComponent({ issuesCount: 1 });
@@ -65,41 +65,25 @@ describe('Board list component', () => {
expect(wrapper.find('.board-card').attributes('data-item-id')).toBe('1');
});
- it('shows new issue form', async () => {
- wrapper.vm.toggleForm();
-
- await nextTick();
- expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
- });
-
it('shows new issue form after eventhub event', async () => {
- eventHub.$emit(`toggle-issue-form-${wrapper.vm.list.id}`);
+ eventHub.$emit(`toggle-issue-form-${mockList.id}`);
await nextTick();
expect(wrapper.find('.board-new-issue-form').exists()).toBe(true);
});
- it('does not show new issue form for closed list', () => {
- wrapper.setProps({ list: { type: 'closed' } });
- wrapper.vm.toggleForm();
-
- expect(wrapper.find('.board-new-issue-form').exists()).toBe(false);
- });
-
- it('shows count list item', async () => {
- wrapper.vm.showCount = true;
-
- await nextTick();
- expect(wrapper.find('.board-list-count').exists()).toBe(true);
-
- expect(wrapper.find('.board-list-count').text()).toBe('Showing all issues');
- });
+ it('does not show new issue form for closed list', async () => {
+ wrapper = createComponent({
+ listProps: {
+ listType: ListType.closed,
+ },
+ });
+ await waitForPromises();
- it('sets data attribute with invalid id', async () => {
- wrapper.vm.showCount = true;
+ eventHub.$emit(`toggle-issue-form-${mockList.id}`);
await nextTick();
- expect(wrapper.find('.board-list-count').attributes('data-issue-id')).toBe('-1');
+ expect(wrapper.find('.board-new-issue-form').exists()).toBe(false);
});
it('renders the move to position icon', () => {
@@ -122,61 +106,41 @@ describe('Board list component', () => {
});
describe('load more issues', () => {
- const actions = {
- fetchItemsForList: jest.fn(),
- };
-
- it('does not load issues if already loading', () => {
- wrapper = createComponent({
- actions,
- state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
+ describe('when loading is not in progress', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ listProps: {
+ id: 'gid://gitlab/List/1',
+ },
+ componentProps: {
+ boardItems: mockIssuesMore,
+ },
+ actions: {
+ fetchItemsForList: jest.fn(),
+ },
+ state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: false } } },
+ });
});
- wrapper.vm.listRef.dispatchEvent(new Event('scroll'));
-
- expect(actions.fetchItemsForList).not.toHaveBeenCalled();
- });
- it('shows loading more spinner', async () => {
- wrapper = createComponent({
- state: { listsFlags: { 'gid://gitlab/List/1': { isLoadingMore: true } } },
- data: {
- showCount: true,
- },
+ it('has intersection observer when the number of board list items are more than 5', () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
});
- await nextTick();
-
- expect(findIssueCountLoadingIcon().exists()).toBe(true);
- });
-
- it('shows how many more issues to load', async () => {
- wrapper = createComponent({
- data: {
- showCount: true,
- },
+ it('shows count when loaded more items and correct data attribute', async () => {
+ triggerInfiniteScroll();
+ await waitForPromises();
+ expect(findBoardListCount().exists()).toBe(true);
+ expect(findBoardListCount().attributes('data-issue-id')).toBe('-1');
});
-
- await nextTick();
- await waitForPromises();
- await nextTick();
- await nextTick();
-
- expect(wrapper.find('.board-list-count').text()).toBe('Showing 1 of 20 issues');
});
});
describe('max issue count warning', () => {
- beforeEach(() => {
- wrapper = createComponent({
- listProps: { issuesCount: 50 },
- });
- });
-
describe('when issue count exceeds max issue count', () => {
it('sets background to gl-bg-red-100', async () => {
- wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
+ wrapper = createComponent({ listProps: { issuesCount: 4, maxIssueCount: 3 } });
- await nextTick();
+ await waitForPromises();
const block = wrapper.find('.gl-bg-red-100');
expect(block.exists()).toBe(true);
@@ -187,16 +151,18 @@ describe('Board list component', () => {
});
describe('when list issue count does NOT exceed list max issue count', () => {
- it('does not sets background to gl-bg-red-100', () => {
- wrapper.setProps({ list: { issuesCount: 2, maxIssueCount: 3 } });
+ it('does not sets background to gl-bg-red-100', async () => {
+ wrapper = createComponent({ list: { issuesCount: 2, maxIssueCount: 3 } });
+ await waitForPromises();
expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false);
});
});
describe('when list max issue count is 0', () => {
- it('does not sets background to gl-bg-red-100', () => {
- wrapper.setProps({ list: { maxIssueCount: 0 } });
+ it('does not sets background to gl-bg-red-100', async () => {
+ wrapper = createComponent({ list: { maxIssueCount: 0 } });
+ await waitForPromises();
expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false);
});
diff --git a/spec/frontend/boards/components/board_add_new_column_form_spec.js b/spec/frontend/boards/components/board_add_new_column_form_spec.js
index 0b3c6cb24c4..4fc9a6859a6 100644
--- a/spec/frontend/boards/components/board_add_new_column_form_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_form_spec.js
@@ -1,15 +1,13 @@
-import { GlDropdown, GlFormGroup, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import defaultState from '~/boards/stores/state';
import { mockLabelList } from '../mock_data';
Vue.use(Vuex);
-describe('Board card layout', () => {
+describe('BoardAddNewColumnForm', () => {
let wrapper;
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
@@ -23,56 +21,30 @@ describe('Board card layout', () => {
});
};
- const mountComponent = ({
- loading = false,
- noneSelected = '',
- searchLabel = '',
- searchPlaceholder = '',
- selectedId,
- actions,
- slots,
- } = {}) => {
- wrapper = extendedWrapper(
- shallowMount(BoardAddNewColumnForm, {
- propsData: {
- loading,
- noneSelected,
- searchLabel,
- searchPlaceholder,
- selectedId,
- },
- slots,
- store: createStore({
- actions: {
- setAddColumnFormVisibility: jest.fn(),
- ...actions,
- },
- }),
- stubs: {
- GlDropdown,
+ const mountComponent = ({ searchLabel = '', selectedIdValid = true, actions, slots } = {}) => {
+ wrapper = shallowMountExtended(BoardAddNewColumnForm, {
+ propsData: {
+ searchLabel,
+ selectedIdValid,
+ },
+ slots,
+ store: createStore({
+ actions: {
+ setAddColumnFormVisibility: jest.fn(),
+ ...actions,
},
}),
- );
+ });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const formTitle = () => wrapper.findByTestId('board-add-column-form-title').text();
- const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
- const findSearchLabelFormGroup = () => wrapper.findComponent(GlFormGroup);
const cancelButton = () => wrapper.findByTestId('cancelAddNewColumn');
const submitButton = () => wrapper.findByTestId('addNewColumnButton');
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- it('shows form title & search input', () => {
+ it('shows form title', () => {
mountComponent();
- findDropdown().vm.$emit('show');
-
expect(formTitle()).toEqual(BoardAddNewColumnForm.i18n.newList);
- expect(findSearchInput().exists()).toBe(true);
});
it('clicking cancel hides the form', () => {
@@ -88,61 +60,6 @@ describe('Board card layout', () => {
expect(setAddColumnFormVisibility).toHaveBeenCalledWith(expect.anything(), false);
});
- describe('items', () => {
- const mountWithItems = (loading) =>
- mountComponent({
- loading,
- slots: {
- items: '<div class="item-slot">Some kind of list</div>',
- },
- });
-
- it('hides items slot and shows skeleton while loading', () => {
- mountWithItems(true);
-
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
- expect(wrapper.find('.item-slot').exists()).toBe(false);
- });
-
- it('shows items slot and hides skeleton while not loading', () => {
- mountWithItems(false);
-
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
- expect(wrapper.find('.item-slot').exists()).toBe(true);
- });
- });
-
- describe('search box', () => {
- it('sets label and placeholder text from props', () => {
- const props = {
- searchLabel: 'Some items',
- searchPlaceholder: 'Search for an item',
- };
-
- mountComponent(props);
-
- expect(findSearchLabelFormGroup().attributes('label')).toEqual(props.searchLabel);
- expect(findSearchInput().attributes('placeholder')).toEqual(props.searchPlaceholder);
- });
-
- it('does not show the dropdown as invalid by default', () => {
- mountComponent();
-
- expect(findSearchLabelFormGroup().attributes('state')).toBe('true');
- expect(findDropdown().props('toggleClass')).not.toContain('gl-inset-border-1-red-400!');
- });
-
- it('emits filter event on input', () => {
- mountComponent();
-
- const searchText = 'some text';
-
- findSearchInput().vm.$emit('input', searchText);
-
- expect(wrapper.emitted('filter-items')).toEqual([[searchText]]);
- });
- });
-
describe('Add list button', () => {
it('is enabled by default', () => {
mountComponent();
@@ -159,16 +76,5 @@ describe('Board card layout', () => {
expect(wrapper.emitted('add-list')).toEqual([[]]);
});
-
- it('does not emit the add-list event on click and shows the dropdown as invalid when no ID is selected', async () => {
- mountComponent();
-
- await submitButton().vm.$emit('click');
-
- expect(findSearchLabelFormGroup().attributes('state')).toBeUndefined();
- expect(findDropdown().props('toggleClass')).toContain('gl-inset-border-1-red-400!');
-
- expect(wrapper.emitted('add-list')).toBeUndefined();
- });
});
});
diff --git a/spec/frontend/boards/components/board_add_new_column_spec.js b/spec/frontend/boards/components/board_add_new_column_spec.js
index a3b2988ce75..a09c3aaa55e 100644
--- a/spec/frontend/boards/components/board_add_new_column_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_spec.js
@@ -1,8 +1,7 @@
-import { GlFormRadioGroup } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardAddNewColumn from '~/boards/components/board_add_new_column.vue';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import defaultState from '~/boards/stores/state';
@@ -13,8 +12,9 @@ Vue.use(Vuex);
describe('Board card layout', () => {
let wrapper;
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const selectLabel = (id) => {
- wrapper.findComponent(GlFormRadioGroup).vm.$emit('change', id);
+ findDropdown().vm.$emit('select', id);
};
const createStore = ({ actions = {}, getters = {}, state = {} } = {}) => {
@@ -34,33 +34,34 @@ describe('Board card layout', () => {
getListByLabelId = jest.fn(),
actions = {},
} = {}) => {
- wrapper = extendedWrapper(
- shallowMount(BoardAddNewColumn, {
- data() {
- return {
- selectedId,
- };
+ wrapper = shallowMountExtended(BoardAddNewColumn, {
+ data() {
+ return {
+ selectedId,
+ };
+ },
+ store: createStore({
+ actions: {
+ fetchLabels: jest.fn(),
+ setAddColumnFormVisibility: jest.fn(),
+ ...actions,
},
- store: createStore({
- actions: {
- fetchLabels: jest.fn(),
- setAddColumnFormVisibility: jest.fn(),
- ...actions,
- },
- getters: {
- getListByLabelId: () => getListByLabelId,
- },
- state: {
- labels,
- labelsLoading: false,
- },
- }),
- provide: {
- scopedLabelsAvailable: true,
- isEpicBoard: false,
+ getters: {
+ getListByLabelId: () => getListByLabelId,
+ },
+ state: {
+ labels,
+ labelsLoading: false,
},
}),
- );
+ provide: {
+ scopedLabelsAvailable: true,
+ isEpicBoard: false,
+ },
+ stubs: {
+ GlCollapsibleListbox,
+ },
+ });
// trigger change event
if (selectedId) {
@@ -68,10 +69,6 @@ describe('Board card layout', () => {
}
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Add list button', () => {
it('calls addList', async () => {
const getListByLabelId = jest.fn().mockReturnValue(null);
diff --git a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
index 354eb7bff16..d8b93e1f3b6 100644
--- a/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
+++ b/spec/frontend/boards/components/board_add_new_column_trigger_spec.js
@@ -17,7 +17,7 @@ describe('BoardAddNewColumnTrigger', () => {
const mountComponent = () => {
wrapper = mountExtended(BoardAddNewColumnTrigger, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
store: createStore(),
});
@@ -27,10 +27,6 @@ describe('BoardAddNewColumnTrigger', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when button is active', () => {
it('does not show the tooltip', () => {
const tooltip = findTooltipText();
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index 12318fb5d16..3d6e4c18f51 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -1,14 +1,22 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import BoardApp from '~/boards/components/board_app.vue';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
+import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
+import { rawIssue, boardListsQueryResponse } from '../mock_data';
describe('BoardApp', () => {
let wrapper;
let store;
+ const boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse);
+ const mockApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
Vue.use(Vuex);
+ Vue.use(VueApollo);
const createStore = ({ mockGetters = {} } = {}) => {
store = new Vuex.Store({
@@ -23,18 +31,31 @@ describe('BoardApp', () => {
});
};
- const createComponent = () => {
+ const createComponent = ({ isApolloBoard = false, issue = rawIssue } = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: activeBoardItemQuery,
+ data: {
+ activeBoardItem: issue,
+ },
+ });
+
wrapper = shallowMount(BoardApp, {
+ apolloProvider: mockApollo,
store,
provide: {
+ fullPath: 'gitlab-org',
initialBoardId: 'gid://gitlab/Board/1',
+ initialFilterParams: {},
+ issuableType: 'issue',
+ boardType: 'group',
+ isIssueBoard: true,
+ isGroupBoard: true,
+ isApolloBoard,
},
});
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
store = null;
});
@@ -51,4 +72,26 @@ describe('BoardApp', () => {
expect(wrapper.classes()).not.toContain('is-compact');
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createComponent({ isApolloBoard: true });
+ await nextTick();
+ });
+
+ it('fetches lists', () => {
+ expect(boardListQueryHandler).toHaveBeenCalled();
+ });
+
+ it('should have is-compact class when a card is selected', () => {
+ expect(wrapper.classes()).toContain('is-compact');
+ });
+
+ it('should not have is-compact class when no card is selected', async () => {
+ createComponent({ isApolloBoard: true, issue: {} });
+ await nextTick();
+
+ expect(wrapper.classes()).not.toContain('is-compact');
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 84e6318d98e..897219303b5 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -1,8 +1,10 @@
import { GlLabel } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardCard from '~/boards/components/board_card.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { inactiveId } from '~/boards/constants';
@@ -14,6 +16,14 @@ describe('Board card', () => {
let mockActions;
Vue.use(Vuex);
+ Vue.use(VueApollo);
+
+ const mockSetActiveBoardItemResolver = jest.fn();
+ const mockApollo = createMockApollo([], {
+ Mutation: {
+ setActiveBoardItem: mockSetActiveBoardItemResolver,
+ },
+ });
const createStore = ({ initialState = {} } = {}) => {
mockActions = {
@@ -36,11 +46,11 @@ describe('Board card', () => {
const mountComponent = ({
propsData = {},
provide = {},
- mountFn = shallowMount,
stubs = { BoardCardInner },
item = mockIssue,
} = {}) => {
- wrapper = mountFn(BoardCard, {
+ wrapper = shallowMountExtended(BoardCard, {
+ apolloProvider: mockApollo,
stubs: {
...stubs,
BoardCardInner,
@@ -56,9 +66,9 @@ describe('Board card', () => {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
+ isIssueBoard: true,
isEpicBoard: false,
issuableType: 'issue',
- isProjectBoard: false,
isGroupBoard: true,
disabled: false,
isApolloBoard: false,
@@ -82,8 +92,6 @@ describe('Board card', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
store = null;
});
@@ -98,7 +106,7 @@ describe('Board card', () => {
});
});
- it('should not highlight the card by default', async () => {
+ it('should not highlight the card by default', () => {
createStore();
mountComponent();
@@ -106,7 +114,7 @@ describe('Board card', () => {
expect(wrapper.classes()).not.toContain('multi-select');
});
- it('should highlight the card with a correct style when selected', async () => {
+ it('should highlight the card with a correct style when selected', () => {
createStore({
initialState: {
activeId: mockIssue.id,
@@ -118,7 +126,7 @@ describe('Board card', () => {
expect(wrapper.classes()).not.toContain('multi-select');
});
- it('should highlight the card with a correct style when multi-selected', async () => {
+ it('should highlight the card with a correct style when multi-selected', () => {
createStore({
initialState: {
activeId: inactiveId,
@@ -220,4 +228,25 @@ describe('Board card', () => {
expect(wrapper.attributes('style')).toBeUndefined();
});
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createStore();
+ mountComponent({ provide: { isApolloBoard: true } });
+ await nextTick();
+ });
+
+ it('set active board item on client when clicking on card', async () => {
+ await selectCard();
+
+ expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
+ {},
+ {
+ boardItem: mockIssue,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index c0bb51620f2..5717031be20 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -10,11 +10,6 @@ describe('Board Column Component', () => {
let wrapper;
let store;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const initStore = () => {
store = createStore();
};
@@ -36,6 +31,7 @@ describe('Board Column Component', () => {
propsData: {
list: listMock,
boardId: 'gid://gitlab/Board/1',
+ filters: {},
},
provide: {
isApolloBoard: false,
@@ -85,7 +81,7 @@ describe('Board Column Component', () => {
});
describe('on mount', () => {
- beforeEach(async () => {
+ beforeEach(() => {
initStore();
jest.spyOn(store, 'dispatch').mockImplementation();
});
diff --git a/spec/frontend/boards/components/board_configuration_options_spec.js b/spec/frontend/boards/components/board_configuration_options_spec.js
index 6f0971a9458..199a08c5d83 100644
--- a/spec/frontend/boards/components/board_configuration_options_spec.js
+++ b/spec/frontend/boards/components/board_configuration_options_spec.js
@@ -16,10 +16,6 @@ describe('BoardConfigurationOptions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const backlogListCheckbox = () => wrapper.find('[data-testid="backlog-list-checkbox"]');
const closedListCheckbox = () => wrapper.find('[data-testid="closed-list-checkbox"]');
@@ -66,8 +62,8 @@ describe('BoardConfigurationOptions', () => {
it('renders checkboxes disabled when user does not have edit rights', () => {
createComponent({ readonly: true });
- expect(closedListCheckbox().attributes('disabled')).toBe('true');
- expect(backlogListCheckbox().attributes('disabled')).toBe('true');
+ expect(closedListCheckbox().attributes('disabled')).toBeDefined();
+ expect(backlogListCheckbox().attributes('disabled')).toBeDefined();
});
it('renders checkboxes enabled when user has edit rights', () => {
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 955267a415c..9be2696de56 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -1,10 +1,15 @@
import { GlDrawer } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
@@ -14,13 +19,21 @@ import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severit
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
-import { mockActiveIssue, mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
+import { mockActiveIssue, mockIssue, rawIssue } from '../mock_data';
Vue.use(Vuex);
+Vue.use(VueApollo);
describe('BoardContentSidebar', () => {
let wrapper;
let store;
+ const mockSetActiveBoardItemResolver = jest.fn();
+ const mockApollo = createMockApollo([], {
+ Mutation: {
+ setActiveBoardItem: mockSetActiveBoardItemResolver,
+ },
+ });
+
const createStore = ({ mockGetters = {}, mockActions = {} } = {}) => {
store = new Vuex.Store({
state: {
@@ -32,30 +45,29 @@ describe('BoardContentSidebar', () => {
activeBoardItem: () => {
return { ...mockActiveIssue, epic: null };
},
- groupPathForActiveIssue: () => mockIssueGroupPath,
- projectPathForActiveIssue: () => mockIssueProjectPath,
- isSidebarOpen: () => true,
...mockGetters,
},
actions: mockActions,
});
};
- const createComponent = () => {
- /*
- Dynamically imported components (in our case ee imports)
- aren't stubbed automatically in VTU v1:
- https://github.com/vuejs/vue-test-utils/issues/1279.
+ const createComponent = ({ isApolloBoard = false } = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: activeBoardItemQuery,
+ data: {
+ activeBoardItem: rawIssue,
+ },
+ });
- This requires us to additionally mock apollo or vuex stores.
- */
- wrapper = shallowMount(BoardContentSidebar, {
+ wrapper = shallowMountExtended(BoardContentSidebar, {
+ apolloProvider: mockApollo,
provide: {
canUpdate: true,
rootPath: '/',
groupId: 1,
issuableType: TYPE_ISSUE,
isGroupBoard: false,
+ isApolloBoard,
},
store,
stubs: {
@@ -63,24 +75,6 @@ describe('BoardContentSidebar', () => {
template: '<div><slot name="header"></slot><slot></slot></div>',
}),
},
- mocks: {
- $apollo: {
- queries: {
- participants: {
- loading: false,
- },
- currentIteration: {
- loading: false,
- },
- iterations: {
- loading: false,
- },
- attributesList: {
- loading: false,
- },
- },
- },
- },
});
};
@@ -89,10 +83,6 @@ describe('BoardContentSidebar', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('confirms we render GlDrawer', () => {
expect(wrapper.findComponent(GlDrawer).exists()).toBe(true);
});
@@ -105,10 +95,12 @@ describe('BoardContentSidebar', () => {
});
});
- it('does not render GlDrawer when isSidebarOpen is false', () => {
- createStore({ mockGetters: { isSidebarOpen: () => false } });
+ it('does not render GlDrawer when no active item is set', async () => {
+ createStore({ mockGetters: { activeBoardItem: () => ({ id: '', iid: '' }) } });
createComponent();
+ await nextTick();
+
expect(wrapper.findComponent(GlDrawer).props('open')).toBe(false);
});
@@ -170,7 +162,7 @@ describe('BoardContentSidebar', () => {
createComponent();
});
- it('calls toggleBoardItem with correct parameters', async () => {
+ it('calls toggleBoardItem with correct parameters', () => {
wrapper.findComponent(GlDrawer).vm.$emit('close');
expect(toggleBoardItem).toHaveBeenCalledTimes(1);
@@ -193,4 +185,27 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(SidebarSeverityWidget).exists()).toBe(true);
});
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createStore();
+ createComponent({ isApolloBoard: true });
+ await nextTick();
+ });
+
+ it('calls setActiveBoardItemMutation on close', async () => {
+ wrapper.findComponent(GlDrawer).vm.$emit('close');
+
+ await waitForPromises();
+
+ expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
+ {},
+ {
+ boardItem: null,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 97596c86198..e14f661a8bd 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -1,20 +1,18 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
+import Vue from 'vue';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
+import eventHub from '~/boards/eventhub';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
-import createMockApollo from 'helpers/mock_apollo_helper';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
-import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
-import { mockLists, boardListsQueryResponse } from '../mock_data';
+import { mockLists, mockListsById } from '../mock_data';
-Vue.use(VueApollo);
Vue.use(Vuex);
const actions = {
@@ -23,8 +21,6 @@ const actions = {
describe('BoardContent', () => {
let wrapper;
- let fakeApollo;
- window.gon = {};
const defaultState = {
isShowingEpicsSwimlanes: false,
@@ -49,24 +45,21 @@ describe('BoardContent', () => {
issuableType = 'issue',
isIssueBoard = true,
isEpicBoard = false,
- boardListQueryHandler = jest.fn().mockResolvedValue(boardListsQueryResponse),
} = {}) => {
- fakeApollo = createMockApollo([[boardListsQuery, boardListQueryHandler]]);
-
const store = createStore({
...defaultState,
...state,
});
wrapper = shallowMount(BoardContent, {
- apolloProvider: fakeApollo,
propsData: {
boardId: 'gid://gitlab/Board/1',
+ filterParams: {},
+ isSwimlanesOn: false,
+ boardListsApollo: mockListsById,
...props,
},
provide: {
canAdminList,
- boardType: 'group',
- fullPath: 'gitlab-org/gitlab',
issuableType,
isIssueBoard,
isEpicBoard,
@@ -75,37 +68,14 @@ describe('BoardContent', () => {
isApolloBoard,
},
store,
+ stubs: {
+ BoardContentSidebar: stubComponent(BoardContentSidebar, {
+ template: '<div></div>',
+ }),
+ },
});
};
- beforeAll(() => {
- global.ResizeObserver = class MockResizeObserver {
- constructor(callback) {
- this.callback = callback;
-
- this.entries = [];
- }
-
- observe(entry) {
- this.entries.push(entry);
- }
-
- disconnect() {
- this.entries = [];
- this.callback = null;
- }
-
- trigger() {
- this.callback(this.entries);
- }
- };
- });
-
- afterEach(() => {
- wrapper.destroy();
- fakeApollo = null;
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -124,34 +94,6 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
- it('on small screens, sets board container height to full height', async () => {
- window.innerHeight = 1000;
- window.innerWidth = 767;
- jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 });
-
- wrapper.vm.resizeObserver.trigger();
-
- await nextTick();
-
- const style = wrapper.findComponent({ ref: 'list' }).attributes('style');
-
- expect(style).toBe('height: 1000px;');
- });
-
- it('on large screens, sets board container height fill area below filters', async () => {
- window.innerHeight = 1000;
- window.innerWidth = 768;
- jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue({ top: 100 });
-
- wrapper.vm.resizeObserver.trigger();
-
- await nextTick();
-
- const style = wrapper.findComponent({ ref: 'list' }).attributes('style');
-
- expect(style).toBe('height: 900px;');
- });
-
it('sets delay and delayOnTouchOnly attributes on board list', () => {
const listEl = wrapper.findComponent({ ref: 'list' });
@@ -203,5 +145,14 @@ describe('BoardContent', () => {
it('renders BoardContentSidebar', () => {
expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true);
});
+
+ it('refetches lists when updateBoard event is received', async () => {
+ jest.spyOn(eventHub, '$on').mockImplementation(() => {});
+
+ createComponent({ isApolloBoard: true });
+ await waitForPromises();
+
+ expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
+ });
});
});
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index 4c0cc36889c..5a976816f74 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
-import * as urlUtility from '~/lib/utils/url_utility';
+import { updateHistory } from '~/lib/utils/url_utility';
import {
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_LABEL,
@@ -23,6 +23,12 @@ import { createStore } from '~/boards/stores';
Vue.use(Vuex);
+jest.mock('~/lib/utils/url_utility', () => ({
+ updateHistory: jest.fn(),
+ setUrlParams: jest.requireActual('~/lib/utils/url_utility').setUrlParams,
+ queryToObject: jest.requireActual('~/lib/utils/url_utility').queryToObject,
+}));
+
describe('BoardFilteredSearch', () => {
let wrapper;
let store;
@@ -55,10 +61,10 @@ describe('BoardFilteredSearch', () => {
},
];
- const createComponent = ({ initialFilterParams = {}, props = {} } = {}) => {
+ const createComponent = ({ initialFilterParams = {}, props = {}, provide = {} } = {}) => {
store = createStore();
wrapper = shallowMount(BoardFilteredSearch, {
- provide: { initialFilterParams, fullPath: '' },
+ provide: { initialFilterParams, fullPath: '', isApolloBoard: false, ...provide },
store,
propsData: {
...props,
@@ -69,10 +75,6 @@ describe('BoardFilteredSearch', () => {
const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBarRoot);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -92,10 +94,9 @@ describe('BoardFilteredSearch', () => {
});
it('calls historyPushState', () => {
- jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]);
- expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ expect(updateHistory).toHaveBeenCalledWith({
replace: true,
title: '',
url: 'http://test.host/',
@@ -124,10 +125,10 @@ describe('BoardFilteredSearch', () => {
beforeEach(() => {
createComponent();
- jest.spyOn(wrapper.vm, 'performSearch').mockImplementation();
+ jest.spyOn(store, 'dispatch').mockImplementation();
});
- it('sets the url params to the correct results', async () => {
+ it('sets the url params to the correct results', () => {
const mockFilters = [
{ type: TOKEN_TYPE_AUTHOR, value: { data: 'root', operator: '=' } },
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: 'root', operator: '=' } },
@@ -141,10 +142,11 @@ describe('BoardFilteredSearch', () => {
{ type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: '=' } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: '!=' } },
];
- jest.spyOn(urlUtility, 'updateHistory');
+
findFilteredSearch().vm.$emit('onFilter', mockFilters);
- expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ expect(store.dispatch).toHaveBeenCalledWith('performSearch');
+ expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
url:
@@ -162,10 +164,10 @@ describe('BoardFilteredSearch', () => {
const mockFilters = [
{ type: TOKEN_TYPE_ASSIGNEE, value: { data: assigneeParam, operator: '=' } },
];
- jest.spyOn(urlUtility, 'updateHistory');
+
findFilteredSearch().vm.$emit('onFilter', mockFilters);
- expect(urlUtility.updateHistory).toHaveBeenCalledWith({
+ expect(updateHistory).toHaveBeenCalledWith({
title: '',
replace: true,
url: expected,
@@ -179,8 +181,6 @@ describe('BoardFilteredSearch', () => {
createComponent({
initialFilterParams: { authorUsername: 'root', labelName: ['label'], healthStatus: 'Any' },
});
-
- jest.spyOn(store, 'dispatch');
});
it('passes the correct props to FilterSearchBar', () => {
@@ -191,4 +191,22 @@ describe('BoardFilteredSearch', () => {
]);
});
});
+
+ describe('when Apollo boards FF is on', () => {
+ beforeEach(() => {
+ createComponent({ provide: { isApolloBoard: true } });
+ });
+
+ it('emits setFilters and updates URL when onFilter is emitted', () => {
+ findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]);
+
+ expect(updateHistory).toHaveBeenCalledWith({
+ title: '',
+ replace: true,
+ url: 'http://test.host/',
+ });
+
+ expect(wrapper.emitted('setFilters')).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index f8154145d43..f340dfab359 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -10,12 +10,14 @@ import { formType } from '~/boards/constants';
import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql';
import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
+import eventHub from '~/boards/eventhub';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn().mockName('visitUrlMock'),
}));
+jest.mock('~/boards/eventhub');
Vue.use(Vuex);
@@ -59,18 +61,14 @@ describe('BoardForm', () => {
},
});
- const createComponent = (props, data) => {
+ const createComponent = (props, provide) => {
wrapper = shallowMountExtended(BoardForm, {
propsData: { ...defaultProps, ...props },
- data() {
- return {
- ...data,
- };
- },
provide: {
boardBaseUrl: 'root',
isGroupBoard: true,
isProjectBoard: false,
+ ...provide,
},
mocks: {
$apollo: {
@@ -83,8 +81,6 @@ describe('BoardForm', () => {
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mutate = null;
});
@@ -119,7 +115,7 @@ describe('BoardForm', () => {
expect(findForm().exists()).toBe(true);
});
- it('focuses an input field', async () => {
+ it('focuses an input field', () => {
expect(document.activeElement).toBe(wrapper.vm.$refs.name);
});
});
@@ -140,7 +136,7 @@ describe('BoardForm', () => {
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Create board');
- expect(findModalActionPrimary().attributes[0].variant).toBe('confirm');
+ expect(findModalActionPrimary().attributes.variant).toBe('confirm');
});
it('does not render delete confirmation message', () => {
@@ -209,6 +205,30 @@ describe('BoardForm', () => {
expect(setBoardMock).not.toHaveBeenCalled();
expect(setErrorMock).toHaveBeenCalled();
});
+
+ describe('when Apollo boards FF is on', () => {
+ it('calls a correct GraphQL mutation and emits addBoard event when creating a board', async () => {
+ createComponent(
+ { canAdminBoard: true, currentPage: formType.new },
+ { isApolloBoard: true },
+ );
+ fillForm();
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: createBoardMutation,
+ variables: {
+ input: expect.objectContaining({
+ name: 'test',
+ }),
+ },
+ });
+
+ await waitForPromises();
+ expect(wrapper.emitted('addBoard')).toHaveLength(1);
+ });
+ });
});
});
@@ -228,7 +248,7 @@ describe('BoardForm', () => {
it('passes correct primary action text and variant', () => {
expect(findModalActionPrimary().text).toBe('Save changes');
- expect(findModalActionPrimary().attributes[0].variant).toBe('confirm');
+ expect(findModalActionPrimary().attributes.variant).toBe('confirm');
});
it('does not render delete confirmation message', () => {
@@ -308,13 +328,48 @@ describe('BoardForm', () => {
expect(setBoardMock).not.toHaveBeenCalled();
expect(setErrorMock).toHaveBeenCalled();
});
+
+ describe('when Apollo boards FF is on', () => {
+ it('calls a correct GraphQL mutation and emits updateBoard event when updating a board', async () => {
+ mutate = jest.fn().mockResolvedValue({
+ data: {
+ updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
+ },
+ });
+ setWindowLocation('https://test/boards/1');
+
+ createComponent(
+ { canAdminBoard: true, currentPage: formType.edit },
+ { isApolloBoard: true },
+ );
+ findInput().trigger('keyup.enter', { metaKey: true });
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: updateBoardMutation,
+ variables: {
+ input: expect.objectContaining({
+ id: currentBoard.id,
+ }),
+ },
+ });
+
+ await waitForPromises();
+ expect(eventHub.$emit).toHaveBeenCalledTimes(1);
+ expect(eventHub.$emit).toHaveBeenCalledWith('updateBoard', {
+ id: 'gid://gitlab/Board/321',
+ webPath: 'test-path',
+ });
+ });
+ });
});
describe('when deleting a board', () => {
it('passes correct primary action text and variant', () => {
createComponent({ canAdminBoard: true, currentPage: formType.delete });
expect(findModalActionPrimary().text).toBe('Delete');
- expect(findModalActionPrimary().attributes[0].variant).toBe('danger');
+ expect(findModalActionPrimary().attributes.variant).toBe('danger');
});
it('renders delete confirmation message', () => {
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 9e65e900440..d4489b3c535 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -1,12 +1,16 @@
-import { shallowMount } from '@vue/test-utils';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-
-import { boardListQueryResponse, mockLabelList } from 'jest/boards/mock_data';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ boardListQueryResponse,
+ mockLabelList,
+ updateBoardListResponse,
+} from 'jest/boards/mock_data';
import BoardListHeader from '~/boards/components/board_list_header.vue';
+import updateBoardListMutation from '~/boards/graphql/board_list_update.mutation.graphql';
import { ListType } from '~/boards/constants';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
@@ -20,10 +24,10 @@ describe('Board List Header Component', () => {
const updateListSpy = jest.fn();
const toggleListCollapsedSpy = jest.fn();
+ const mockClientToggleListCollapsedResolver = jest.fn();
+ const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse);
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
fakeApollo = null;
localStorage.clear();
@@ -37,7 +41,7 @@ describe('Board List Header Component', () => {
listQueryHandler = jest.fn().mockResolvedValue(boardListQueryResponse()),
injectedProps = {},
} = {}) => {
- const boardId = '1';
+ const boardId = 'gid://gitlab/Board/1';
const listMock = {
...mockLabelList,
@@ -61,34 +65,46 @@ describe('Board List Header Component', () => {
state: {},
actions: { updateList: updateListSpy, toggleListCollapsed: toggleListCollapsedSpy },
});
-
- fakeApollo = createMockApollo([[listQuery, listQueryHandler]]);
-
- wrapper = extendedWrapper(
- shallowMount(BoardListHeader, {
- apolloProvider: fakeApollo,
- store,
- propsData: {
- list: listMock,
- },
- provide: {
- boardId,
- weightFeatureAvailable: false,
- currentUserId,
- isEpicBoard: false,
- disabled: false,
- ...injectedProps,
+ fakeApollo = createMockApollo(
+ [
+ [listQuery, listQueryHandler],
+ [updateBoardListMutation, updateListHandler],
+ ],
+ {
+ Mutation: {
+ clientToggleListCollapsed: mockClientToggleListCollapsedResolver,
},
- }),
+ },
);
+
+ wrapper = shallowMountExtended(BoardListHeader, {
+ apolloProvider: fakeApollo,
+ store,
+ propsData: {
+ list: listMock,
+ filterParams: {},
+ boardId,
+ },
+ provide: {
+ weightFeatureAvailable: false,
+ currentUserId,
+ isEpicBoard: false,
+ disabled: false,
+ ...injectedProps,
+ },
+ stubs: {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ },
+ });
};
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const isCollapsed = () => wrapper.vm.list.collapsed;
-
- const findAddIssueButton = () => wrapper.findComponent({ ref: 'newIssueBtn' });
const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.findByTestId('board-title-caret');
- const findSettingsButton = () => wrapper.findComponent({ ref: 'settingsBtn' });
+ const findNewIssueButton = () => wrapper.findByTestId('newIssueBtn');
+ const findSettingsButton = () => wrapper.findByTestId('settingsBtn');
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
@@ -100,64 +116,54 @@ describe('Board List Header Component', () => {
ListType.assignee,
];
- it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
+ it.each(hasNoAddButton)('does not render dropdown when List Type is `%s`', (listType) => {
createComponent({ listType });
- expect(findAddIssueButton().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
it.each(hasAddButton)('does render when List Type is `%s`', (listType) => {
createComponent({ listType });
- expect(findAddIssueButton().exists()).toBe(true);
+ expect(findDropdown().exists()).toBe(true);
+ expect(findNewIssueButton().exists()).toBe(true);
});
- it('has a test for each list type', () => {
- createComponent();
-
- Object.values(ListType).forEach((value) => {
- expect([...hasAddButton, ...hasNoAddButton]).toContain(value);
- });
- });
-
- it('does not render when logged out', () => {
+ it('does not render dropdown when logged out', () => {
createComponent({
currentUserId: null,
});
- expect(findAddIssueButton().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
});
describe('Settings Button', () => {
- describe('with disabled=true', () => {
- const hasSettings = [
- ListType.assignee,
- ListType.milestone,
- ListType.iteration,
- ListType.label,
- ];
- const hasNoSettings = [ListType.backlog, ListType.closed];
-
- it.each(hasSettings)('does render for List Type `%s` when disabled=true', (listType) => {
- createComponent({ listType, injectedProps: { disabled: true } });
-
- expect(findSettingsButton().exists()).toBe(true);
- });
+ const hasSettings = [ListType.assignee, ListType.milestone, ListType.iteration, ListType.label];
- it.each(hasNoSettings)(
- 'does not render for List Type `%s` when disabled=true',
- (listType) => {
- createComponent({ listType });
+ it.each(hasSettings)('does render for List Type `%s`', (listType) => {
+ createComponent({ listType });
- expect(findSettingsButton().exists()).toBe(false);
- },
- );
+ expect(findDropdown().exists()).toBe(true);
+ expect(findSettingsButton().exists()).toBe(true);
+ });
+
+ it('does not render dropdown when ListType `closed`', () => {
+ createComponent({ listType: ListType.closed });
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('renders dropdown but not the Settings button when ListType `backlog`', () => {
+ createComponent({ listType: ListType.backlog });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findSettingsButton().exists()).toBe(false);
});
});
describe('expanding / collapsing the column', () => {
- it('should display collapse icon when column is expanded', async () => {
+ it('should display collapse icon when column is expanded', () => {
createComponent();
const icon = findCaret();
@@ -165,7 +171,7 @@ describe('Board List Header Component', () => {
expect(icon.props('icon')).toBe('chevron-lg-down');
});
- it('should display expand icon when column is collapsed', async () => {
+ it('should display expand icon when column is collapsed', () => {
createComponent({ collapsed: true });
const icon = findCaret();
@@ -201,7 +207,9 @@ describe('Board List Header Component', () => {
await nextTick();
expect(updateListSpy).not.toHaveBeenCalled();
- expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(String(isCollapsed()));
+ expect(localStorage.getItem(`${wrapper.vm.uniqueKey}.collapsed`)).toBe(
+ String(!isCollapsed()),
+ );
});
});
@@ -224,4 +232,44 @@ describe('Board List Header Component', () => {
expect(findTitle().classes()).toContain('gl-cursor-grab');
});
});
+
+ describe('Apollo boards', () => {
+ beforeEach(async () => {
+ createComponent({ listType: ListType.label, injectedProps: { isApolloBoard: true } });
+ await nextTick();
+ });
+
+ it('set active board item on client when clicking on card', async () => {
+ findCaret().vm.$emit('click');
+ await nextTick();
+
+ expect(mockClientToggleListCollapsedResolver).toHaveBeenCalledWith(
+ {},
+ {
+ list: mockLabelList,
+ collapsed: true,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
+ it('does not call update list mutation when user is not logged in', async () => {
+ createComponent({ currentUserId: null, injectedProps: { isApolloBoard: true } });
+
+ findCaret().vm.$emit('click');
+ await nextTick();
+
+ expect(updateListHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls update list mutation when user is logged in', async () => {
+ createComponent({ currentUserId: 1, injectedProps: { isApolloBoard: true } });
+
+ findCaret().vm.$emit('click');
+ await nextTick();
+
+ expect(updateListHandler).toHaveBeenCalledWith({ listId: mockLabelList.id, collapsed: true });
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index c3e69ba0e40..651d1daee52 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -51,10 +51,6 @@ describe('Issue boards new issue form', () => {
await nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders board-new-item component', () => {
const boardNewItem = findBoardNewItem();
expect(boardNewItem.exists()).toBe(true);
diff --git a/spec/frontend/boards/components/board_new_item_spec.js b/spec/frontend/boards/components/board_new_item_spec.js
index f4e9901aad2..f11eb2baca7 100644
--- a/spec/frontend/boards/components/board_new_item_spec.js
+++ b/spec/frontend/boards/components/board_new_item_spec.js
@@ -35,10 +35,6 @@ describe('BoardNewItem', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe('when the user provides a valid input', () => {
it('finds an enabled create button', async () => {
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index 7d602042685..b1e14be8ceb 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -2,32 +2,42 @@ import { GlDrawer, GlLabel, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { MountingPortal } from 'portal-vue';
import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import { inactiveId, LIST } from '~/boards/constants';
+import destroyBoardListMutation from '~/boards/graphql/board_list_destroy.mutation.graphql';
import actions from '~/boards/stores/actions';
import getters from '~/boards/stores/getters';
import mutations from '~/boards/stores/mutations';
import sidebarEventHub from '~/sidebar/event_hub';
-import { mockLabelList } from '../mock_data';
+import { mockLabelList, destroyBoardListMutationResponse } from '../mock_data';
+Vue.use(VueApollo);
Vue.use(Vuex);
describe('BoardSettingsSidebar', () => {
let wrapper;
+ let mockApollo;
const labelTitle = mockLabelList.label.title;
const labelColor = mockLabelList.label.color;
const listId = mockLabelList.id;
const modalID = 'board-settings-sidebar-modal';
+ const destroyBoardListMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(destroyBoardListMutationResponse);
+
const createComponent = ({
canAdminList = false,
list = {},
sidebarType = LIST,
activeId = inactiveId,
+ isApolloBoard = false,
} = {}) => {
const boardLists = {
[listId]: list,
@@ -39,16 +49,30 @@ describe('BoardSettingsSidebar', () => {
actions,
});
+ mockApollo = createMockApollo([
+ [destroyBoardListMutation, destroyBoardListMutationHandlerSuccess],
+ ]);
+
wrapper = extendedWrapper(
shallowMount(BoardSettingsSidebar, {
store,
+ apolloProvider: mockApollo,
provide: {
canAdminList,
scopedLabelsAvailable: false,
isIssueBoard: true,
+ boardType: 'group',
+ issuableType: 'issue',
+ isApolloBoard,
+ },
+ propsData: {
+ listId: list.id || '',
+ boardId: 'gid://gitlab/Board/1',
+ list,
+ queryVariables: {},
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
stubs: {
GlDrawer: stubComponent(GlDrawer, {
@@ -57,6 +81,9 @@ describe('BoardSettingsSidebar', () => {
},
}),
);
+
+ // Necessary for cache update
+ mockApollo.clients.defaultClient.cache.updateQuery = jest.fn();
};
const findLabel = () => wrapper.findComponent(GlLabel);
const findDrawer = () => wrapper.findComponent(GlDrawer);
@@ -65,8 +92,6 @@ describe('BoardSettingsSidebar', () => {
afterEach(() => {
jest.restoreAllMocks();
- wrapper.destroy();
- wrapper = null;
});
it('finds a MountingPortal component', () => {
@@ -168,6 +193,21 @@ describe('BoardSettingsSidebar', () => {
expect(findRemoveButton().exists()).toBe(true);
});
+ it('removes the list', () => {
+ createComponent({
+ canAdminList: true,
+ activeId: listId,
+ list: mockLabelList,
+ isApolloBoard: true,
+ });
+
+ findRemoveButton().vm.$emit('click');
+
+ wrapper.findComponent(GlModal).vm.$emit('primary');
+
+ expect(destroyBoardListMutationHandlerSuccess).toHaveBeenCalled();
+ });
+
it('has the correct ID on the button', () => {
createComponent({ canAdminList: true, activeId: listId, list: mockLabelList });
const binding = getBinding(findRemoveButton().element, 'gl-modal');
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
index 8258d9fe7f4..d97a1dbff47 100644
--- a/spec/frontend/boards/components/board_top_bar_spec.js
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -11,7 +11,7 @@ import ConfigToggle from '~/boards/components/config_toggle.vue';
import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import ToggleFocus from '~/boards/components/toggle_focus.vue';
-import { BoardType } from '~/boards/constants';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
@@ -43,8 +43,9 @@ describe('BoardTopBar', () => {
wrapper = shallowMount(BoardTopBar, {
store,
apolloProvider: mockApollo,
- props: {
+ propsData: {
boardId: 'gid://gitlab/Board/1',
+ isSwimlanesOn: false,
},
provide: {
swimlanesFeatureAvailable: false,
@@ -64,7 +65,6 @@ describe('BoardTopBar', () => {
};
afterEach(() => {
- wrapper.destroy();
mockApollo = null;
});
@@ -96,6 +96,11 @@ describe('BoardTopBar', () => {
it('does not render BoardAddNewColumnTrigger component', () => {
expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(false);
});
+
+ it('emits setFilters when setFilters is emitted by filtered search', () => {
+ wrapper.findComponent(IssueBoardFilteredSearch).vm.$emit('setFilters');
+ expect(wrapper.emitted('setFilters')).toHaveLength(1);
+ });
});
describe('when user can admin list', () => {
@@ -111,14 +116,14 @@ describe('BoardTopBar', () => {
describe('Apollo boards', () => {
it.each`
boardType | queryHandler | notCalledHandler
- ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
- ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ ${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
`('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
createComponent({
provide: {
boardType,
- isProjectBoard: boardType === BoardType.project,
- isGroupBoard: boardType === BoardType.group,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
+ isGroupBoard: boardType === WORKSPACE_GROUP,
isApolloBoard: true,
},
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 28f51e0ecbf..13c017706ef 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -5,11 +5,11 @@ import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
-import { BoardType } from '~/boards/constants';
import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql';
import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
@@ -116,7 +116,6 @@ describe('BoardsSelector', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -228,13 +227,13 @@ describe('BoardsSelector', () => {
describe('fetching all boards', () => {
it.each`
boardType | queryHandler | notCalledHandler
- ${BoardType.group} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess}
- ${BoardType.project} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
+ ${WORKSPACE_GROUP} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess}
+ ${WORKSPACE_PROJECT} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
`('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
createStore();
createComponent({
- isGroupBoard: boardType === BoardType.group,
- isProjectBoard: boardType === BoardType.project,
+ isGroupBoard: boardType === WORKSPACE_GROUP,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
});
await nextTick();
@@ -251,7 +250,7 @@ describe('BoardsSelector', () => {
describe('dropdown visibility', () => {
describe('when multipleIssueBoardsAvailable is enabled', () => {
- it('show dropdown', async () => {
+ it('show dropdown', () => {
createStore();
createComponent({ provide: { multipleIssueBoardsAvailable: true } });
expect(findDropdown().exists()).toBe(true);
@@ -259,7 +258,7 @@ describe('BoardsSelector', () => {
});
describe('when multipleIssueBoardsAvailable is disabled but it hasMissingBoards', () => {
- it('show dropdown', async () => {
+ it('show dropdown', () => {
createStore();
createComponent({
provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: true },
@@ -269,7 +268,7 @@ describe('BoardsSelector', () => {
});
describe("when multipleIssueBoardsAvailable is disabled and it dosn't hasMissingBoards", () => {
- it('hide dropdown', async () => {
+ it('hide dropdown', () => {
createStore();
createComponent({
provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: false },
diff --git a/spec/frontend/boards/components/config_toggle_spec.js b/spec/frontend/boards/components/config_toggle_spec.js
index 47d4692453d..5330721451e 100644
--- a/spec/frontend/boards/components/config_toggle_spec.js
+++ b/spec/frontend/boards/components/config_toggle_spec.js
@@ -23,10 +23,6 @@ describe('ConfigToggle', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a button with label `View scope` when `canAdminList` is `false`', () => {
wrapper = createComponent({ canAdminList: false });
expect(findButton().text()).toBe('View scope');
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 57a30ddc512..5b5b68d5dbe 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -2,10 +2,10 @@ import { orderBy } from 'lodash';
import { shallowMount } from '@vue/test-utils';
import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
import IssueBoardFilteredSpec from '~/boards/components/issue_board_filtered_search.vue';
-import issueBoardFilters from '~/boards/issue_board_filters';
+import issueBoardFilters from 'ee_else_ce/boards/issue_board_filters';
import { mockTokens } from '../mock_data';
-jest.mock('~/boards/issue_board_filters');
+jest.mock('ee_else_ce/boards/issue_board_filters');
describe('IssueBoardFilter', () => {
let wrapper;
@@ -14,6 +14,9 @@ describe('IssueBoardFilter', () => {
const createComponent = ({ isSignedIn = false } = {}) => {
wrapper = shallowMount(IssueBoardFilteredSpec, {
+ propsData: {
+ boardId: 'gid://gitlab/Board/1',
+ },
provide: {
isSignedIn,
releasesFetchPath: '/releases',
@@ -35,10 +38,6 @@ describe('IssueBoardFilter', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -48,6 +47,11 @@ describe('IssueBoardFilter', () => {
expect(findBoardsFilteredSearch().exists()).toBe(true);
});
+ it('emits setFilters when setFilters is emitted', () => {
+ findBoardsFilteredSearch().vm.$emit('setFilters');
+ expect(wrapper.emitted('setFilters')).toHaveLength(1);
+ });
+
it.each`
isSignedIn
${true}
diff --git a/spec/frontend/boards/components/issue_due_date_spec.js b/spec/frontend/boards/components/issue_due_date_spec.js
index 45fa10bf03a..dee8febfe4d 100644
--- a/spec/frontend/boards/components/issue_due_date_spec.js
+++ b/spec/frontend/boards/components/issue_due_date_spec.js
@@ -20,10 +20,6 @@ describe('Issue Due Date component', () => {
date = new Date();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render "Today" if the due date is today', () => {
wrapper = createComponent();
diff --git a/spec/frontend/boards/components/issue_time_estimate_spec.js b/spec/frontend/boards/components/issue_time_estimate_spec.js
index 948a7a20f7f..42507ef560b 100644
--- a/spec/frontend/boards/components/issue_time_estimate_spec.js
+++ b/spec/frontend/boards/components/issue_time_estimate_spec.js
@@ -7,10 +7,6 @@ describe('Issue Time Estimate component', () => {
const findIssueTimeEstimate = () => wrapper.find('[data-testid="issue-time-estimate"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when limitToHours is false', () => {
beforeEach(() => {
wrapper = shallowMount(IssueTimeEstimate, {
diff --git a/spec/frontend/boards/components/item_count_spec.js b/spec/frontend/boards/components/item_count_spec.js
index 0c0c7f66933..f2cc8eb1167 100644
--- a/spec/frontend/boards/components/item_count_spec.js
+++ b/spec/frontend/boards/components/item_count_spec.js
@@ -41,10 +41,6 @@ describe('IssueCount', () => {
createComponent({ maxIssueCount, itemsSize });
});
- afterEach(() => {
- vm.destroy();
- });
-
it('contains issueSize in the template', () => {
expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize));
});
@@ -66,10 +62,6 @@ describe('IssueCount', () => {
createComponent({ maxIssueCount, itemsSize });
});
- afterEach(() => {
- vm.destroy();
- });
-
it('contains issueSize in the template', () => {
expect(vm.find('[data-testid="board-items-count"]').text()).toEqual(String(itemsSize));
});
diff --git a/spec/frontend/boards/components/new_board_button_spec.js b/spec/frontend/boards/components/new_board_button_spec.js
index 2bbd3797abf..7ec35d1b796 100644
--- a/spec/frontend/boards/components/new_board_button_spec.js
+++ b/spec/frontend/boards/components/new_board_button_spec.js
@@ -21,12 +21,6 @@ describe('NewBoardButton', () => {
}),
);
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('control variant', () => {
beforeAll(() => {
stubExperiments({ [FEATURE]: 'control' });
diff --git a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
index 5e2222ac3d7..6dbeac3864f 100644
--- a/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_editable_item_spec.js
@@ -21,11 +21,6 @@ describe('boards sidebar remove issue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
it('renders title', () => {
const title = 'Sidebar item title';
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
index e2e4baefad0..b01ee01120e 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_time_tracker_spec.js
@@ -37,11 +37,6 @@ describe('BoardSidebarTimeTracker', () => {
store.state.activeId = '1';
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
timeTrackingLimitToHours | canUpdate
${true} | ${false}
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index bc66a0515aa..1b526e6fbec 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -1,9 +1,17 @@
import { GlAlert, GlFormInput, GlForm, GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
+import issueSetTitleMutation from '~/boards/graphql/issue_set_title.mutation.graphql';
+import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
+import { updateIssueTitleResponse, updateEpicTitleResponse } from '../../mock_data';
+
+Vue.use(VueApollo);
const TEST_TITLE = 'New item title';
const TEST_ISSUE_A = {
@@ -21,26 +29,45 @@ const TEST_ISSUE_B = {
webUrl: 'webUrl',
};
-describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
+describe('BoardSidebarTitle', () => {
let wrapper;
let store;
+ let storeDispatch;
+ let mockApollo;
+
+ const issueSetTitleMutationHandlerSuccess = jest.fn().mockResolvedValue(updateIssueTitleResponse);
+ const updateEpicTitleMutationHandlerSuccess = jest
+ .fn()
+ .mockResolvedValue(updateEpicTitleResponse);
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
store = null;
- wrapper = null;
});
- const createWrapper = (item = TEST_ISSUE_A) => {
+ const createWrapper = ({ item = TEST_ISSUE_A, provide = {} } = {}) => {
store = createStore();
store.state.boardItems = { [item.id]: { ...item } };
store.dispatch('setActiveId', { id: item.id });
+ mockApollo = createMockApollo([
+ [issueSetTitleMutation, issueSetTitleMutationHandlerSuccess],
+ [updateEpicTitleMutation, updateEpicTitleMutationHandlerSuccess],
+ ]);
+ storeDispatch = jest.spyOn(store, 'dispatch');
- wrapper = shallowMount(BoardSidebarTitle, {
+ wrapper = shallowMountExtended(BoardSidebarTitle, {
store,
+ apolloProvider: mockApollo,
provide: {
canUpdate: true,
+ fullPath: 'gitlab-org',
+ issuableType: 'issue',
+ isEpicBoard: false,
+ isApolloBoard: false,
+ ...provide,
+ },
+ propsData: {
+ activeItem: item,
},
stubs: {
'board-editable-item': BoardEditableItem,
@@ -53,9 +80,9 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
const findFormInput = () => wrapper.findComponent(GlFormInput);
const findGlLink = () => wrapper.findComponent(GlLink);
const findEditableItem = () => wrapper.findComponent(BoardEditableItem);
- const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
- const findTitle = () => wrapper.find('[data-testid="item-title"]');
- const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+ const findTitle = () => wrapper.findByTestId('item-title');
+ const findCollapsed = () => wrapper.findByTestId('collapsed-content');
it('renders title and reference', () => {
createWrapper();
@@ -80,39 +107,42 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
beforeEach(async () => {
createWrapper();
- jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE;
- });
findFormInput().vm.$emit('input', TEST_TITLE);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
});
- it('collapses sidebar and renders new title', () => {
+ it('collapses sidebar and renders new title', async () => {
+ await waitForPromises();
expect(findCollapsed().isVisible()).toBe(true);
- expect(findTitle().text()).toContain(TEST_TITLE);
});
it('commits change to the server', () => {
- expect(wrapper.vm.setActiveItemTitle).toHaveBeenCalledWith({
- title: TEST_TITLE,
+ expect(storeDispatch).toHaveBeenCalledWith('setActiveItemTitle', {
projectPath: 'h/b',
+ title: 'New item title',
});
});
+
+ it('renders correct title', async () => {
+ createWrapper({ item: { ...TEST_ISSUE_A, title: TEST_TITLE } });
+ await waitForPromises();
+
+ expect(findTitle().text()).toContain(TEST_TITLE);
+ });
});
describe('when submitting and invalid title', () => {
beforeEach(async () => {
createWrapper();
- jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {});
findFormInput().vm.$emit('input', '');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
});
it('commits change to the server', () => {
- expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
+ expect(storeDispatch).not.toHaveBeenCalled();
});
});
@@ -142,8 +172,8 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
createWrapper();
});
- it('sets title, expands item and shows alert', async () => {
- expect(wrapper.vm.title).toBe(TEST_TITLE);
+ it('sets title, expands item and shows alert', () => {
+ expect(findFormInput().attributes('value')).toBe(TEST_TITLE);
expect(findCollapsed().isVisible()).toBe(false);
expect(findAlert().exists()).toBe(true);
});
@@ -151,18 +181,15 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
describe('when cancel button is clicked', () => {
beforeEach(async () => {
- createWrapper(TEST_ISSUE_B);
+ createWrapper({ item: TEST_ISSUE_B });
- jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE;
- });
findFormInput().vm.$emit('input', TEST_TITLE);
findCancelButton().vm.$emit('click');
await nextTick();
});
it('collapses sidebar and render former title', () => {
- expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
+ expect(storeDispatch).not.toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
});
@@ -170,12 +197,8 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
describe('when the mutation fails', () => {
beforeEach(async () => {
- createWrapper(TEST_ISSUE_B);
+ createWrapper({ item: TEST_ISSUE_B });
- jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
- throw new Error(['failed mutation']);
- });
- jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findFormInput().vm.$emit('input', 'Invalid title');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await nextTick();
@@ -184,7 +207,38 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
it('collapses sidebar and renders former item title', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
- expect(wrapper.vm.setError).toHaveBeenCalled();
+ expect(storeDispatch).toHaveBeenCalledWith(
+ 'setError',
+ expect.objectContaining({ message: 'An error occurred when updating the title' }),
+ );
});
});
+
+ describe('Apollo boards', () => {
+ it.each`
+ issuableType | isEpicBoard | queryHandler | notCalledHandler
+ ${'issue'} | ${false} | ${issueSetTitleMutationHandlerSuccess} | ${updateEpicTitleMutationHandlerSuccess}
+ ${'epic'} | ${true} | ${updateEpicTitleMutationHandlerSuccess} | ${issueSetTitleMutationHandlerSuccess}
+ `(
+ 'updates $issuableType title',
+ async ({ issuableType, isEpicBoard, queryHandler, notCalledHandler }) => {
+ createWrapper({
+ provide: {
+ issuableType,
+ isEpicBoard,
+ isApolloBoard: true,
+ },
+ });
+
+ await nextTick();
+
+ findFormInput().vm.$emit('input', TEST_TITLE);
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ },
+ );
+ });
});
diff --git a/spec/frontend/boards/components/toggle_focus_spec.js b/spec/frontend/boards/components/toggle_focus_spec.js
index 3cbaac91f8d..cad287954d7 100644
--- a/spec/frontend/boards/components/toggle_focus_spec.js
+++ b/spec/frontend/boards/components/toggle_focus_spec.js
@@ -10,7 +10,7 @@ describe('ToggleFocus', () => {
const createComponent = () => {
wrapper = shallowMountExtended(ToggleFocus, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
attachTo: document.body,
});
@@ -18,10 +18,6 @@ describe('ToggleFocus', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a button with `maximize` icon', () => {
createComponent();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 1d011eacf1c..ec3ae27b6a1 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,7 +1,6 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import { ListType } from '~/boards/constants';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
OPERATORS_IS,
OPERATORS_IS_NOT,
@@ -277,6 +276,9 @@ export const labels = [
},
];
+export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test';
+export const mockEpicFullPath = 'gitlab-org/test-subgroup';
+
export const rawIssue = {
title: 'Issue 1',
id: 'gid://gitlab/Issue/436',
@@ -302,12 +304,24 @@ export const rawIssue = {
epic: {
id: 'gid://gitlab/Epic/41',
},
+ totalTimeSpent: 0,
+ humanTimeEstimate: null,
+ humanTotalTimeSpent: null,
+ emailsDisabled: false,
+ hidden: false,
+ webUrl: `${mockIssueFullPath}/-/issue/27`,
+ relativePosition: null,
+ severity: null,
+ milestone: null,
+ weight: null,
+ blocked: false,
+ blockedByCount: 0,
+ iteration: null,
+ healthStatus: null,
type: 'ISSUE',
+ __typename: 'Issue',
};
-export const mockIssueFullPath = 'gitlab-org/test-subgroup/gitlab-test';
-export const mockEpicFullPath = 'gitlab-org/test-subgroup';
-
export const mockIssue = {
id: 'gid://gitlab/Issue/436',
iid: '27',
@@ -329,7 +343,22 @@ export const mockIssue = {
epic: {
id: 'gid://gitlab/Epic/41',
},
+ totalTimeSpent: 0,
+ humanTimeEstimate: null,
+ humanTotalTimeSpent: null,
+ emailsDisabled: false,
+ hidden: false,
+ webUrl: `${mockIssueFullPath}/-/issue/27`,
+ relativePosition: null,
+ severity: null,
+ milestone: null,
+ weight: null,
+ blocked: false,
+ blockedByCount: 0,
+ iteration: null,
+ healthStatus: null,
type: 'ISSUE',
+ __typename: 'Issue',
};
export const mockEpic = {
@@ -425,45 +454,59 @@ export const mockIssue4 = {
epic: null,
};
-export const mockIssues = [mockIssue, mockIssue2];
+export const mockIssue5 = {
+ id: 'gid://gitlab/Issue/440',
+ iid: 40,
+ title: 'Issue 5',
+ referencePath: '#40',
+ dueDate: null,
+ timeEstimate: 0,
+ confidential: false,
+ path: '/gitlab-org/gitlab-test/-/issues/40',
+ assignees,
+ labels,
+ epic: null,
+};
-export const BoardsMockData = {
- GET: {
- '/test/-/boards/1/lists/300/issues?id=300&page=1': {
- issues: [
- {
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
- },
- ],
- },
- '/test/issue-boards/-/milestones.json': [
- {
- id: 1,
- title: 'test',
- },
- ],
- },
- POST: {
- '/test/-/boards/1/lists': listObj,
- },
- PUT: {
- '/test/issue-boards/-/board/1/lists{/id}': {},
- },
- DELETE: {
- '/test/issue-boards/-/board/1/lists{/id}': {},
- },
+export const mockIssue6 = {
+ id: 'gid://gitlab/Issue/441',
+ iid: 41,
+ title: 'Issue 6',
+ referencePath: '#41',
+ dueDate: null,
+ timeEstimate: 0,
+ confidential: false,
+ path: '/gitlab-org/gitlab-test/-/issues/41',
+ assignees,
+ labels,
+ epic: null,
};
-export const boardsMockInterceptor = (config) => {
- const body = BoardsMockData[config.method.toUpperCase()][config.url];
- return [HTTP_STATUS_OK, body];
+export const mockIssue7 = {
+ id: 'gid://gitlab/Issue/442',
+ iid: 42,
+ title: 'Issue 6',
+ referencePath: '#42',
+ dueDate: null,
+ timeEstimate: 0,
+ confidential: false,
+ path: '/gitlab-org/gitlab-test/-/issues/42',
+ assignees,
+ labels,
+ epic: null,
};
+export const mockIssues = [mockIssue, mockIssue2];
+export const mockIssuesMore = [
+ mockIssue,
+ mockIssue2,
+ mockIssue3,
+ mockIssue4,
+ mockIssue5,
+ mockIssue6,
+ mockIssue7,
+];
+
export const mockList = {
id: 'gid://gitlab/List/1',
title: 'Open',
@@ -477,6 +520,9 @@ export const mockList = {
loading: false,
issuesCount: 1,
maxIssueCount: 0,
+ metadata: {
+ epicsCount: 1,
+ },
__typename: 'BoardList',
};
@@ -552,23 +598,6 @@ export const issues = {
[mockIssue4.id]: mockIssue4,
};
-// The response from group project REST API
-export const mockRawGroupProjects = [
- {
- id: 0,
- name: 'Example Project',
- name_with_namespace: 'Awesome Group / Example Project',
- path_with_namespace: 'awesome-group/example-project',
- },
- {
- id: 1,
- name: 'Foobar Project',
- name_with_namespace: 'Awesome Group / Foobar Project',
- path_with_namespace: 'awesome-group/foobar-project',
- },
-];
-
-// The response from GraphQL endpoint
export const mockGroupProject1 = {
id: 0,
name: 'Example Project',
@@ -898,6 +927,22 @@ export const boardListsQueryResponse = {
},
};
+export const issueBoardListsQueryResponse = {
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/1',
+ board: {
+ id: 'gid://gitlab/Board/1',
+ hideBacklogList: false,
+ lists: {
+ nodes: [mockLabelList],
+ },
+ },
+ __typename: 'Group',
+ },
+ },
+};
+
export const boardListQueryResponse = (issuesCount = 20) => ({
data: {
boardList: {
@@ -915,10 +960,50 @@ export const epicBoardListQueryResponse = (totalWeight = 5) => ({
__typename: 'EpicList',
id: 'gid://gitlab/Boards::EpicList/3',
metadata: {
+ epicsCount: 1,
totalWeight,
},
},
},
});
+export const updateIssueTitleResponse = {
+ data: {
+ updateIssuableTitle: {
+ issue: {
+ id: 'gid://gitlab/Issue/436',
+ title: 'Issue 1 edit',
+ },
+ },
+ },
+};
+
+export const updateEpicTitleResponse = {
+ data: {
+ updateIssuableTitle: {
+ epic: {
+ id: 'gid://gitlab/Epic/426',
+ title: 'Epic 1 edit',
+ },
+ },
+ },
+};
+
+export const updateBoardListResponse = {
+ data: {
+ updateBoardList: {
+ list: mockList,
+ },
+ },
+};
+
+export const destroyBoardListMutationResponse = {
+ data: {
+ destroyBoardList: {
+ errors: [],
+ __typename: 'DestroyBoardListPayload',
+ },
+ },
+};
+
export const DEFAULT_COLOR = '#1068bf';
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index 4324e7068e0..74ce4b6b786 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -71,11 +71,6 @@ describe('ProjectSelect component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays a header title', () => {
createWrapper();
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index ab959abaa99..b8d3be28ca6 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -2,13 +2,7 @@ import * as Sentry from '@sentry/browser';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import Vuex from 'vuex';
-import {
- inactiveId,
- ISSUABLE,
- ListType,
- BoardType,
- DraggableItemTypes,
-} from 'ee_else_ce/boards/constants';
+import { inactiveId, ISSUABLE, ListType, DraggableItemTypes } from 'ee_else_ce/boards/constants';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import testAction from 'helpers/vuex_action_helper';
import {
@@ -26,7 +20,7 @@ import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
@@ -49,7 +43,7 @@ import {
mockMilestones,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
// We need this helper to make sure projectPath is including
// subgroups when the movIssue action is called.
@@ -300,8 +294,8 @@ describe('fetchLists', () => {
it.each`
issuableType | boardType | fullBoardId | isGroup | isProject
- ${TYPE_ISSUE} | ${BoardType.group} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
- ${TYPE_ISSUE} | ${BoardType.project} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
+ ${TYPE_ISSUE} | ${WORKSPACE_GROUP} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
+ ${TYPE_ISSUE} | ${WORKSPACE_PROJECT} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
`(
'calls $issuableType query with correct variables',
async ({ issuableType, boardType, fullBoardId, isGroup, isProject }) => {
@@ -336,7 +330,7 @@ describe('fetchLists', () => {
describe('fetchMilestones', () => {
const queryResponse = {
data: {
- project: {
+ workspace: {
milestones: {
nodes: mockMilestones,
},
@@ -346,7 +340,7 @@ describe('fetchMilestones', () => {
const queryErrors = {
data: {
- project: {
+ workspace: {
errors: ['You cannot view these milestones'],
milestones: {},
},
@@ -407,7 +401,7 @@ describe('fetchMilestones', () => {
},
);
- it('sets milestonesLoading to true', async () => {
+ it('sets milestonesLoading to true', () => {
jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
const store = createStore();
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index c86a256bd96..944a7493504 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -31,10 +31,6 @@ describe('Boards - Getters', () => {
});
describe('isSwimlanesOn', () => {
- afterEach(() => {
- window.gon = { features: {} };
- });
-
it('returns false', () => {
expect(getters.isSwimlanesOn()).toBe(false);
});
@@ -171,10 +167,6 @@ describe('Boards - Getters', () => {
});
describe('isEpicBoard', () => {
- afterEach(() => {
- window.gon = { features: {} };
- });
-
it('returns false', () => {
expect(getters.isEpicBoard()).toBe(false);
});
diff --git a/spec/frontend/bootstrap_linked_tabs_spec.js b/spec/frontend/bootstrap_linked_tabs_spec.js
index 5ee1ca32141..f900cd9da3b 100644
--- a/spec/frontend/bootstrap_linked_tabs_spec.js
+++ b/spec/frontend/bootstrap_linked_tabs_spec.js
@@ -1,9 +1,10 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlLinkedTabs from 'test_fixtures_static/linked_tabs.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
describe('Linked Tabs', () => {
beforeEach(() => {
- loadHTMLFixture('static/linked_tabs.html');
+ setHTMLFixture(htmlLinkedTabs);
});
afterEach(() => {
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 6aab3b51806..300b6f4a39a 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
@@ -2,25 +2,34 @@
exports[`Delete merged branches component Delete merged branches confirmation modal matches snapshot 1`] = `
<div>
- <b-button-stub
- class="gl-mr-3 gl-button btn-danger-secondary"
- data-qa-selector="delete_merged_branches_button"
- size="md"
- tag="button"
- type="button"
- variant="danger"
+ <gl-base-dropdown-stub
+ category="tertiary"
+ class="gl-disclosure-dropdown"
+ data-qa-selector="delete_merged_branches_dropdown_button"
+ icon="ellipsis_v"
+ nocaret="true"
+ placement="right"
+ popperoptions="[object Object]"
+ size="medium"
+ textsronly="true"
+ toggleid="dropdown-toggle-btn-25"
+ toggletext=""
+ variant="default"
>
- <!---->
-
- <!---->
- <span
- class="gl-button-text"
+ <ul
+ aria-labelledby="dropdown-toggle-btn-25"
+ class="gl-new-dropdown-contents"
+ data-testid="disclosure-content"
+ id="disclosure-26"
+ tabindex="-1"
>
- Delete merged branches
-
- </span>
- </b-button-stub>
+ <gl-disclosure-dropdown-item-stub
+ item="[object Object]"
+ />
+ </ul>
+
+ </gl-base-dropdown-stub>
<div>
<form
diff --git a/spec/frontend/branches/components/delete_branch_button_spec.js b/spec/frontend/branches/components/delete_branch_button_spec.js
index b029f34c3d7..5b2ec443c59 100644
--- a/spec/frontend/branches/components/delete_branch_button_spec.js
+++ b/spec/frontend/branches/components/delete_branch_button_spec.js
@@ -25,10 +25,6 @@ describe('Delete branch button', () => {
eventHubSpy = jest.spyOn(eventHub, '$emit');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the button with default tooltip, style, and icon', () => {
createComponent();
diff --git a/spec/frontend/branches/components/delete_branch_modal_spec.js b/spec/frontend/branches/components/delete_branch_modal_spec.js
index c977868ca93..7851d86466f 100644
--- a/spec/frontend/branches/components/delete_branch_modal_spec.js
+++ b/spec/frontend/branches/components/delete_branch_modal_spec.js
@@ -7,6 +7,8 @@ import DeleteBranchModal from '~/branches/components/delete_branch_modal.vue';
import eventHub from '~/branches/event_hub';
let wrapper;
+let showMock;
+let hideMock;
const branchName = 'test_modal';
const defaultBranchName = 'default';
@@ -14,23 +16,20 @@ const deletePath = '/path/to/branch';
const merged = false;
const isProtectedBranch = false;
-const createComponent = (data = {}) => {
+const createComponent = () => {
+ showMock = jest.fn();
+ hideMock = jest.fn();
+
wrapper = extendedWrapper(
shallowMount(DeleteBranchModal, {
- data() {
- return {
- branchName,
- deletePath,
- defaultBranchName,
- merged,
- isProtectedBranch,
- ...data,
- };
- },
stubs: {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ methods: {
+ show: showMock,
+ hide: hideMock,
+ },
}),
GlButton,
GlFormInput,
@@ -46,14 +45,29 @@ const findDeleteButton = () => wrapper.findByTestId('delete-branch-confirmation-
const findCancelButton = () => wrapper.findByTestId('delete-branch-cancel-button');
const findFormInput = () => wrapper.findComponent(GlFormInput);
const findForm = () => wrapper.find('form');
-const submitFormSpy = () => jest.spyOn(wrapper.vm.$refs.form, 'submit');
+const createSubmitFormSpy = () => jest.spyOn(findForm().element, 'submit');
+
+const emitOpenModal = (data = {}) =>
+ eventHub.$emit('openModal', {
+ isProtectedBranch,
+ branchName,
+ defaultBranchName,
+ deletePath,
+ merged,
+ ...data,
+ });
describe('Delete branch modal', () => {
const expectedUnmergedWarning =
"This branch hasn't been merged into default. To avoid data loss, consider merging this branch before deleting it.";
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ createComponent();
+
+ emitOpenModal();
+
+ showMock.mockClear();
+ hideMock.mockClear();
});
describe('Deleting a regular branch', () => {
@@ -61,10 +75,6 @@ describe('Delete branch modal', () => {
const expectedWarning = "You're about to permanently delete the branch test_modal.";
const expectedMessage = `${expectedWarning} ${expectedUnmergedWarning}`;
- beforeEach(() => {
- createComponent();
- });
-
it('renders the modal correctly', () => {
expect(findModal().props('title')).toBe(expectedTitle);
expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessage);
@@ -74,32 +84,30 @@ describe('Delete branch modal', () => {
});
it('submits the form when the delete button is clicked', () => {
+ const submitSpy = createSubmitFormSpy();
+
+ expect(submitSpy).not.toHaveBeenCalled();
+
findDeleteButton().trigger('click');
expect(findForm().attributes('action')).toBe(deletePath);
- expect(submitFormSpy()).toHaveBeenCalled();
+ expect(submitSpy).toHaveBeenCalled();
});
- it('calls show on the modal when a `openModal` event is received through the event hub', async () => {
- const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show');
+ it('calls show on the modal when a `openModal` event is received through the event hub', () => {
+ expect(showMock).not.toHaveBeenCalled();
- eventHub.$emit('openModal', {
- isProtectedBranch,
- branchName,
- defaultBranchName,
- deletePath,
- merged,
- });
+ emitOpenModal();
- expect(showSpy).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalled();
});
it('calls hide on the modal when cancel button is clicked', () => {
- const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
+ expect(hideMock).not.toHaveBeenCalled();
findCancelButton().trigger('click');
- expect(closeModalSpy).toHaveBeenCalled();
+ expect(hideMock).toHaveBeenCalled();
});
});
@@ -112,7 +120,9 @@ describe('Delete branch modal', () => {
'After you confirm and select Yes, delete protected branch, you cannot recover this branch. Please type the following to confirm: test_modal';
beforeEach(() => {
- createComponent({ isProtectedBranch: true });
+ emitOpenModal({
+ isProtectedBranch: true,
+ });
});
describe('rendering the modal correctly for a protected branch', () => {
@@ -142,8 +152,11 @@ describe('Delete branch modal', () => {
await waitForPromises();
+ const submitSpy = createSubmitFormSpy();
+
findDeleteButton().trigger('click');
- expect(submitFormSpy()).not.toHaveBeenCalled();
+
+ expect(submitSpy).not.toHaveBeenCalled();
});
it('opens with the delete button disabled and enables it when branch name is confirmed and fires submit', async () => {
@@ -155,16 +168,23 @@ describe('Delete branch modal', () => {
expect(findDeleteButton().props('disabled')).not.toBe(true);
+ const submitSpy = createSubmitFormSpy();
+
+ expect(submitSpy).not.toHaveBeenCalled();
+
findDeleteButton().trigger('click');
- expect(submitFormSpy()).toHaveBeenCalled();
+
+ expect(submitSpy).toHaveBeenCalled();
});
});
describe('Deleting a merged branch', () => {
- it('does not include the unmerged branch warning when merged is true', () => {
- createComponent({ merged: true });
+ beforeEach(() => {
+ emitOpenModal({ merged: true });
+ });
- expect(findModalMessage().html()).not.toContain(expectedUnmergedWarning);
+ it('does not include the unmerged branch warning when merged is true', () => {
+ expect(findModalMessage().text()).not.toContain(expectedUnmergedWarning);
});
});
});
diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js
index 4f1e772f4a4..4d8b887efd3 100644
--- a/spec/frontend/branches/components/delete_merged_branches_spec.js
+++ b/spec/frontend/branches/components/delete_merged_branches_spec.js
@@ -1,21 +1,27 @@
-import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlButton, GlFormInput, GlModal, GlSprintf } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import DeleteMergedBranches, { i18n } from '~/branches/components/delete_merged_branches.vue';
import { formPath, propsDataMock } from '../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
let wrapper;
+const modalShowSpy = jest.fn();
+const modalHideSpy = jest.fn();
const stubsData = {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ methods: {
+ show: modalShowSpy,
+ hide: modalHideSpy,
+ },
}),
+ GlDisclosureDropdown,
GlButton,
GlFormInput,
GlSprintf,
@@ -26,14 +32,12 @@ const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
propsData: {
...propsDataMock,
},
- directives: {
- GlTooltip: createMockDirective(),
- },
stubs,
});
};
-const findDeleteButton = () => wrapper.findComponent(GlButton);
+const findDeleteButton = () =>
+ wrapper.findComponent('[data-qa-selector="delete_merged_branches_button"]');
const findModal = () => wrapper.findComponent(GlModal);
const findConfirmationButton = () =>
wrapper.findByTestId('delete-merged-branches-confirmation-button');
@@ -48,28 +52,16 @@ describe('Delete merged branches component', () => {
});
describe('Delete merged branches button', () => {
- it('has correct attributes, text and tooltip', () => {
- expect(findDeleteButton().attributes()).toMatchObject({
- category: 'secondary',
- variant: 'danger',
- });
-
+ it('has correct text', () => {
+ createComponent(mount, stubsData);
expect(findDeleteButton().text()).toBe(i18n.deleteButtonText);
});
- it('displays a tooltip', () => {
- const tooltip = getBinding(findDeleteButton().element, 'gl-tooltip');
-
- expect(tooltip).toBeDefined();
- expect(tooltip.value).toBe(wrapper.vm.buttonTooltipText);
- });
-
it('opens modal when clicked', () => {
- createComponent(mount);
- jest.spyOn(wrapper.vm.$refs.modal, 'show');
+ createComponent(mount, stubsData);
findDeleteButton().trigger('click');
- expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ expect(modalShowSpy).toHaveBeenCalled();
});
});
@@ -78,10 +70,6 @@ describe('Delete merged branches component', () => {
createComponent(shallowMountExtended, stubsData);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders correct modal title and text', () => {
const modalText = findModal().text();
expect(findModal().props('title')).toBe(i18n.modalTitle);
@@ -129,15 +117,13 @@ describe('Delete merged branches component', () => {
it('submits form when correct amount is provided and the confirm button is clicked', async () => {
findFormInput().vm.$emit('input', 'delete');
await waitForPromises();
- expect(findDeleteButton().props('disabled')).not.toBe(true);
findConfirmationButton().trigger('click');
expect(submitFormSpy()).toHaveBeenCalled();
});
it('calls hide on the modal when cancel button is clicked', () => {
- const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
findCancelButton().trigger('click');
- expect(closeModalSpy).toHaveBeenCalled();
+ expect(modalHideSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/branches/components/divergence_graph_spec.js b/spec/frontend/branches/components/divergence_graph_spec.js
index 9429a6e982c..66193c2ebf0 100644
--- a/spec/frontend/branches/components/divergence_graph_spec.js
+++ b/spec/frontend/branches/components/divergence_graph_spec.js
@@ -9,10 +9,6 @@ function factory(propsData = {}) {
}
describe('Branch divergence graph component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it('renders ahead and behind count', () => {
factory({
defaultBranch: 'main',
diff --git a/spec/frontend/branches/components/graph_bar_spec.js b/spec/frontend/branches/components/graph_bar_spec.js
index 61c051b49c6..585b376081b 100644
--- a/spec/frontend/branches/components/graph_bar_spec.js
+++ b/spec/frontend/branches/components/graph_bar_spec.js
@@ -8,10 +8,6 @@ function factory(propsData = {}) {
}
describe('Branch divergence graph bar component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it.each`
position | positionClass
${'left'} | ${'position-right-0'}
diff --git a/spec/frontend/branches/components/sort_dropdown_spec.js b/spec/frontend/branches/components/sort_dropdown_spec.js
index bd41b0daaaa..64ef30bb8a8 100644
--- a/spec/frontend/branches/components/sort_dropdown_spec.js
+++ b/spec/frontend/branches/components/sort_dropdown_spec.js
@@ -29,12 +29,6 @@ describe('Branches Sort Dropdown', () => {
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
const findBranchesDropdown = () => wrapper.findByTestId('branches-dropdown');
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('When in overview mode', () => {
beforeEach(() => {
wrapper = createWrapper();
diff --git a/spec/frontend/captcha/captcha_modal_spec.js b/spec/frontend/captcha/captcha_modal_spec.js
index 20e69b5a834..4bbed8ab3bb 100644
--- a/spec/frontend/captcha/captcha_modal_spec.js
+++ b/spec/frontend/captcha/captcha_modal_spec.js
@@ -1,6 +1,5 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
import CaptchaModal from '~/captcha/captcha_modal.vue';
import { initRecaptchaScript } from '~/captcha/init_recaptcha_script';
@@ -9,10 +8,11 @@ jest.mock('~/captcha/init_recaptcha_script');
describe('Captcha Modal', () => {
let wrapper;
- let modal;
let grecaptcha;
const captchaSiteKey = 'abc123';
+ const showSpy = jest.fn();
+ const hideSpy = jest.fn();
function createComponent({ props = {} } = {}) {
wrapper = shallowMount(CaptchaModal, {
@@ -21,11 +21,18 @@ describe('Captcha Modal', () => {
...props,
},
stubs: {
- GlModal: stubComponent(GlModal),
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showSpy,
+ hide: hideSpy,
+ },
+ }),
},
});
}
+ const findGlModal = () => wrapper.findComponent(GlModal);
+
beforeEach(() => {
grecaptcha = {
render: jest.fn(),
@@ -34,38 +41,17 @@ describe('Captcha Modal', () => {
initRecaptchaScript.mockResolvedValue(grecaptcha);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const findGlModal = () => {
- const glModal = wrapper.findComponent(GlModal);
-
- jest.spyOn(glModal.vm, 'show').mockImplementation(() => glModal.vm.$emit('shown'));
- jest
- .spyOn(glModal.vm, 'hide')
- .mockImplementation(() => glModal.vm.$emit('hide', { trigger: '' }));
-
- return glModal;
- };
-
- const showModal = () => {
- wrapper.setProps({ needsCaptchaResponse: true });
- };
-
- beforeEach(() => {
- createComponent();
- modal = findGlModal();
- });
-
describe('rendering', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('renders', () => {
- expect(modal.exists()).toBe(true);
+ expect(findGlModal().exists()).toBe(true);
});
it('assigns the modal a unique ID', () => {
- const firstInstanceModalId = modal.props('modalId');
+ const firstInstanceModalId = findGlModal().props('modalId');
createComponent();
const secondInstanceModalId = findGlModal().props('modalId');
expect(firstInstanceModalId).not.toEqual(secondInstanceModalId);
@@ -75,14 +61,13 @@ describe('Captcha Modal', () => {
describe('functionality', () => {
describe('when modal is shown', () => {
describe('when initRecaptchaScript promise resolves successfully', () => {
- beforeEach(async () => {
- showModal();
-
- await nextTick();
+ beforeEach(() => {
+ createComponent({ props: { needsCaptchaResponse: true } });
+ findGlModal().vm.$emit('shown');
});
- it('shows modal', async () => {
- expect(findGlModal().vm.show).toHaveBeenCalled();
+ it('shows modal', () => {
+ expect(showSpy).toHaveBeenCalled();
});
it('renders window.grecaptcha', () => {
@@ -105,10 +90,10 @@ describe('Captcha Modal', () => {
expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[captchaResponse]]);
});
- it('hides modal with null trigger', async () => {
+ it('hides modal with null trigger', () => {
// Assert that hide is called with zero args, so that we don't trigger the logic
// for hiding the modal via cancel, esc, headerclose, etc, without a captcha response
- expect(modal.vm.hide).toHaveBeenCalledWith();
+ expect(hideSpy).toHaveBeenCalledWith();
});
});
@@ -127,7 +112,7 @@ describe('Captcha Modal', () => {
const bvModalEvent = {
trigger,
};
- modal.vm.$emit('hide', bvModalEvent);
+ findGlModal().vm.$emit('hide', bvModalEvent);
});
it(`emits receivedCaptchaResponse with ${JSON.stringify(expected)}`, () => {
@@ -141,21 +126,24 @@ describe('Captcha Modal', () => {
const fakeError = {};
beforeEach(() => {
- initRecaptchaScript.mockImplementation(() => Promise.reject(fakeError));
+ createComponent({
+ props: { needsCaptchaResponse: true },
+ });
+ initRecaptchaScript.mockImplementation(() => Promise.reject(fakeError));
jest.spyOn(console, 'error').mockImplementation();
- showModal();
+ findGlModal().vm.$emit('shown');
});
it('emits receivedCaptchaResponse exactly once with null', () => {
expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[null]]);
});
- it('hides modal with null trigger', async () => {
+ it('hides modal with null trigger', () => {
// Assert that hide is called with zero args, so that we don't trigger the logic
// for hiding the modal via cancel, esc, headerclose, etc, without a captcha response
- expect(modal.vm.hide).toHaveBeenCalledWith();
+ expect(hideSpy).toHaveBeenCalledWith();
});
it('calls console.error with a message and the exception', () => {
diff --git a/spec/frontend/captcha/init_recaptcha_script_spec.js b/spec/frontend/captcha/init_recaptcha_script_spec.js
index 78480821d95..3e2d7ba00ee 100644
--- a/spec/frontend/captcha/init_recaptcha_script_spec.js
+++ b/spec/frontend/captcha/init_recaptcha_script_spec.js
@@ -50,7 +50,7 @@ describe('initRecaptchaScript', () => {
await expect(result).resolves.toBe(window.grecaptcha);
});
- it('sets window[RECAPTCHA_ONLOAD_CALLBACK_NAME] to undefined', async () => {
+ it('sets window[RECAPTCHA_ONLOAD_CALLBACK_NAME] to undefined', () => {
expect(getScriptOnload()).toBeUndefined();
});
});
diff --git a/spec/frontend/artifacts/components/app_spec.js b/spec/frontend/ci/artifacts/components/app_spec.js
index 931c4703e95..c6874428e2a 100644
--- a/spec/frontend/artifacts/components/app_spec.js
+++ b/spec/frontend/ci/artifacts/components/app_spec.js
@@ -2,27 +2,32 @@ import { GlSkeletonLoader } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import ArtifactsApp from '~/artifacts/components/app.vue';
-import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
-import getBuildArtifactsSizeQuery from '~/artifacts/graphql/queries/get_build_artifacts_size.query.graphql';
+import ArtifactsApp from '~/ci/artifacts/components/app.vue';
+import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
+import getBuildArtifactsSizeQuery from '~/ci/artifacts/graphql/queries/get_build_artifacts_size.query.graphql';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/artifacts/constants';
+import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/ci/artifacts/constants';
const TEST_BUILD_ARTIFACTS_SIZE = 1024;
const TEST_PROJECT_PATH = 'project/path';
const TEST_PROJECT_ID = 'gid://gitlab/Project/22';
-const createBuildArtifactsSizeResponse = (buildArtifactsSize) => ({
+const createBuildArtifactsSizeResponse = ({
+ buildArtifactsSize = TEST_BUILD_ARTIFACTS_SIZE,
+ nullStatistics = false,
+}) => ({
data: {
project: {
__typename: 'Project',
id: TEST_PROJECT_ID,
- statistics: {
- __typename: 'ProjectStatistics',
- buildArtifactsSize,
- },
+ statistics: nullStatistics
+ ? null
+ : {
+ __typename: 'ProjectStatistics',
+ buildArtifactsSize,
+ },
},
},
});
@@ -82,28 +87,32 @@ describe('ArtifactsApp component', () => {
});
describe.each`
- buildArtifactsSize | expectedText
- ${TEST_BUILD_ARTIFACTS_SIZE} | ${numberToHumanSize(TEST_BUILD_ARTIFACTS_SIZE)}
- ${null} | ${SIZE_UNKNOWN}
- `('when buildArtifactsSize is $buildArtifactsSize', ({ buildArtifactsSize, expectedText }) => {
- beforeEach(async () => {
- getBuildArtifactsSizeSpy.mockResolvedValue(
- createBuildArtifactsSizeResponse(buildArtifactsSize),
- );
-
- createComponent();
-
- await waitForPromises();
- });
-
- it('hides loader', () => {
- expect(findSkeletonLoader().exists()).toBe(false);
- });
-
- it('shows the size', () => {
- expect(findBuildArtifactsSize().text()).toMatchInterpolatedText(
- `${TOTAL_ARTIFACTS_SIZE} ${expectedText}`,
- );
- });
- });
+ buildArtifactsSize | nullStatistics | expectedText
+ ${TEST_BUILD_ARTIFACTS_SIZE} | ${false} | ${numberToHumanSize(TEST_BUILD_ARTIFACTS_SIZE)}
+ ${null} | ${false} | ${SIZE_UNKNOWN}
+ ${null} | ${true} | ${SIZE_UNKNOWN}
+ `(
+ 'when buildArtifactsSize is $buildArtifactsSize',
+ ({ buildArtifactsSize, nullStatistics, expectedText }) => {
+ beforeEach(async () => {
+ getBuildArtifactsSizeSpy.mockResolvedValue(
+ createBuildArtifactsSizeResponse({ buildArtifactsSize, nullStatistics }),
+ );
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('hides loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('shows the size', () => {
+ expect(findBuildArtifactsSize().text()).toMatchInterpolatedText(
+ `${TOTAL_ARTIFACTS_SIZE} ${expectedText}`,
+ );
+ });
+ },
+ );
});
diff --git a/spec/frontend/ci/artifacts/components/artifact_row_spec.js b/spec/frontend/ci/artifacts/components/artifact_row_spec.js
new file mode 100644
index 00000000000..96ddedc3a9d
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/artifact_row_spec.js
@@ -0,0 +1,127 @@
+import { GlBadge, GlFriendlyWrap, GlFormCheckbox } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
+import { BULK_DELETE_FEATURE_FLAG, I18N_BULK_DELETE_MAX_SELECTED } from '~/ci/artifacts/constants';
+
+describe('ArtifactRow component', () => {
+ let wrapper;
+
+ const artifact = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0];
+
+ const findName = () => wrapper.findByTestId('job-artifact-row-name');
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findSize = () => wrapper.findByTestId('job-artifact-row-size');
+ const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button');
+ const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+
+ const createComponent = ({ canDestroyArtifacts = true, glFeatures = {}, props = {} } = {}) => {
+ wrapper = shallowMountExtended(ArtifactRow, {
+ propsData: {
+ artifact,
+ isSelected: false,
+ isLoading: false,
+ isLastRow: false,
+ isSelectedArtifactsLimitReached: false,
+ ...props,
+ },
+ provide: { canDestroyArtifacts, glFeatures },
+ stubs: { GlBadge, GlFriendlyWrap },
+ });
+ };
+
+ describe('artifact details', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('displays the artifact name and type', () => {
+ expect(findName().text()).toContain(artifact.name);
+ expect(findBadge().text()).toBe(artifact.fileType.toLowerCase());
+ });
+
+ it('displays the artifact size', () => {
+ expect(findSize().text()).toBe(numberToHumanSize(artifact.size));
+ });
+
+ it('displays the download button as a link to the download path', () => {
+ expect(findDownloadButton().attributes('href')).toBe(artifact.downloadPath);
+ });
+ });
+
+ describe('delete button', () => {
+ it('does not show when user does not have permission', () => {
+ createComponent({ canDestroyArtifacts: false });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ it('shows when user has permission', () => {
+ createComponent();
+
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('emits the delete event when clicked', async () => {
+ createComponent();
+
+ expect(wrapper.emitted('delete')).toBeUndefined();
+
+ findDeleteButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toBeDefined();
+ });
+ });
+
+ describe('bulk delete checkbox', () => {
+ describe('with permission and feature flag enabled', () => {
+ it('emits selectArtifact when toggled', () => {
+ createComponent({ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true } });
+
+ findCheckbox().vm.$emit('input', true);
+
+ expect(wrapper.emitted('selectArtifact')).toStrictEqual([[artifact, true]]);
+ });
+
+ describe('when the selected artifacts limit is reached', () => {
+ it('remains enabled if the artifact was selected', () => {
+ createComponent({
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ props: { isSelected: true, isSelectedArtifactsLimitReached: true },
+ });
+
+ expect(findCheckbox().attributes('disabled')).toBeUndefined();
+ expect(findCheckbox().attributes('title')).toBe('');
+ });
+
+ it('is disabled if the artifact was not selected', () => {
+ createComponent({
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ props: { isSelected: false, isSelectedArtifactsLimitReached: true },
+ });
+
+ expect(findCheckbox().attributes('disabled')).toBeDefined();
+ expect(findCheckbox().attributes('title')).toBe(I18N_BULK_DELETE_MAX_SELECTED);
+ });
+ });
+ });
+
+ it('is not shown without permission', () => {
+ createComponent({ canDestroyArtifacts: false });
+
+ expect(findCheckbox().exists()).toBe(false);
+ });
+
+ it('is not shown with feature flag disabled', () => {
+ createComponent();
+
+ expect(findCheckbox().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js
new file mode 100644
index 00000000000..549f6e1e375
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/artifacts_bulk_delete_spec.js
@@ -0,0 +1,58 @@
+import { GlSprintf, GlAlert } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
+import { I18N_BULK_DELETE_MAX_SELECTED } from '~/ci/artifacts/constants';
+
+describe('ArtifactsBulkDelete component', () => {
+ let wrapper;
+
+ const selectedArtifacts = [
+ mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[0].id,
+ mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes[1].id,
+ ];
+
+ const findText = () => wrapper.findComponent(GlSprintf).text();
+ const findDeleteButton = () => wrapper.findByTestId('bulk-delete-delete-button');
+ const findClearButton = () => wrapper.findByTestId('bulk-delete-clear-button');
+ const findAlertText = () => wrapper.findComponent(GlAlert).text();
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(ArtifactsBulkDelete, {
+ propsData: {
+ selectedArtifacts,
+ isSelectedArtifactsLimitReached: false,
+ ...props,
+ },
+ stubs: { GlSprintf },
+ });
+ };
+
+ describe('selected artifacts box', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('displays selected artifacts count', () => {
+ expect(findText()).toContain(String(selectedArtifacts.length));
+ });
+
+ it('emits showBulkDeleteModal event when the delete button is clicked', () => {
+ findDeleteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('showBulkDeleteModal')).toBeDefined();
+ });
+
+ it('emits clearSelectedArtifacts event when the clear button is clicked', () => {
+ findClearButton().vm.$emit('click');
+
+ expect(wrapper.emitted('clearSelectedArtifacts')).toBeDefined();
+ });
+ });
+
+ it('shows an alert when the selected artifacts limit is reached', () => {
+ createComponent({ isSelectedArtifactsLimitReached: true });
+
+ expect(findAlertText()).toBe(I18N_BULK_DELETE_MAX_SELECTED);
+ });
+});
diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js
index d006e0285d2..479ecf6b183 100644
--- a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
+++ b/spec/frontend/ci/artifacts/components/artifacts_table_row_details_spec.js
@@ -1,18 +1,18 @@
import { GlModal } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
import waitForPromises from 'helpers/wait_for_promises';
-import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
-import ArtifactRow from '~/artifacts/components/artifact_row.vue';
-import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
+import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactRow from '~/ci/artifacts/components/artifact_row.vue';
+import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import destroyArtifactMutation from '~/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
-import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/artifacts/constants';
-import { createAlert } from '~/flash';
+import destroyArtifactMutation from '~/ci/artifacts/graphql/mutations/destroy_artifact.mutation.graphql';
+import { I18N_DESTROY_ERROR, I18N_MODAL_TITLE } from '~/ci/artifacts/constants';
+import { createAlert } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
const { artifacts } = getJobArtifactsResponse.data.project.jobs.nodes[0];
const refetchArtifacts = jest.fn();
@@ -25,11 +25,12 @@ describe('ArtifactsTableRowDetails component', () => {
const findModal = () => wrapper.findComponent(GlModal);
- const createComponent = (
+ const createComponent = ({
handlers = {
destroyArtifactMutation: jest.fn(),
},
- ) => {
+ selectedArtifacts = [],
+ } = {}) => {
requestHandlers = handlers;
wrapper = mountExtended(ArtifactsTableRowDetails, {
apolloProvider: createMockApollo([
@@ -37,8 +38,10 @@ describe('ArtifactsTableRowDetails component', () => {
]),
propsData: {
artifacts,
+ selectedArtifacts,
refetchArtifacts,
queryVariables: {},
+ isSelectedArtifactsLimitReached: false,
},
provide: { canDestroyArtifacts: true },
data() {
@@ -47,10 +50,6 @@ describe('ArtifactsTableRowDetails component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('passes correct props', () => {
beforeEach(() => {
createComponent();
@@ -92,7 +91,7 @@ describe('ArtifactsTableRowDetails component', () => {
});
});
- it('displays a flash message and refetches artifacts when the mutation fails', async () => {
+ it('displays an alert message and refetches artifacts when the mutation fails', async () => {
createComponent({
destroyArtifactMutation: jest.fn().mockRejectedValue(new Error('Error!')),
});
@@ -120,4 +119,20 @@ describe('ArtifactsTableRowDetails component', () => {
expect(requestHandlers.destroyArtifactMutation).not.toHaveBeenCalled();
});
});
+
+ describe('bulk delete selection', () => {
+ it('is not selected for unselected artifact', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ArtifactRow).at(0).props('isSelected')).toBe(false);
+ });
+
+ it('is selected for selected artifacts', async () => {
+ createComponent({ selectedArtifacts: [artifacts.nodes[0].id] });
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ArtifactRow).at(0).props('isSelected')).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/artifacts/components/feedback_banner_spec.js b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
index 3421486020a..53e0fdac6f6 100644
--- a/spec/frontend/artifacts/components/feedback_banner_spec.js
+++ b/spec/frontend/ci/artifacts/components/feedback_banner_spec.js
@@ -1,12 +1,12 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
+import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
import {
I18N_FEEDBACK_BANNER_TITLE,
I18N_FEEDBACK_BANNER_BUTTON,
FEEDBACK_URL,
-} from '~/artifacts/constants';
+} from '~/ci/artifacts/constants';
const mockBannerImagePath = 'banner/image/path';
@@ -32,10 +32,6 @@ describe('Artifacts management feedback banner', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('is displayed with the correct props', () => {
createComponent();
diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
new file mode 100644
index 00000000000..514644a92f2
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -0,0 +1,684 @@
+import {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlBadge,
+ GlPagination,
+ GlModal,
+ GlFormCheckbox,
+} from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
+import FeedbackBanner from '~/ci/artifacts/components/feedback_banner.vue';
+import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
+import ArtifactDeleteModal from '~/ci/artifacts/components/artifact_delete_modal.vue';
+import ArtifactsBulkDelete from '~/ci/artifacts/components/artifacts_bulk_delete.vue';
+import BulkDeleteModal from '~/ci/artifacts/components/bulk_delete_modal.vue';
+import JobCheckbox from '~/ci/artifacts/components/job_checkbox.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import bulkDestroyArtifactsMutation from '~/ci/artifacts/graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
+import {
+ ARCHIVE_FILE_TYPE,
+ JOBS_PER_PAGE,
+ I18N_FETCH_ERROR,
+ INITIAL_CURRENT_PAGE,
+ BULK_DELETE_FEATURE_FLAG,
+ I18N_BULK_DELETE_ERROR,
+ SELECTED_ARTIFACTS_MAX_COUNT,
+} from '~/ci/artifacts/constants';
+import { totalArtifactsSizeForJob } from '~/ci/artifacts/utils';
+import { createAlert } from '~/alert';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('JobArtifactsTable component', () => {
+ let wrapper;
+ let requestHandlers;
+
+ const mockToastShow = jest.fn();
+
+ const findBanner = () => wrapper.findComponent(FeedbackBanner);
+
+ const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails);
+ const findDetailsInRow = (i) =>
+ findTable().findAll('tbody tr').at(i).findComponent(ArtifactsTableRowDetails);
+
+ const findCount = () => wrapper.findByTestId('job-artifacts-count');
+ const findCountAt = (i) => wrapper.findAllByTestId('job-artifacts-count').at(i);
+
+ const findDeleteModal = () => wrapper.findComponent(ArtifactDeleteModal);
+ const findBulkDeleteModal = () => wrapper.findComponent(BulkDeleteModal);
+
+ const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
+ const findSuccessfulJobStatus = () => findStatuses().at(0);
+ const findFailedJobStatus = () => findStatuses().at(1);
+
+ const findLinks = () => wrapper.findAllComponents(GlLink);
+ const findJobLink = () => findLinks().at(0);
+ const findPipelineLink = () => findLinks().at(1);
+ const findRefLink = () => findLinks().at(2);
+ const findCommitLink = () => findLinks().at(3);
+
+ const findSize = () => wrapper.findByTestId('job-artifacts-size');
+ const findCreated = () => wrapper.findByTestId('job-artifacts-created');
+
+ const findDownloadButton = () => wrapper.findByTestId('job-artifacts-download-button');
+ const findBrowseButton = () => wrapper.findByTestId('job-artifacts-browse-button');
+ const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button');
+ const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+
+ // first checkbox is a "select all", this finder should get the first job checkbox
+ const findJobCheckbox = (i = 1) => wrapper.findAllComponents(GlFormCheckbox).at(i);
+ const findAnyCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findBulkDelete = () => wrapper.findComponent(ArtifactsBulkDelete);
+ const findBulkDeleteContainer = () => wrapper.findByTestId('bulk-delete-container');
+
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const setPage = async (page) => {
+ findPagination().vm.$emit('input', page);
+ await waitForPromises();
+ };
+
+ const projectId = 'some/project/id';
+
+ let enoughJobsToPaginate = [...getJobArtifactsResponse.data.project.jobs.nodes];
+ while (enoughJobsToPaginate.length <= JOBS_PER_PAGE) {
+ enoughJobsToPaginate = [
+ ...enoughJobsToPaginate,
+ ...getJobArtifactsResponse.data.project.jobs.nodes,
+ ];
+ }
+ const getJobArtifactsResponseThatPaginates = {
+ data: {
+ project: {
+ jobs: {
+ nodes: enoughJobsToPaginate,
+ pageInfo: { ...getJobArtifactsResponse.data.project.jobs.pageInfo, hasNextPage: true },
+ },
+ },
+ },
+ };
+
+ const job = getJobArtifactsResponse.data.project.jobs.nodes[0];
+ const archiveArtifact = job.artifacts.nodes.find(
+ (artifact) => artifact.fileType === ARCHIVE_FILE_TYPE,
+ );
+ const job2 = getJobArtifactsResponse.data.project.jobs.nodes[1];
+
+ const destroyedCount = job.artifacts.nodes.length;
+ const destroyedIds = job.artifacts.nodes.map((node) => node.id);
+ const bulkDestroyMutationHandler = jest.fn().mockResolvedValue({
+ data: {
+ bulkDestroyJobArtifacts: { errors: [], destroyedCount, destroyedIds },
+ },
+ });
+
+ const maxSelectedArtifacts = new Array(SELECTED_ARTIFACTS_MAX_COUNT).fill({});
+
+ const createComponent = ({
+ handlers = {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: bulkDestroyMutationHandler,
+ },
+ data = {},
+ canDestroyArtifacts = true,
+ glFeatures = {},
+ } = {}) => {
+ requestHandlers = handlers;
+ wrapper = mountExtended(JobArtifactsTable, {
+ apolloProvider: createMockApollo([
+ [getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
+ [bulkDestroyArtifactsMutation, requestHandlers.bulkDestroyArtifactsMutation],
+ ]),
+ provide: {
+ projectPath: 'project/path',
+ projectId,
+ canDestroyArtifacts,
+ artifactsManagementFeedbackImagePath: 'banner/image/path',
+ glFeatures,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ data() {
+ return data;
+ },
+ });
+ };
+
+ it('renders feedback banner', () => {
+ createComponent();
+
+ expect(findBanner().exists()).toBe(true);
+ });
+
+ it('when loading, shows a loading state', () => {
+ createComponent();
+
+ expect(findLoadingState().exists()).toBe(true);
+ });
+
+ it('on error, shows an alert', async () => {
+ createComponent({
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockRejectedValue(new Error('Error!')),
+ },
+ });
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N_FETCH_ERROR });
+ });
+
+ it('with data, renders the table', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findTable().exists()).toBe(true);
+ });
+
+ describe('job details', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('shows the artifact count', () => {
+ expect(findCount().text()).toBe(`${job.artifacts.nodes.length} files`);
+ });
+
+ it('shows the job status as an icon for a successful job', () => {
+ expect(findSuccessfulJobStatus().findComponent(CiIcon).exists()).toBe(true);
+ expect(findSuccessfulJobStatus().findComponent(GlBadge).exists()).toBe(false);
+ });
+
+ it('shows the job status as a badge for other job statuses', () => {
+ expect(findFailedJobStatus().findComponent(GlBadge).exists()).toBe(true);
+ expect(findFailedJobStatus().findComponent(CiIcon).exists()).toBe(false);
+ });
+
+ it('shows links to the job, pipeline, ref, and commit', () => {
+ expect(findJobLink().text()).toBe(job.name);
+ expect(findJobLink().attributes('href')).toBe(job.webPath);
+
+ expect(findPipelineLink().text()).toBe(`#${getIdFromGraphQLId(job.pipeline.id)}`);
+ expect(findPipelineLink().attributes('href')).toBe(job.pipeline.path);
+
+ expect(findRefLink().text()).toBe(job.refName);
+ expect(findRefLink().attributes('href')).toBe(job.refPath);
+
+ expect(findCommitLink().text()).toBe(job.shortSha);
+ expect(findCommitLink().attributes('href')).toBe(job.commitPath);
+ });
+
+ it('shows the total size of artifacts', () => {
+ expect(findSize().text()).toBe(totalArtifactsSizeForJob(job));
+ });
+
+ it('shows the created time', () => {
+ expect(findCreated().text()).toBe('5 years ago');
+ });
+
+ describe('row expansion', () => {
+ it('toggles the visibility of the row details', async () => {
+ expect(findDetailsRows().length).toBe(0);
+
+ findCount().trigger('click');
+ await nextTick();
+
+ expect(findDetailsRows().length).toBe(1);
+
+ findCount().trigger('click');
+ await nextTick();
+
+ expect(findDetailsRows().length).toBe(0);
+ });
+
+ it('expands and collapses jobs', async () => {
+ // both jobs start collapsed
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(false);
+
+ findCountAt(0).trigger('click');
+ await nextTick();
+
+ // first job is expanded, second row has its details
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ expect(findDetailsInRow(2).exists()).toBe(false);
+
+ findCountAt(1).trigger('click');
+ await nextTick();
+
+ // both jobs are expanded, each has details below it
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ expect(findDetailsInRow(2).exists()).toBe(false);
+ expect(findDetailsInRow(3).exists()).toBe(true);
+
+ findCountAt(0).trigger('click');
+ await nextTick();
+
+ // first job collapsed, second job expanded
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(false);
+ expect(findDetailsInRow(2).exists()).toBe(true);
+ });
+
+ it('keeps the job expanded when an artifact is deleted', async () => {
+ findCount().trigger('click');
+ await waitForPromises();
+
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+
+ findArtifactDeleteButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findDeleteModal().findComponent(GlModal).props('visible')).toBe(true);
+
+ findDeleteModal().vm.$emit('primary');
+ await waitForPromises();
+
+ expect(findDetailsInRow(0).exists()).toBe(false);
+ expect(findDetailsInRow(1).exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('download button', () => {
+ it('is a link to the download path for the archive artifact', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDownloadButton().attributes('href')).toBe(archiveArtifact.downloadPath);
+ });
+
+ it('is disabled when there is no download path', async () => {
+ const jobWithoutDownloadPath = {
+ ...job,
+ archive: { downloadPath: null },
+ };
+
+ createComponent({
+ handlers: { getJobArtifactsQuery: jest.fn() },
+ data: { jobArtifacts: [jobWithoutDownloadPath] },
+ });
+
+ await waitForPromises();
+
+ expect(findDownloadButton().attributes('disabled')).toBeDefined();
+ });
+ });
+
+ describe('browse button', () => {
+ it('is a link to the browse path for the job', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findBrowseButton().attributes('href')).toBe(job.browseArtifactsPath);
+ });
+
+ it('is disabled when there is no browse path', async () => {
+ const jobWithoutBrowsePath = {
+ ...job,
+ browseArtifactsPath: null,
+ };
+
+ createComponent({
+ handlers: { getJobArtifactsQuery: jest.fn() },
+ data: { jobArtifacts: [jobWithoutBrowsePath] },
+ });
+
+ await waitForPromises();
+
+ expect(findBrowseButton().attributes('disabled')).toBeDefined();
+ });
+ });
+
+ describe('delete button', () => {
+ const artifactsFromJob = job.artifacts.nodes.map((node) => node.id);
+
+ describe('with delete permission and bulk delete feature flag enabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+ });
+
+ it('opens the confirmation modal with the artifacts from the job', async () => {
+ await findDeleteButton().vm.$emit('click');
+
+ expect(findBulkDeleteModal().props()).toMatchObject({
+ visible: true,
+ artifactsToDelete: artifactsFromJob,
+ });
+ });
+
+ it('on confirm, deletes the artifacts from the job and shows a toast', async () => {
+ findDeleteButton().vm.$emit('click');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: artifactsFromJob,
+ });
+
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ `${artifactsFromJob.length} selected artifacts deleted`,
+ );
+ });
+
+ it('does not clear selected artifacts on success', async () => {
+ // select job 2 via checkbox
+ findJobCheckbox(2).vm.$emit('input', true);
+
+ // click delete button job 1
+ findDeleteButton().vm.$emit('click');
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+
+ // confirm delete
+ findBulkDeleteModal().vm.$emit('primary');
+
+ // job 1's artifacts should be deleted
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: artifactsFromJob,
+ });
+
+ await waitForPromises();
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+ });
+ });
+
+ it('shows an alert and does not clear selected artifacts on error', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: jest.fn().mockRejectedValue(),
+ },
+ });
+ await waitForPromises();
+
+ // select job 2 via checkbox
+ findJobCheckbox(2).vm.$emit('input', true);
+
+ // click delete button job 1
+ findDeleteButton().vm.$emit('click');
+
+ // confirm delete
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ // job 2's artifacts should still be selected
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(
+ job2.artifacts.nodes.map((node) => node.id),
+ );
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Error),
+ message: I18N_BULK_DELETE_ERROR,
+ });
+ });
+
+ it('is disabled when bulk delete feature flag is disabled', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
+
+ await waitForPromises();
+
+ expect(findDeleteButton().attributes('disabled')).toBeDefined();
+ });
+
+ it('is hidden when user does not have delete permission', async () => {
+ createComponent({
+ canDestroyArtifacts: false,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
+
+ await waitForPromises();
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+ });
+
+ describe('bulk delete', () => {
+ const selectedArtifacts = job.artifacts.nodes.map((node) => node.id);
+
+ describe('with permission and feature flag enabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+ });
+
+ it('shows selected artifacts when a job is checked', async () => {
+ expect(findBulkDeleteContainer().exists()).toBe(false);
+
+ await findJobCheckbox().vm.$emit('input', true);
+
+ expect(findBulkDeleteContainer().exists()).toBe(true);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
+ });
+
+ it('disappears when selected artifacts are cleared', async () => {
+ await findJobCheckbox().vm.$emit('input', true);
+
+ expect(findBulkDeleteContainer().exists()).toBe(true);
+
+ await findBulkDelete().vm.$emit('clearSelectedArtifacts');
+
+ expect(findBulkDeleteContainer().exists()).toBe(false);
+ });
+
+ it('shows a modal to confirm bulk delete', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+
+ await nextTick();
+
+ expect(findBulkDeleteModal().props('visible')).toBe(true);
+ });
+
+ it('deletes the selected artifacts and shows a toast', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ expect(bulkDestroyMutationHandler).toHaveBeenCalledWith({
+ projectId: convertToGraphQLId(TYPENAME_PROJECT, projectId),
+ ids: selectedArtifacts,
+ });
+
+ await waitForPromises();
+
+ expect(mockToastShow).toHaveBeenCalledWith(
+ `${selectedArtifacts.length} selected artifacts deleted`,
+ );
+ });
+
+ it('clears selected artifacts on success', async () => {
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
+ });
+ });
+
+ describe('when the selected artifacts limit is reached', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ data: { selectedArtifacts: maxSelectedArtifacts },
+ });
+
+ await nextTick();
+ });
+
+ it('passes isSelectedArtifactsLimitReached to bulk delete', () => {
+ expect(findBulkDelete().props('isSelectedArtifactsLimitReached')).toBe(true);
+ });
+
+ it('passes isSelectedArtifactsLimitReached to job checkbox', () => {
+ expect(wrapper.findComponent(JobCheckbox).props('isSelectedArtifactsLimitReached')).toBe(
+ true,
+ );
+ });
+
+ it('passes isSelectedArtifactsLimitReached to table row details', async () => {
+ findCount().trigger('click');
+ await nextTick();
+
+ expect(findDetailsInRow(1).props('isSelectedArtifactsLimitReached')).toBe(true);
+ });
+ });
+
+ it('shows an alert and does not clear selected artifacts on error', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ handlers: {
+ getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
+ bulkDestroyArtifactsMutation: jest.fn().mockRejectedValue(),
+ },
+ });
+
+ await waitForPromises();
+
+ findJobCheckbox().vm.$emit('input', true);
+ findBulkDelete().vm.$emit('showBulkDeleteModal');
+ findBulkDeleteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Error),
+ message: I18N_BULK_DELETE_ERROR,
+ });
+ });
+
+ it('shows no checkboxes without permission', async () => {
+ createComponent({
+ canDestroyArtifacts: false,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ });
+
+ await waitForPromises();
+
+ expect(findAnyCheckbox().exists()).toBe(false);
+ });
+
+ it('shows no checkboxes with feature flag disabled', async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: false },
+ });
+
+ await waitForPromises();
+
+ expect(findAnyCheckbox().exists()).toBe(false);
+ });
+ });
+
+ describe('pagination', () => {
+ const { pageInfo } = getJobArtifactsResponseThatPaginates.data.project.jobs;
+ const query = jest.fn().mockResolvedValue(getJobArtifactsResponseThatPaginates);
+
+ beforeEach(async () => {
+ createComponent({
+ handlers: {
+ getJobArtifactsQuery: query,
+ },
+ data: { pageInfo },
+ });
+
+ await nextTick();
+ });
+
+ it('renders pagination and passes page props', () => {
+ expect(findPagination().props()).toMatchObject({
+ value: INITIAL_CURRENT_PAGE,
+ prevPage: Number(pageInfo.hasPreviousPage),
+ nextPage: Number(pageInfo.hasNextPage),
+ });
+
+ expect(query).toHaveBeenCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: null,
+ nextPageCursor: '',
+ prevPageCursor: '',
+ });
+ });
+
+ it('updates query variables when going to previous page', async () => {
+ await setPage(1);
+
+ expect(query).toHaveBeenLastCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: null,
+ lastPageSize: JOBS_PER_PAGE,
+ prevPageCursor: pageInfo.startCursor,
+ });
+ expect(findPagination().props('value')).toEqual(1);
+ });
+
+ it('updates query variables when going to next page', async () => {
+ await setPage(2);
+
+ expect(query).toHaveBeenLastCalledWith({
+ projectPath: 'project/path',
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: null,
+ prevPageCursor: '',
+ nextPageCursor: pageInfo.endCursor,
+ });
+ expect(findPagination().props('value')).toEqual(2);
+ });
+ });
+});
diff --git a/spec/frontend/ci/artifacts/components/job_checkbox_spec.js b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
new file mode 100644
index 00000000000..8b47571239c
--- /dev/null
+++ b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
@@ -0,0 +1,132 @@
+import { GlFormCheckbox } from '@gitlab/ui';
+import mockGetJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import JobCheckbox from '~/ci/artifacts/components/job_checkbox.vue';
+import { I18N_BULK_DELETE_MAX_SELECTED } from '~/ci/artifacts/constants';
+
+describe('JobCheckbox component', () => {
+ let wrapper;
+
+ const mockArtifactNodes = mockGetJobArtifactsResponse.data.project.jobs.nodes[0].artifacts.nodes;
+ const mockSelectedArtifacts = [mockArtifactNodes[0], mockArtifactNodes[1]];
+ const mockUnselectedArtifacts = [mockArtifactNodes[2]];
+
+ const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+
+ const createComponent = ({
+ hasArtifacts = true,
+ selectedArtifacts = mockSelectedArtifacts,
+ unselectedArtifacts = mockUnselectedArtifacts,
+ isSelectedArtifactsLimitReached = false,
+ } = {}) => {
+ wrapper = shallowMountExtended(JobCheckbox, {
+ propsData: {
+ hasArtifacts,
+ selectedArtifacts,
+ unselectedArtifacts,
+ isSelectedArtifactsLimitReached,
+ },
+ mocks: { GlFormCheckbox },
+ });
+ };
+
+ it('is disabled when the job has no artifacts', () => {
+ createComponent({ hasArtifacts: false });
+
+ expect(findCheckbox().attributes('disabled')).toBeDefined();
+ });
+
+ describe('when some artifacts from this job are selected', () => {
+ describe('when the selected artifacts limit has not been reached', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('is indeterminate', () => {
+ expect(findCheckbox().attributes('indeterminate')).toBe('true');
+ expect(findCheckbox().attributes('checked')).toBeUndefined();
+ });
+
+ it('selects the unselected artifacts on click', () => {
+ findCheckbox().vm.$emit('input', true);
+
+ expect(wrapper.emitted('selectArtifact')).toMatchObject([
+ [mockUnselectedArtifacts[0], true],
+ ]);
+ });
+ });
+
+ describe('when the selected artifacts limit has been reached', () => {
+ beforeEach(() => {
+ // limit has been reached by selecting artifacts from this job
+ createComponent({
+ selectedArtifacts: mockSelectedArtifacts,
+ isSelectedArtifactsLimitReached: true,
+ });
+ });
+
+ it('remains enabled', () => {
+ // job checkbox remains enabled to allow de-selection
+ expect(findCheckbox().attributes('disabled')).toBeUndefined();
+ expect(findCheckbox().attributes('title')).not.toBe(I18N_BULK_DELETE_MAX_SELECTED);
+ });
+ });
+ });
+
+ describe('when all artifacts from this job are selected', () => {
+ beforeEach(() => {
+ createComponent({ unselectedArtifacts: [] });
+ });
+
+ it('is checked', () => {
+ expect(findCheckbox().attributes('checked')).toBe('true');
+ });
+
+ it('deselects the selected artifacts on click', () => {
+ findCheckbox().vm.$emit('input', false);
+
+ expect(wrapper.emitted('selectArtifact')).toMatchObject([
+ [mockSelectedArtifacts[0], false],
+ [mockSelectedArtifacts[1], false],
+ ]);
+ });
+ });
+
+ describe('when no artifacts from this job are selected', () => {
+ describe('when the selected artifacts limit has not been reached', () => {
+ beforeEach(() => {
+ createComponent({ selectedArtifacts: [] });
+ });
+
+ it('is enabled and not checked', () => {
+ expect(findCheckbox().attributes('checked')).toBeUndefined();
+ expect(findCheckbox().attributes('disabled')).toBeUndefined();
+ expect(findCheckbox().attributes('title')).toBe('');
+ });
+
+ it('selects the artifacts on click', () => {
+ findCheckbox().vm.$emit('input', true);
+
+ expect(wrapper.emitted('selectArtifact')).toMatchObject([
+ [mockUnselectedArtifacts[0], true],
+ ]);
+ });
+ });
+
+ describe('when the selected artifacts limit has been reached', () => {
+ beforeEach(() => {
+ // limit has been reached by selecting artifacts from other jobs
+ createComponent({
+ selectedArtifacts: [],
+ isSelectedArtifactsLimitReached: true,
+ });
+ });
+
+ it('is disabled when the selected artifacts limit has been reached', () => {
+ // job checkbox is disabled to block further selection
+ expect(findCheckbox().attributes('disabled')).toBeDefined();
+ expect(findCheckbox().attributes('title')).toBe(I18N_BULK_DELETE_MAX_SELECTED);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/artifacts/graphql/cache_update_spec.js b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js
index 4d610328298..3c415534c7c 100644
--- a/spec/frontend/artifacts/graphql/cache_update_spec.js
+++ b/spec/frontend/ci/artifacts/graphql/cache_update_spec.js
@@ -1,5 +1,5 @@
-import getJobArtifactsQuery from '~/artifacts/graphql/queries/get_job_artifacts.query.graphql';
-import { removeArtifactFromStore } from '~/artifacts/graphql/cache_update';
+import getJobArtifactsQuery from '~/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql';
+import { removeArtifactFromStore } from '~/ci/artifacts/graphql/cache_update';
describe('Artifact table cache updates', () => {
let store;
diff --git a/spec/frontend/ci/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
index d4f588a0e09..4b7ca36f331 100644
--- a/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
+++ b/spec/frontend/ci/ci_lint/components/ci_lint_spec.js
@@ -48,7 +48,6 @@ describe('CI Lint', () => {
afterEach(() => {
mockMutate.mockClear();
- wrapper.destroy();
});
it('displays the editor', () => {
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
index e4abedb412f..8990a70d4ef 100644
--- 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
@@ -1,5 +1,7 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+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';
@@ -11,7 +13,7 @@ describe('VariableList', () => {
describe('with only key/value inputs', () => {
describe('with no variables', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
+ setHTMLFixture(htmlPipelineSchedulesEdit);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -69,7 +71,7 @@ describe('VariableList', () => {
describe('with persisted variables', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit_with_variables.html');
+ setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -106,7 +108,7 @@ describe('VariableList', () => {
describe('toggleEnableRow method', () => {
beforeEach(() => {
- loadHTMLFixture('pipeline_schedules/edit_with_variables.html');
+ setHTMLFixture(htmlPipelineSchedulesEditWithVariables);
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
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
index 71e8e6d3afb..3ef5427f288 100644
--- 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
@@ -1,12 +1,13 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+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(() => {
- loadHTMLFixture('pipeline_schedules/edit.html');
+ setHTMLFixture(htmlPipelineSchedulesEdit);
$wrapper = $('.js-ci-variable-list-section');
setupNativeFormVariableList({
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
index 5e0c35c9f90..1d0dcf242a4 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
@@ -1,7 +1,16 @@
import { shallowMount } from '@vue/test-utils';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
import ciAdminVariables from '~/ci/ci_variable_list/components/ci_admin_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
+import addAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
+import getAdminVariables from '~/ci/ci_variable_list/graphql/queries/variables.query.graphql';
describe('Ci Project Variable wrapper', () => {
let wrapper;
@@ -16,18 +25,23 @@ describe('Ci Project Variable wrapper', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
areScopedVariablesAvailable: false,
componentName: 'InstanceVariables',
entity: '',
hideEnvironmentScope: true,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addAdminVariable,
+ [UPDATE_MUTATION_ACTION]: updateAdminVariable,
+ [DELETE_MUTATION_ACTION]: deleteAdminVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getAdminVariables,
+ },
+ },
refetchAfterMutation: true,
fullPath: null,
id: null,
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
index 2fd395a1230..1937e3b34b7 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -1,13 +1,17 @@
import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { allEnvironments } from '~/ci/ci_variable_list/constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { allEnvironments, ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_variable_list/constants';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
describe('Ci environments dropdown', () => {
let wrapper;
const envs = ['dev', 'prod', 'staging'];
- const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
+ const defaultProps = {
+ areEnvironmentsLoading: false,
+ environments: envs,
+ selectedEnvironmentScope: '',
+ };
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findListboxItemByIndex = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
@@ -15,22 +19,24 @@ describe('Ci environments dropdown', () => {
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
+ const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
- const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
- wrapper = mount(CiEnvironmentsDropdown, {
+ const createComponent = ({ props = {}, searchTerm = '', enableFeatureFlag = false } = {}) => {
+ wrapper = mountExtended(CiEnvironmentsDropdown, {
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures: {
+ ciLimitEnvironmentScope: enableFeatureFlag,
+ },
+ },
});
findListbox().vm.$emit('search', searchTerm);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No environments found', () => {
beforeEach(() => {
createComponent({ searchTerm: 'stable' });
@@ -44,19 +50,32 @@ describe('Ci environments dropdown', () => {
});
describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent({ props: { environments: envs } });
- });
+ describe.each`
+ featureFlag | flagStatus | defaultEnvStatus | firstItemValue | envIndices
+ ${true} | ${'enabled'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
+ ${false} | ${'disabled'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
+ `(
+ 'when ciLimitEnvironmentScope feature flag is $flagStatus',
+ ({ featureFlag, defaultEnvStatus, firstItemValue, envIndices }) => {
+ beforeEach(() => {
+ createComponent({ props: { environments: envs }, enableFeatureFlag: featureFlag });
+ });
- it('renders all environments when search term is empty', () => {
- expect(findListboxItemByIndex(0).text()).toBe(envs[0]);
- expect(findListboxItemByIndex(1).text()).toBe(envs[1]);
- expect(findListboxItemByIndex(2).text()).toBe(envs[2]);
- });
+ it(`${defaultEnvStatus} * in listbox`, () => {
+ expect(findListboxItemByIndex(0).text()).toBe(firstItemValue);
+ });
- it('does not display active checkmark on the inactive stage', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
- });
+ it('renders all environments', () => {
+ expect(findListboxItemByIndex(envIndices[0]).text()).toBe(envs[0]);
+ expect(findListboxItemByIndex(envIndices[1]).text()).toBe(envs[1]);
+ expect(findListboxItemByIndex(envIndices[2]).text()).toBe(envs[2]);
+ });
+
+ it('does not display active checkmark', () => {
+ expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
+ });
+ },
+ );
});
describe('when `*` is the value of selectedEnvironmentScope props', () => {
@@ -72,46 +91,92 @@ describe('Ci environments dropdown', () => {
});
});
- describe('Environments found', () => {
+ describe('When ciLimitEnvironmentScope feature flag is disabled', () => {
const currentEnv = envs[2];
beforeEach(() => {
- createComponent({ searchTerm: currentEnv });
+ createComponent();
});
- it('renders only the environment searched for', () => {
+ it('filters on the frontend and renders only the environment searched for', async () => {
+ await findListbox().vm.$emit('search', currentEnv);
+
expect(findAllListboxItems()).toHaveLength(1);
expect(findListboxItemByIndex(0).text()).toBe(currentEnv);
});
- it('does not display create button', () => {
- expect(findCreateWildcardButton().exists()).toBe(false);
+ it('does not emit event when searching', async () => {
+ expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
+
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
});
- describe('Custom events', () => {
- describe('when selecting an environment', () => {
- const itemIndex = 0;
+ it('does not display note about max environments shown', () => {
+ expect(findMaxEnvNote().exists()).toBe(false);
+ });
+ });
- beforeEach(() => {
- createComponent();
- });
+ describe('When ciLimitEnvironmentScope feature flag is enabled', () => {
+ const currentEnv = envs[2];
- it('emits `select-environment` when an environment is clicked', () => {
- findListbox().vm.$emit('select', envs[itemIndex]);
- expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
- });
+ beforeEach(() => {
+ createComponent({ enableFeatureFlag: true });
+ });
+
+ it('renders environments passed down to it', async () => {
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(findAllListboxItems()).toHaveLength(envs.length);
+ });
+
+ it('emits event when searching', async () => {
+ expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
+
+ await findListbox().vm.$emit('search', currentEnv);
+
+ expect(wrapper.emitted('search-environment-scope')).toHaveLength(2);
+ expect(wrapper.emitted('search-environment-scope')[1]).toEqual([currentEnv]);
+ });
+
+ it('renders loading icon while search query is loading', () => {
+ createComponent({ enableFeatureFlag: true, props: { areEnvironmentsLoading: true } });
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('displays note about max environments shown', () => {
+ expect(findMaxEnvNote().exists()).toBe(true);
+ expect(findMaxEnvNote().text()).toContain(String(ENVIRONMENT_QUERY_LIMIT));
+ });
+ });
+
+ describe('Custom events', () => {
+ describe('when selecting an environment', () => {
+ const itemIndex = 0;
+
+ beforeEach(() => {
+ createComponent();
});
- describe('when creating a new environment from a search term', () => {
- const search = 'new-env';
- beforeEach(() => {
- createComponent({ searchTerm: search });
- });
+ it('emits `select-environment` when an environment is clicked', () => {
+ findListbox().vm.$emit('select', envs[itemIndex]);
- it('emits create-environment-scope', () => {
- findCreateWildcardButton().vm.$emit('click');
- expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
- });
+ expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
+ });
+ });
+
+ describe('when creating a new environment from a search term', () => {
+ const search = 'new-env';
+ beforeEach(() => {
+ createComponent({ searchTerm: search });
+ });
+
+ it('emits create-environment-scope', () => {
+ findCreateWildcardButton().vm.$emit('click');
+
+ expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index c0fb133b9b1..7436210fe70 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
@@ -4,6 +4,15 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciGroupVariables from '~/ci/ci_variable_list/components/ci_group_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
+import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import addGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
+import deleteGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
+import updateGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
const mockProvide = {
glFeatures: {
@@ -24,10 +33,6 @@ describe('Ci Group Variable wrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Props', () => {
beforeEach(() => {
createComponent();
@@ -41,8 +46,17 @@ describe('Ci Group Variable wrapper', () => {
entity: 'group',
fullPath: mockProvide.groupPath,
hideEnvironmentScope: false,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addGroupVariable,
+ [UPDATE_MUTATION_ACTION]: updateGroupVariable,
+ [DELETE_MUTATION_ACTION]: deleteGroupVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getGroupVariables,
+ },
+ },
refetchAfterMutation: false,
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
index bd1e6b17d6b..69b0d4261b2 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
@@ -4,6 +4,16 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciProjectVariables from '~/ci/ci_variable_list/components/ci_project_variables.vue';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+} from '~/ci/ci_variable_list/constants';
+import getProjectEnvironments from '~/ci/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql';
+import addProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
+import deleteProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
+import updateProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
const mockProvide = {
projectFullPath: '/namespace/project',
@@ -25,10 +35,6 @@ describe('Ci Project Variable wrapper', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
id: convertToGraphQLId(TYPENAME_PROJECT, mockProvide.projectId),
@@ -37,8 +43,21 @@ describe('Ci Project Variable wrapper', () => {
entity: 'project',
fullPath: mockProvide.projectFullPath,
hideEnvironmentScope: false,
- mutationData: wrapper.vm.$options.mutationData,
- queryData: wrapper.vm.$options.queryData,
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addProjectVariable,
+ [UPDATE_MUTATION_ACTION]: updateProjectVariable,
+ [DELETE_MUTATION_ACTION]: deleteProjectVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: expect.any(Function),
+ query: getProjectVariables,
+ },
+ environments: {
+ lookup: expect.any(Function),
+ query: getProjectEnvironments,
+ },
+ },
refetchAfterMutation: false,
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 508af964ca3..b6ffde9b33f 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -10,10 +10,12 @@ import {
EVENT_LABEL,
EVENT_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE,
+ groupString,
instanceString,
+ projectString,
variableOptions,
} from '~/ci/ci_variable_list/constants';
-import { mockVariablesWithScopes } from '../mocks';
+import { mockEnvs, mockVariablesWithScopes, mockVariablesWithUniqueScopes } from '../mocks';
import ModalStub from '../stubs';
describe('Ci variable modal', () => {
@@ -42,12 +44,13 @@ describe('Ci variable modal', () => {
};
const defaultProps = {
+ areEnvironmentsLoading: false,
areScopedVariablesAvailable: true,
environments: [],
hideEnvironmentScope: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
- variable: [],
+ variables: [],
};
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
@@ -85,10 +88,6 @@ describe('Ci variable modal', () => {
const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type');
const findEnvironmentScopeText = () => wrapper.findByText('Environment scope');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Adding a variable', () => {
describe('when no key/value pair are present', () => {
beforeEach(() => {
@@ -96,7 +95,7 @@ describe('Ci variable modal', () => {
});
it('shows the submit button as disabled', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
+ expect(findAddorUpdateButton().attributes('disabled')).toBeDefined();
});
});
@@ -115,7 +114,6 @@ describe('Ci variable modal', () => {
beforeEach(() => {
createComponent({ props: { selectedVariable: currentVariable } });
- jest.spyOn(wrapper.vm, '$emit');
});
it('Dispatches `add-variable` action on submit', () => {
@@ -156,7 +154,7 @@ describe('Ci variable modal', () => {
findModal().vm.$emit('shown');
});
- it('keeps the value as false', async () => {
+ it('keeps the value as false', () => {
expect(
findProtectedVariableCheckbox().attributes('data-is-protected-checked'),
).toBeUndefined();
@@ -241,7 +239,6 @@ describe('Ci variable modal', () => {
it('defaults to expanded and raw:false when adding a variable', () => {
createComponent({ props: { selectedVariable: variable } });
- jest.spyOn(wrapper.vm, '$emit');
findModal().vm.$emit('shown');
@@ -266,7 +263,6 @@ describe('Ci variable modal', () => {
mode: EDIT_VARIABLE_ACTION,
},
});
- jest.spyOn(wrapper.vm, '$emit');
findModal().vm.$emit('shown');
await findExpandedVariableCheckbox().vm.$emit('change');
@@ -305,7 +301,6 @@ describe('Ci variable modal', () => {
beforeEach(() => {
createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
- jest.spyOn(wrapper.vm, '$emit');
});
it('button text is Update variable when updating', () => {
@@ -353,6 +348,42 @@ describe('Ci variable modal', () => {
expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
});
+
+ describe('when feature flag is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ environments: mockEnvs,
+ variables: mockVariablesWithUniqueScopes(projectString),
+ },
+ provide: { glFeatures: { ciLimitEnvironmentScope: true } },
+ });
+ });
+
+ it('does not merge environment scope sources', () => {
+ const expectedLength = mockEnvs.length;
+
+ expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
+ });
+ });
+
+ describe('when feature flag is disabled', () => {
+ const mockGroupVariables = mockVariablesWithUniqueScopes(groupString);
+ beforeEach(() => {
+ createComponent({
+ props: {
+ environments: mockEnvs,
+ variables: mockGroupVariables,
+ },
+ });
+ });
+
+ it('merges environment scope sources', () => {
+ const expectedLength = mockGroupVariables.length + mockEnvs.length;
+
+ expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
+ });
+ });
});
describe('and section is hidden', () => {
@@ -476,7 +507,7 @@ describe('Ci variable modal', () => {
});
it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
+ expect(findAddorUpdateButton().attributes('disabled')).toBeDefined();
});
it('shows the correct error text', () => {
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 32af2ec4de9..12ca9a78369 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
@@ -1,4 +1,3 @@
-import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
@@ -16,12 +15,14 @@ describe('Ci variable table', () => {
let wrapper;
const defaultProps = {
+ areEnvironmentsLoading: false,
areScopedVariablesAvailable: true,
entity: 'project',
environments: mapEnvironmentNames(mockEnvs),
hideEnvironmentScope: false,
isLoading: false,
maxVariableLimit: 5,
+ pageInfo: { after: '' },
variables: mockVariablesWithScopes(projectString),
};
@@ -37,10 +38,6 @@ describe('Ci variable table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('props passing', () => {
it('passes props down correctly to the ci table', () => {
createComponent();
@@ -49,6 +46,7 @@ describe('Ci variable table', () => {
entity: 'project',
isLoading: defaultProps.isLoading,
maxVariableLimit: defaultProps.maxVariableLimit,
+ pageInfo: defaultProps.pageInfo,
variables: defaultProps.variables,
});
});
@@ -56,10 +54,10 @@ describe('Ci variable table', () => {
it('passes props down correctly to the ci modal', async () => {
createComponent();
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props()).toEqual({
+ areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
@@ -76,15 +74,13 @@ describe('Ci variable table', () => {
});
it('passes down ADD mode when receiving an empty variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
});
it('passes down EDIT mode when receiving a variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
@@ -100,25 +96,21 @@ describe('Ci variable table', () => {
});
it('shows modal when adding a new variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().exists()).toBe(true);
});
it('shows modal when updating a variable', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().exists()).toBe(true);
});
it('hides modal when receiving the event from the modal', async () => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
- findCiVariableModal().vm.$emit('hideModal');
- await nextTick();
+ await findCiVariableModal().vm.$emit('hideModal');
expect(findCiVariableModal().exists()).toBe(false);
});
@@ -135,13 +127,42 @@ describe('Ci variable table', () => {
${'update-variable'}
${'delete-variable'}
`('bubbles up the $eventName event', async ({ eventName }) => {
- findCiVariableTable().vm.$emit('set-selected-variable');
- await nextTick();
+ await findCiVariableTable().vm.$emit('set-selected-variable');
- findCiVariableModal().vm.$emit(eventName, newVariable);
- await nextTick();
+ await findCiVariableModal().vm.$emit(eventName, newVariable);
expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
});
});
+
+ describe('pages events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ eventName | args
+ ${'handle-prev-page'} | ${undefined}
+ ${'handle-next-page'} | ${undefined}
+ ${'sort-changed'} | ${{ sortDesc: true }}
+ `('bubbles up the $eventName event', async ({ args, eventName }) => {
+ await findCiVariableTable().vm.$emit(eventName, args);
+
+ expect(wrapper.emitted(eventName)).toEqual([[args]]);
+ });
+ });
+
+ describe('environment events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('bubbles up the search event', async () => {
+ await findCiVariableTable().vm.$emit('set-selected-variable');
+
+ await findCiVariableModal().vm.$emit('search-environment-scope', 'staging');
+
+ expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
+ });
+ });
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index c977ae773db..a25d325f7a1 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -1,13 +1,12 @@
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { assertProps } from 'helpers/assert_props';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { resolvers } from '~/ci/ci_variable_list/graphql/settings';
-import { TYPENAME_GROUP } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
import ciVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
@@ -18,12 +17,11 @@ import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_varia
import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql';
import {
- ADD_MUTATION_ACTION,
- DELETE_MUTATION_ACTION,
- UPDATE_MUTATION_ACTION,
+ ENVIRONMENT_QUERY_LIMIT,
environmentFetchErrorText,
genericMutationErrorText,
variableFetchErrorText,
+ mapMutationActionToToast,
} from '~/ci/ci_variable_list/constants';
import {
@@ -41,7 +39,7 @@ import {
mockAdminVariables,
} from '../mocks';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -53,6 +51,7 @@ const mockProvide = {
const defaultProps = {
areScopedVariablesAvailable: true,
+ pageInfo: {},
hideEnvironmentScope: false,
refetchAfterMutation: false,
};
@@ -62,15 +61,22 @@ describe('Ci Variable Shared Component', () => {
let mockApollo;
let mockEnvironments;
+ let mockMutation;
+ let mockAddMutation;
+ let mockUpdateMutation;
+ let mockDeleteMutation;
let mockVariables;
+ const mockToastShow = jest.fn();
+
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCiTable = () => wrapper.findComponent(GlTable);
const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
// eslint-disable-next-line consistent-return
- async function createComponentWithApollo({
+ function createComponentWithApollo({
customHandlers = null,
+ customResolvers = null,
isLoading = false,
props = { ...createProjectProps() },
provide = {},
@@ -80,7 +86,9 @@ describe('Ci Variable Shared Component', () => {
[getProjectVariables, mockVariables],
];
- mockApollo = createMockApollo(handlers, resolvers);
+ const mutationResolvers = customResolvers || resolvers;
+
+ mockApollo = createMockApollo(handlers, mutationResolvers);
wrapper = shallowMount(ciVariableShared, {
propsData: {
@@ -93,6 +101,11 @@ describe('Ci Variable Shared Component', () => {
},
apolloProvider: mockApollo,
stubs: { ciVariableSettings, ciVariableTable },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
});
if (!isLoading) {
@@ -103,347 +116,525 @@ describe('Ci Variable Shared Component', () => {
beforeEach(() => {
mockEnvironments = jest.fn();
mockVariables = jest.fn();
+ mockMutation = jest.fn();
+ mockAddMutation = jest.fn();
+ mockUpdateMutation = jest.fn();
+ mockDeleteMutation = jest.fn();
});
- describe('while queries are being fetch', () => {
- beforeEach(() => {
- createComponentWithApollo({ isLoading: true });
- });
+ describe.each`
+ isVariablePagesEnabled | text
+ ${true} | ${'enabled'}
+ ${false} | ${'disabled'}
+ `('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
+ const pagesFeatureFlagProvide = isVariablePagesEnabled
+ ? { glFeatures: { ciVariablesPages: true } }
+ : {};
+
+ describe('while queries are being fetched', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findCiTable().exists()).toBe(false);
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
});
- });
- describe('when queries are resolved', () => {
- describe('successfully', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
+ describe('when queries are resolved', () => {
+ describe('successfully', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo({ provide: createProjectProvide() });
- });
+ await createComponentWithApollo({
+ provide: { ...createProjectProvide(), ...pagesFeatureFlagProvide },
+ });
+ });
- it('passes down the expected max variable limit as props', () => {
- expect(findCiSettings().props('maxVariableLimit')).toBe(
- mockProjectVariables.data.project.ciVariables.limit,
- );
- });
+ it('passes down the expected max variable limit as props', () => {
+ expect(findCiSettings().props('maxVariableLimit')).toBe(
+ mockProjectVariables.data.project.ciVariables.limit,
+ );
+ });
- it('passes down the expected environments as props', () => {
- expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
- });
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([prodName, devName]);
+ });
- it('passes down the expected variables as props', () => {
- expect(findCiSettings().props('variables')).toEqual(
- mockProjectVariables.data.project.ciVariables.nodes,
- );
- });
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockProjectVariables.data.project.ciVariables.nodes,
+ );
+ });
- it('createAlert was not called', () => {
- expect(createAlert).not.toHaveBeenCalled();
+ it('createAlert was not called', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
});
- });
- describe('with an error for variables', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockRejectedValue();
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
+ mockVariables.mockRejectedValue();
- await createComponentWithApollo();
- });
+ await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
+ });
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
});
- });
- describe('with an error for environments', () => {
- beforeEach(async () => {
- mockEnvironments.mockRejectedValue();
- mockVariables.mockResolvedValue(mockProjectVariables);
+ describe('with an error for environments', () => {
+ beforeEach(async () => {
+ mockEnvironments.mockRejectedValue();
+ mockVariables.mockResolvedValue(mockProjectVariables);
- await createComponentWithApollo();
- });
+ await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
+ });
- it('calls createAlert with the expected error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ it('calls createAlert with the expected error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: environmentFetchErrorText });
+ });
});
});
- });
- describe('environment query', () => {
- describe('when there is an environment key in queryData', () => {
- beforeEach(async () => {
- mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- mockVariables.mockResolvedValue(mockProjectVariables);
+ describe('environment query', () => {
+ describe('when there is an environment key in queryData', () => {
+ beforeEach(() => {
+ mockEnvironments.mockResolvedValue(mockProjectEnvironments);
- await createComponentWithApollo({ props: { ...createProjectProps() } });
- });
+ mockVariables.mockResolvedValue(mockProjectVariables);
+ });
- it('is executed', () => {
- expect(mockVariables).toHaveBeenCalled();
- });
- });
+ it('environments are fetched', async () => {
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: pagesFeatureFlagProvide,
+ });
- describe('when there isnt an environment key in queryData', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ expect(mockEnvironments).toHaveBeenCalled();
+ });
- await createComponentWithApollo({ props: { ...createGroupProps() } });
- });
+ describe('when Limit Environment Scope FF is enabled', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: {
+ glFeatures: {
+ ciLimitEnvironmentScope: true,
+ ciVariablesPages: isVariablePagesEnabled,
+ },
+ },
+ });
+ });
- it('is skipped', () => {
- expect(mockVariables).not.toHaveBeenCalled();
- });
- });
- });
+ it('initial query is called with the correct variables', () => {
+ expect(mockEnvironments).toHaveBeenCalledWith({
+ first: ENVIRONMENT_QUERY_LIMIT,
+ fullPath: '/namespace/project/',
+ search: '',
+ });
+ });
- describe('mutations', () => {
- const groupProps = createGroupProps();
+ it(`refetches environments when search term is present`, async () => {
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
+ expect(mockEnvironments).toHaveBeenCalledWith(expect.objectContaining({ search: '' }));
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ await findCiSettings().vm.$emit('search-environment-scope', 'staging');
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: groupProps,
- });
- });
- it.each`
- actionName | mutation | event
- ${'add'} | ${groupProps.mutationData[ADD_MUTATION_ACTION]} | ${'add-variable'}
- ${'update'} | ${groupProps.mutationData[UPDATE_MUTATION_ACTION]} | ${'update-variable'}
- ${'delete'} | ${groupProps.mutationData[DELETE_MUTATION_ACTION]} | ${'delete-variable'}
- `(
- 'calls the right mutation from propsData when user performs $actionName variable',
- async ({ event, mutation }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
-
- await findCiSettings().vm.$emit(event, newVariable);
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation,
- variables: {
- endpoint: mockProvide.endpoint,
- fullPath: groupProps.fullPath,
- id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id),
- variable: newVariable,
- },
- });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws with the specific graphql error if present when user performs $actionName variable',
- async ({ event }) => {
- const graphQLErrorMessage = 'There is a problem with this graphQL action';
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValue({ data: { ciVariableMutation: { errors: [graphQLErrorMessage] } } });
- await findCiSettings().vm.$emit(event, newVariable);
- await nextTick();
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
- },
- );
-
- it.each`
- actionName | event
- ${'add'} | ${'add-variable'}
- ${'update'} | ${'update-variable'}
- ${'delete'} | ${'delete-variable'}
- `(
- 'throws generic error on failure with no graphql errors and user performs $actionName variable',
- async ({ event }) => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
- throw new Error();
+ expect(mockEnvironments).toHaveBeenCalledTimes(2);
+ expect(mockEnvironments).toHaveBeenCalledWith(
+ expect.objectContaining({ search: 'staging' }),
+ );
+ });
});
- await findCiSettings().vm.$emit(event, newVariable);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
- expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
- },
- );
+ describe('when Limit Environment Scope FF is disabled', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({
+ props: { ...createProjectProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ });
+
+ it('initial query is called with the correct variables', () => {
+ expect(mockEnvironments).toHaveBeenCalledWith({ fullPath: '/namespace/project/' });
+ });
- describe('without fullpath and ID props', () => {
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockAdminVariables);
+ it(`does not refetch environments when search term is present`, async () => {
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
- await createComponentWithApollo({
- customHandlers: [[getAdminVariables, mockVariables]],
- props: createInstanceProps(),
+ await findCiSettings().vm.$emit('search-environment-scope', 'staging');
+
+ expect(mockEnvironments).toHaveBeenCalledTimes(1);
+ });
});
});
- it('does not pass fullPath and ID to the mutation', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ describe("when there isn't an environment key in queryData", () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
- await findCiSettings().vm.$emit('add-variable', newVariable);
+ await createComponentWithApollo({
+ props: { ...createGroupProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ });
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: wrapper.props().mutationData[ADD_MUTATION_ACTION],
- variables: {
- endpoint: mockProvide.endpoint,
- variable: newVariable,
- },
+ it('fetching environments is skipped', () => {
+ expect(mockEnvironments).not.toHaveBeenCalled();
});
});
});
- });
- describe('Props', () => {
- const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables;
- const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables;
+ describe('mutations', () => {
+ const groupProps = createGroupProps();
+ const instanceProps = createInstanceProps();
+ const projectProps = createProjectProps();
- describe('in a specific context as', () => {
- it.each`
- name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit
- ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit}
- ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit}
- ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0}
- `(
- 'passes down all the required props when its a $name component',
- async ({
- mutation,
- maxVariableLimit,
- mockVariablesValue,
- mockEnvironmentsValue,
- withEnvironments,
- expectedEnvironments,
- propsFn,
- provideFn,
- }) => {
- const props = propsFn();
- const provide = provideFn();
+ let mockMutationMap;
- mockVariables.mockResolvedValue(mockVariablesValue);
+ describe('error handling and feedback', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ mockMutation.mockResolvedValue({ ...mockGroupVariables.data, errors: [] });
+
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addGroupVariable: mockMutation,
+ updateGroupVariable: mockMutation,
+ deleteGroupVariable: mockMutation,
+ },
+ },
+ props: groupProps,
+ provide: pagesFeatureFlagProvide,
+ });
+ });
- if (withEnvironments) {
- mockEnvironments.mockResolvedValue(mockEnvironmentsValue);
- }
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws the specific graphql error if present when user performs $actionName variable',
+ async ({ event }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ mockMutation.mockResolvedValue({
+ ...mockGroupVariables.data,
+ errors: [graphQLErrorMessage],
+ });
- let customHandlers = null;
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
- if (mutation) {
- customHandlers = [[mutation, mockVariables]];
- }
+ expect(mockMutation).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
- await createComponentWithApollo({ customHandlers, props, provide });
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error on failure with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ mockMutation.mockRejectedValue();
+
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutation).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
- expect(findCiSettings().props()).toEqual({
- areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
- hideEnvironmentScope: defaultProps.hideEnvironmentScope,
- isLoading: false,
- maxVariableLimit,
- variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)?.nodes,
- entity: props.entity,
- environments: expectedEnvironments,
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'displays toast message after user performs $actionName variable',
+ async ({ actionName, event }) => {
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutation).toHaveBeenCalled();
+ expect(mockToastShow).toHaveBeenCalledWith(
+ mapMutationActionToToast[actionName](newVariable.key),
+ );
+ },
+ );
+ });
+
+ const setupMockMutations = (mockResolvedMutation) => {
+ mockAddMutation.mockResolvedValue(mockResolvedMutation);
+ mockUpdateMutation.mockResolvedValue(mockResolvedMutation);
+ mockDeleteMutation.mockResolvedValue(mockResolvedMutation);
+
+ return {
+ add: mockAddMutation,
+ update: mockUpdateMutation,
+ delete: mockDeleteMutation,
+ };
+ };
+
+ describe.each`
+ scope | mockVariablesResolvedValue | getVariablesHandler | addMutationName | updateMutationName | deleteMutationName | props
+ ${'instance'} | ${mockVariables} | ${getAdminVariables} | ${'addAdminVariable'} | ${'updateAdminVariable'} | ${'deleteAdminVariable'} | ${instanceProps}
+ ${'group'} | ${mockGroupVariables} | ${getGroupVariables} | ${'addGroupVariable'} | ${'updateGroupVariable'} | ${'deleteGroupVariable'} | ${groupProps}
+ ${'project'} | ${mockProjectVariables} | ${getProjectVariables} | ${'addProjectVariable'} | ${'updateProjectVariable'} | ${'deleteProjectVariable'} | ${projectProps}
+ `(
+ '$scope variable mutations',
+ ({
+ addMutationName,
+ deleteMutationName,
+ getVariablesHandler,
+ mockVariablesResolvedValue,
+ updateMutationName,
+ props,
+ }) => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockVariablesResolvedValue);
+ mockMutationMap = setupMockMutations({ ...mockVariables.data, errors: [] });
+
+ await createComponentWithApollo({
+ customHandlers: [[getVariablesHandler, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ [addMutationName]: mockAddMutation,
+ [updateMutationName]: mockUpdateMutation,
+ [deleteMutationName]: mockDeleteMutation,
+ },
+ },
+ props,
+ provide: pagesFeatureFlagProvide,
+ });
});
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'calls the right mutation when user performs $actionName variable',
+ async ({ event, actionName }) => {
+ await findCiSettings().vm.$emit(event, newVariable);
+ await waitForPromises();
+
+ expect(mockMutationMap[actionName]).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ endpoint: mockProvide.endpoint,
+ fullPath: props.fullPath,
+ id: props.id,
+ variable: newVariable,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ },
+ );
},
);
- });
- describe('refetchAfterMutation', () => {
- it.each`
- bool | text
- ${true} | ${'refetches the variables'}
- ${false} | ${'does not refetch the variables'}
- `('when $bool it $text', async ({ bool }) => {
- await createComponentWithApollo({
- props: { ...createInstanceProps(), refetchAfterMutation: bool },
+ describe('without fullpath and ID props', () => {
+ beforeEach(async () => {
+ mockMutation.mockResolvedValue({ ...mockAdminVariables.data, errors: [] });
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo({
+ customHandlers: [[getAdminVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addAdminVariable: mockMutation,
+ },
+ },
+ props: createInstanceProps(),
+ provide: pagesFeatureFlagProvide,
+ });
});
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
- jest.spyOn(wrapper.vm.$apollo.queries.ciVariables, 'refetch').mockImplementation(jest.fn());
+ it('does not pass fullPath and ID to the mutation', async () => {
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+ await waitForPromises();
+
+ expect(mockMutation).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ endpoint: mockProvide.endpoint,
+ variable: newVariable,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
+ });
- await findCiSettings().vm.$emit('add-variable', newVariable);
+ describe('Props', () => {
+ const mockGroupCiVariables = mockGroupVariables.data.group.ciVariables;
+ const mockProjectCiVariables = mockProjectVariables.data.project.ciVariables;
+
+ describe('in a specific context as', () => {
+ it.each`
+ name | mockVariablesValue | mockEnvironmentsValue | withEnvironments | expectedEnvironments | propsFn | provideFn | mutation | maxVariableLimit
+ ${'project'} | ${mockProjectVariables} | ${mockProjectEnvironments} | ${true} | ${['prod', 'dev']} | ${createProjectProps} | ${createProjectProvide} | ${null} | ${mockProjectCiVariables.limit}
+ ${'group'} | ${mockGroupVariables} | ${[]} | ${false} | ${[]} | ${createGroupProps} | ${createGroupProvide} | ${getGroupVariables} | ${mockGroupCiVariables.limit}
+ ${'instance'} | ${mockAdminVariables} | ${[]} | ${false} | ${[]} | ${createInstanceProps} | ${() => {}} | ${getAdminVariables} | ${0}
+ `(
+ 'passes down all the required props when its a $name component',
+ async ({
+ mutation,
+ maxVariableLimit,
+ mockVariablesValue,
+ mockEnvironmentsValue,
+ withEnvironments,
+ expectedEnvironments,
+ propsFn,
+ provideFn,
+ }) => {
+ const props = propsFn();
+ const provide = provideFn();
- await nextTick();
+ mockVariables.mockResolvedValue(mockVariablesValue);
- if (bool) {
- expect(wrapper.vm.$apollo.queries.ciVariables.refetch).toHaveBeenCalled();
- } else {
- expect(wrapper.vm.$apollo.queries.ciVariables.refetch).not.toHaveBeenCalled();
- }
- });
- });
+ if (withEnvironments) {
+ mockEnvironments.mockResolvedValue(mockEnvironmentsValue);
+ }
- describe('Validators', () => {
- describe('queryData', () => {
- let error;
+ let customHandlers = null;
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
- });
+ if (mutation) {
+ customHandlers = [[mutation, mockVariables]];
+ }
- it('will mount component with right data', async () => {
- try {
await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: { ...createGroupProps() },
+ customHandlers,
+ props,
+ provide: { ...provide, ...pagesFeatureFlagProvide },
});
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(true);
- expect(error).toBeUndefined();
- }
- });
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- customHandlers: [[getGroupVariables, mockVariables]],
- props: { ...createGroupProps(), queryData: { wrongKey: {} } },
+ expect(findCiSettings().props()).toEqual({
+ areEnvironmentsLoading: false,
+ areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
+ hideEnvironmentScope: defaultProps.hideEnvironmentScope,
+ pageInfo: defaultProps.pageInfo,
+ isLoading: false,
+ maxVariableLimit,
+ variables: wrapper.props().queryData.ciVariables.lookup(mockVariablesValue.data)
+ ?.nodes,
+ entity: props.entity,
+ environments: expectedEnvironments,
});
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
- });
+ },
+ );
});
- describe('mutationData', () => {
- let error;
+ describe('refetchAfterMutation', () => {
+ it.each`
+ bool | text | timesQueryCalled
+ ${true} | ${'refetches the variables'} | ${2}
+ ${false} | ${'does not refetch the variables'} | ${1}
+ `('when $bool it $text', async ({ bool, timesQueryCalled }) => {
+ mockMutation.mockResolvedValue({ ...mockAdminVariables.data, errors: [] });
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo({
+ customHandlers: [[getAdminVariables, mockVariables]],
+ customResolvers: {
+ Mutation: {
+ ...resolvers.Mutation,
+ addAdminVariable: mockMutation,
+ },
+ },
+ props: { ...createInstanceProps(), refetchAfterMutation: bool },
+ provide: pagesFeatureFlagProvide,
+ });
+
+ await findCiSettings().vm.$emit('add-variable', newVariable);
+ await waitForPromises();
- beforeEach(async () => {
- mockVariables.mockResolvedValue(mockGroupVariables);
+ expect(mockVariables).toHaveBeenCalledTimes(timesQueryCalled);
});
+ });
- it('will mount component with right data', async () => {
- try {
- await createComponentWithApollo({
- props: { ...createGroupProps() },
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(true);
- expect(error).toBeUndefined();
- }
+ describe('Validators', () => {
+ describe('queryData', () => {
+ let error;
+
+ beforeEach(() => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ customHandlers: [[getGroupVariables, mockVariables]],
+ props: { ...createGroupProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('report custom validator error on wrong data', () => {
+ expect(() =>
+ assertProps(
+ ciVariableShared,
+ { ...defaultProps, ...createGroupProps(), queryData: { wrongKey: {} } },
+ { provide: mockProvide },
+ ),
+ ).toThrow('custom validator check failed for prop');
+ });
});
- it('will not mount component with wrong data', async () => {
- try {
- await createComponentWithApollo({
- props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
- });
- } catch (e) {
- error = e;
- } finally {
- expect(wrapper.exists()).toBe(false);
- expect(error.toString()).toContain('custom validator check failed for prop');
- }
+ describe('mutationData', () => {
+ let error;
+
+ beforeEach(() => {
+ mockVariables.mockResolvedValue(mockGroupVariables);
+ });
+
+ it('will mount component with right data', async () => {
+ try {
+ await createComponentWithApollo({
+ props: { ...createGroupProps() },
+ provide: pagesFeatureFlagProvide,
+ });
+ } catch (e) {
+ error = e;
+ } finally {
+ expect(wrapper.exists()).toBe(true);
+ expect(error).toBeUndefined();
+ }
+ });
+
+ it('report custom validator error on wrong data', () => {
+ expect(() =>
+ assertProps(
+ ciVariableShared,
+ { ...defaultProps, ...createGroupProps(), mutationData: { wrongKey: {} } },
+ { provide: { ...mockProvide, ...pagesFeatureFlagProvide } },
+ ),
+ ).toThrow('custom validator check failed for prop');
+ });
});
});
});
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 9e2508c56ee..0b28cb06cec 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
@@ -12,18 +12,25 @@ describe('Ci variable table', () => {
entity: 'project',
isLoading: false,
maxVariableLimit: mockVariables(projectString).length + 1,
+ pageInfo: {},
variables: mockVariables(projectString),
};
const mockMaxVariableLimit = defaultProps.variables.length;
- const createComponent = ({ props = {} } = {}) => {
+ const createComponent = ({ props = {}, provide = {} } = {}) => {
wrapper = mountExtended(CiVariableTable, {
attachTo: document.body,
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures: {
+ ciVariablesPages: false,
+ },
+ ...provide,
+ },
});
};
@@ -41,132 +48,136 @@ describe('Ci variable table', () => {
return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, { entity, currentVariableCount, maxVariableLimit });
};
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('When table is empty', () => {
- beforeEach(() => {
- createComponent({ props: { variables: [] } });
- });
+ describe.each`
+ isVariablePagesEnabled | text
+ ${true} | ${'enabled'}
+ ${false} | ${'disabled'}
+ `('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
+ const provide = isVariablePagesEnabled ? { glFeatures: { ciVariablesPages: true } } : {};
- it('displays empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
- });
-
- it('hides the reveal button', () => {
- expect(findRevealButton().exists()).toBe(false);
- });
- });
+ describe('When table is empty', () => {
+ beforeEach(() => {
+ createComponent({ props: { variables: [] }, provide });
+ });
- describe('When table has variables', () => {
- beforeEach(() => {
- createComponent();
- });
+ it('displays empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
+ });
- it('does not display the empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ it('hides the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ });
});
- it('displays the reveal button', () => {
- expect(findRevealButton().exists()).toBe(true);
- });
+ describe('When table has variables', () => {
+ beforeEach(() => {
+ createComponent({ provide });
+ });
- it('displays the correct amount of variables', async () => {
- expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
- });
+ it('does not display the empty message', () => {
+ expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
+ });
- it('displays the correct variable options', async () => {
- expect(findOptionsValues(0)).toBe('Protected, Expanded');
- expect(findOptionsValues(1)).toBe('Masked');
- });
+ it('displays the reveal button', () => {
+ expect(findRevealButton().exists()).toBe(true);
+ });
- it('enables the Add Variable button', () => {
- expect(findAddButton().props('disabled')).toBe(false);
- });
- });
+ it('displays the correct amount of variables', () => {
+ expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(defaultProps.variables.length);
+ });
- describe('When variables have exceeded the max limit', () => {
- beforeEach(() => {
- createComponent({ props: { maxVariableLimit: mockVariables(projectString).length } });
- });
+ it('displays the correct variable options', () => {
+ expect(findOptionsValues(0)).toBe('Protected, Expanded');
+ expect(findOptionsValues(1)).toBe('Masked');
+ });
- it('disables the Add Variable button', () => {
- expect(findAddButton().props('disabled')).toBe(true);
+ it('enables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(false);
+ });
});
- });
- describe('max limit reached alert', () => {
- describe('when there is no variable limit', () => {
+ describe('When variables have exceeded the max limit', () => {
beforeEach(() => {
createComponent({
- props: { maxVariableLimit: 0 },
+ props: { maxVariableLimit: mockVariables(projectString).length },
+ provide,
});
});
- it('hides alert', () => {
- expect(findLimitReachedAlerts().length).toBe(0);
+ it('disables the Add Variable button', () => {
+ expect(findAddButton().props('disabled')).toBe(true);
});
});
- describe('when variable limit exists', () => {
- it('hides alert when limit has not been reached', () => {
- createComponent();
+ describe('max limit reached alert', () => {
+ describe('when there is no variable limit', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { maxVariableLimit: 0 },
+ provide,
+ });
+ });
- expect(findLimitReachedAlerts().length).toBe(0);
+ it('hides alert', () => {
+ expect(findLimitReachedAlerts().length).toBe(0);
+ });
});
- it('shows alert when limit has been reached', () => {
- const exceedsVariableLimitText = generateExceedsVariableLimitText(
- defaultProps.entity,
- defaultProps.variables.length,
- mockMaxVariableLimit,
- );
+ describe('when variable limit exists', () => {
+ it('hides alert when limit has not been reached', () => {
+ createComponent({ provide });
- createComponent({
- props: { maxVariableLimit: mockMaxVariableLimit },
+ expect(findLimitReachedAlerts().length).toBe(0);
});
- expect(findLimitReachedAlerts().length).toBe(2);
+ it('shows alert when limit has been reached', () => {
+ const exceedsVariableLimitText = generateExceedsVariableLimitText(
+ defaultProps.entity,
+ defaultProps.variables.length,
+ mockMaxVariableLimit,
+ );
+
+ createComponent({
+ props: { maxVariableLimit: mockMaxVariableLimit },
+ });
- expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false);
- expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText);
+ expect(findLimitReachedAlerts().length).toBe(2);
- expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false);
- expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText);
+ expect(findLimitReachedAlerts().at(0).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(0).text()).toContain(exceedsVariableLimitText);
+
+ expect(findLimitReachedAlerts().at(1).props('dismissible')).toBe(false);
+ expect(findLimitReachedAlerts().at(1).text()).toContain(exceedsVariableLimitText);
+ });
});
});
- });
- describe('Table click actions', () => {
- beforeEach(() => {
- createComponent();
- });
+ describe('Table click actions', () => {
+ beforeEach(() => {
+ createComponent({ provide });
+ });
- it('reveals secret values when button is clicked', async () => {
- expect(findHiddenValues()).toHaveLength(defaultProps.variables.length);
- expect(findRevealedValues()).toHaveLength(0);
+ it('reveals secret values when button is clicked', async () => {
+ expect(findHiddenValues()).toHaveLength(defaultProps.variables.length);
+ expect(findRevealedValues()).toHaveLength(0);
- await findRevealButton().trigger('click');
+ await findRevealButton().trigger('click');
- expect(findHiddenValues()).toHaveLength(0);
- expect(findRevealedValues()).toHaveLength(defaultProps.variables.length);
- });
+ expect(findHiddenValues()).toHaveLength(0);
+ expect(findRevealedValues()).toHaveLength(defaultProps.variables.length);
+ });
- it('dispatches `setSelectedVariable` with correct variable to edit', async () => {
- await findEditButton().trigger('click');
+ it('dispatches `setSelectedVariable` with correct variable to edit', async () => {
+ await findEditButton().trigger('click');
- expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]);
- });
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[defaultProps.variables[0]]]);
+ });
- it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => {
- await findAddButton().trigger('click');
+ it('dispatches `setSelectedVariable` with no variable when adding a new one', async () => {
+ await findAddButton().trigger('click');
- expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]);
+ expect(wrapper.emitted('set-selected-variable')).toEqual([[null]]);
+ });
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js
index 4da4f53f69f..f9450803308 100644
--- a/spec/frontend/ci/ci_variable_list/mocks.js
+++ b/spec/frontend/ci/ci_variable_list/mocks.js
@@ -56,6 +56,11 @@ export const mockVariablesWithScopes = (kind) =>
return { ...variable, environmentScope: '*' };
});
+export const mockVariablesWithUniqueScopes = (kind) =>
+ mockVariables(kind).map((variable) => {
+ return { ...variable, environmentScope: variable.value };
+ });
+
const createDefaultVars = ({ withScope = true, kind } = {}) => {
let base = mockVariables(kind);
diff --git a/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
index b00e1adab63..48a85eba433 100644
--- a/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert_spec.js
@@ -41,10 +41,6 @@ describe('EE - CodeSnippetAlert', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it("provides a link to the feature's documentation", () => {
const docsLink = findDocsLink();
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
index 8e1d8081dd8..4b0ddacef93 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_form_spec.js
@@ -33,12 +33,8 @@ describe('Pipeline Editor | Commit Form', () => {
const findSubmitBtn = () => wrapper.find('[type="submit"]');
const findCancelBtn = () => wrapper.find('[type="reset"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the form is displayed', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
@@ -61,7 +57,7 @@ describe('Pipeline Editor | Commit Form', () => {
});
describe('when buttons are clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({}, mount);
});
@@ -97,7 +93,7 @@ describe('Pipeline Editor | Commit Form', () => {
createComponent({ props: { hasUnsavedChanges, isNewCiConfigFile } });
if (isDisabled) {
- expect(findSubmitBtn().attributes('disabled')).toBe('true');
+ expect(findSubmitBtn().attributes('disabled')).toBeDefined();
} else {
expect(findSubmitBtn().attributes('disabled')).toBeUndefined();
}
@@ -136,7 +132,7 @@ describe('Pipeline Editor | Commit Form', () => {
it('when the commit message is empty, submit button is disabled', async () => {
await findCommitTextarea().setValue('');
- expect(findSubmitBtn().attributes('disabled')).toBe('disabled');
+ expect(findSubmitBtn().attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
index f6e93c55bbb..8834231aaef 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
@@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { mockTracking } from 'helpers/tracking_helper';
import CommitForm from '~/ci/pipeline_editor/components/commit/commit_form.vue';
import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
import {
@@ -11,12 +12,12 @@ import {
COMMIT_ACTION_UPDATE,
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
+ pipelineEditorTrackingOptions,
} from '~/ci/pipeline_editor/constants';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import commitCreate from '~/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
import updatePipelineEtag from '~/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
-
import {
mockCiConfigPath,
mockCiYml,
@@ -113,10 +114,6 @@ describe('Pipeline Editor | Commit section', () => {
await waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the user commits a new file', () => {
beforeEach(async () => {
mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
@@ -284,4 +281,43 @@ describe('Pipeline Editor | Commit section', () => {
createComponent({ props: { 'scroll-to-commit-form': true } });
expect(findCommitForm().props('scrollToCommitForm')).toBe(true);
});
+
+ describe('tracking', () => {
+ let trackingSpy;
+ const { actions, label } = pipelineEditorTrackingOptions;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ describe('when user commit a new file', () => {
+ beforeEach(async () => {
+ mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
+ createComponentWithApollo({ props: { isNewCiConfigFile: true } });
+ await submitCommit();
+ });
+
+ it('calls tracking event with the CREATE property', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.commitCiConfig, {
+ label,
+ property: COMMIT_ACTION_CREATE,
+ });
+ });
+ });
+
+ describe('when user commit an update to the CI file', () => {
+ beforeEach(async () => {
+ mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
+ createComponentWithApollo({ props: { isNewCiConfigFile: false } });
+ await submitCommit();
+ });
+
+ it('calls the tracking event with the UPDATE property', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.commitCiConfig, {
+ label,
+ property: COMMIT_ACTION_UPDATE,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
index 137137ec657..0ecb77674d5 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js
@@ -21,10 +21,6 @@ describe('First pipeline card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
index cdce757ce7c..417597eaf1f 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/getting_started_card_spec.js
@@ -12,10 +12,6 @@ describe('Getting started card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
index 6909916c3e6..0296ab5a65c 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js
@@ -33,10 +33,6 @@ describe('Pipeline config reference card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
@@ -72,7 +68,7 @@ describe('Pipeline config reference card', () => {
});
};
- it('tracks help page links', async () => {
+ it('tracks help page links', () => {
const {
CI_EXAMPLES_LINK,
CI_HELP_LINK,
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
index 0c6879020de..547ba3cbd8b 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js
@@ -12,10 +12,6 @@ describe('Visual and Lint card', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the title', () => {
expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
index 42e372cc1db..b07d63dd5d9 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js
@@ -11,10 +11,6 @@ describe('Pipeline editor drawer', () => {
wrapper = shallowMount(PipelineEditorDrawer);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits close event when closing the drawer', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
index f510c61ee74..b0c889cfc9f 100644
--- a/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js
@@ -17,10 +17,6 @@ describe('Demo job pill', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the jobName', () => {
expect(wrapper.text()).toContain(jobName);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
index 2a2bc2547cc..2182b6e9cc6 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_config_merged_preview_spec.js
@@ -34,10 +34,6 @@ describe('Text editor component', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findEditor = () => wrapper.findComponent(MockSourceEditor);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when status is valid', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
index dc72694d26f..f1a5c4169fb 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -11,12 +11,25 @@ describe('CI Editor Header', () => {
let wrapper;
let trackingSpy = null;
- const createComponent = ({ showDrawer = false, showJobAssistantDrawer = false } = {}) => {
+ const createComponent = ({
+ showDrawer = false,
+ showJobAssistantDrawer = false,
+ showAiAssistantDrawer = false,
+ aiChatAvailable = false,
+ aiCiConfigGenerator = false,
+ } = {}) => {
wrapper = extendedWrapper(
shallowMount(CiEditorHeader, {
+ provide: {
+ aiChatAvailable,
+ glFeatures: {
+ aiCiConfigGenerator,
+ },
+ },
propsData: {
showDrawer,
showJobAssistantDrawer,
+ showAiAssistantDrawer,
},
}),
);
@@ -24,9 +37,9 @@ describe('CI Editor Header', () => {
const findLinkBtn = () => wrapper.findByTestId('template-repo-link');
const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
+ const findAiAssistnantBtn = () => wrapper.findByTestId('ai-assistant-drawer-toggle');
afterEach(() => {
- wrapper.destroy();
unmockTracking();
});
@@ -40,7 +53,29 @@ describe('CI Editor Header', () => {
label,
});
};
+ describe('Ai Assistant toggle button', () => {
+ describe('when feature is unavailable', () => {
+ it('should not show ai button when feature toggle is off', () => {
+ createComponent({ aiChatAvailable: true });
+ mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(findAiAssistnantBtn().exists()).toBe(false);
+ });
+
+ it('should not show ai button when feature is unavailable', () => {
+ createComponent({ aiCiConfigGenerator: true });
+ mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(findAiAssistnantBtn().exists()).toBe(false);
+ });
+ });
+ describe('when feature is available', () => {
+ it('should show ai button', () => {
+ createComponent({ aiCiConfigGenerator: true, aiChatAvailable: true });
+ mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(findAiAssistnantBtn().exists()).toBe(true);
+ });
+ });
+ });
describe('link button', () => {
beforeEach(() => {
createComponent();
@@ -59,7 +94,7 @@ describe('CI Editor Header', () => {
expect(findLinkBtn().props('icon')).toBe('external-link');
});
- it('tracks the click on the browse button', async () => {
+ it('tracks the click on the browse button', () => {
const { browseTemplates } = pipelineEditorTrackingOptions.actions;
testTracker(findLinkBtn(), browseTemplates);
@@ -92,7 +127,7 @@ describe('CI Editor Header', () => {
expect(wrapper.emitted('open-drawer')).toHaveLength(1);
});
- it('tracks open help drawer action', async () => {
+ it('tracks open help drawer action', () => {
const { actions } = pipelineEditorTrackingOptions;
testTracker(findHelpBtn(), actions.openHelpDrawer);
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
index ec987be8cb8..0be26570fbf 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
@@ -1,7 +1,11 @@
import { shallowMount } from '@vue/test-utils';
+import { editor as monacoEditor } from 'monaco-editor';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
import { EDITOR_READY_EVENT } from '~/editor/constants';
+import { CiSchemaExtension as MockedCiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import { SOURCE_EDITOR_DEBOUNCE } from '~/ci/pipeline_editor/constants';
+import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
import TextEditor from '~/ci/pipeline_editor/components/editor/text_editor.vue';
import {
mockCiConfigPath,
@@ -12,19 +16,26 @@ import {
mockDefaultBranch,
} from '../../mock_data';
+jest.mock('monaco-editor');
+jest.mock('~/editor/extensions/source_editor_ci_schema_ext', () => {
+ const { createMockSourceEditorExtension } = jest.requireActual(
+ 'helpers/create_mock_source_editor_extension',
+ );
+ const { CiSchemaExtension } = jest.requireActual(
+ '~/editor/extensions/source_editor_ci_schema_ext',
+ );
+
+ return {
+ CiSchemaExtension: createMockSourceEditorExtension(CiSchemaExtension),
+ };
+});
+
describe('Pipeline Editor | Text editor component', () => {
let wrapper;
let editorReadyListener;
- let mockUse;
- let mockRegisterCiSchema;
- let mockEditorInstance;
- let editorInstanceDetail;
-
- const MockSourceEditor = {
- template: '<div/>',
- props: ['value', 'fileName', 'editorOptions', 'debounceValue'],
- };
+
+ const getMonacoEditor = () => monacoEditor.create.mock.results[0].value;
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TextEditor, {
@@ -44,33 +55,17 @@ describe('Pipeline Editor | Text editor component', () => {
[EDITOR_READY_EVENT]: editorReadyListener,
},
stubs: {
- SourceEditor: MockSourceEditor,
+ SourceEditor,
},
});
};
- const findEditor = () => wrapper.findComponent(MockSourceEditor);
+ const findEditor = () => wrapper.findComponent(SourceEditor);
beforeEach(() => {
- editorReadyListener = jest.fn();
- mockUse = jest.fn();
- mockRegisterCiSchema = jest.fn();
- mockEditorInstance = {
- use: mockUse,
- registerCiSchema: mockRegisterCiSchema,
- };
- editorInstanceDetail = {
- detail: {
- instance: mockEditorInstance,
- },
- };
- });
+ jest.spyOn(monacoEditor, 'create');
- afterEach(() => {
- wrapper.destroy();
-
- mockUse.mockClear();
- mockRegisterCiSchema.mockClear();
+ editorReadyListener = jest.fn();
});
describe('template', () => {
@@ -99,21 +94,34 @@ describe('Pipeline Editor | Text editor component', () => {
});
it('bubbles up events', () => {
- findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
-
expect(editorReadyListener).toHaveBeenCalled();
});
+
+ it('scrolls editor to bottom on scroll editor to bottom event', () => {
+ const setScrollTop = jest.spyOn(getMonacoEditor(), 'setScrollTop');
+
+ eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
+
+ expect(setScrollTop).toHaveBeenCalledWith(getMonacoEditor().getScrollHeight());
+ });
+
+ it('when destroyed, destroys scroll listener', () => {
+ const setScrollTop = jest.spyOn(getMonacoEditor(), 'setScrollTop');
+
+ wrapper.destroy();
+ eventHub.$emit(SCROLL_EDITOR_TO_BOTTOM);
+
+ expect(setScrollTop).not.toHaveBeenCalled();
+ });
});
describe('CI schema', () => {
beforeEach(() => {
createComponent();
- findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
});
it('configures editor with syntax highlight', () => {
- expect(mockUse).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
+ expect(MockedCiSchemaExtension.mockedMethods.registerCiSchema).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
index a26232df58f..3a99949413b 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/branch_switcher_spec.js
@@ -133,10 +133,6 @@ describe('Pipeline editor branch switcher', () => {
mockAvailableBranchQuery = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const testErrorHandling = () => {
expect(wrapper.emitted('showError')).toBeDefined();
expect(wrapper.emitted('showError')[0]).toEqual([
@@ -292,7 +288,7 @@ describe('Pipeline editor branch switcher', () => {
});
describe('with a search term', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockAvailableBranchQuery.mockResolvedValue(mockSearchBranches);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
index 907db16913c..19c113689c2 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js
@@ -48,10 +48,6 @@ describe('Pipeline editor file nav', () => {
const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findPopoverContainer = () => wrapper.findComponent(FileTreePopover);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
index 11ba517e0eb..f2effcb2966 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/container_spec.js
@@ -22,7 +22,7 @@ describe('Pipeline editor file nav', () => {
includes,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs,
}),
@@ -35,7 +35,6 @@ describe('Pipeline editor file nav', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('template', () => {
@@ -61,11 +60,11 @@ describe('Pipeline editor file nav', () => {
expect(fileTreeItems().exists()).toBe(false);
});
- it('renders alert tip', async () => {
+ it('renders alert tip', () => {
expect(findTip().exists()).toBe(true);
});
- it('renders learn more link', async () => {
+ it('renders learn more link', () => {
expect(findTip().props('secondaryButtonLink')).toBe(mockIncludesHelpPagePath);
});
@@ -88,7 +87,7 @@ describe('Pipeline editor file nav', () => {
});
});
- it('does not render alert tip', async () => {
+ it('does not render alert tip', () => {
expect(findTip().exists()).toBe(false);
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
index bceb741f91c..80737e9a8ab 100644
--- a/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/file-tree/file_item_spec.js
@@ -18,10 +18,6 @@ describe('Pipeline editor file nav', () => {
const fileIcon = () => wrapper.findComponent(FileIcon);
const link = () => wrapper.findComponent(GlLink);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
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 555b9f29fbf..a651664851e 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
@@ -26,11 +26,6 @@ describe('Pipeline editor header', () => {
const findPipelineStatus = () => wrapper.findComponent(PipelineStatus);
const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
it('hides the pipeline status for new projects without a CI file', () => {
createComponent({ props: { isNewCiConfigFile: true } });
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 7bf955012c7..b8526e569ec 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
@@ -96,7 +96,7 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
- it('should emit an error event when query fails', async () => {
+ it('should emit an error event when query fails', () => {
expect(wrapper.emitted('showError')).toHaveLength(1);
expect(wrapper.emitted('showError')[0]).toEqual([
{
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 a62c51ffb59..8ca88472bf1 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
@@ -48,7 +48,6 @@ describe('Pipeline Status', () => {
afterEach(() => {
mockPipelineQuery.mockReset();
- wrapper.destroy();
});
describe('loading icon', () => {
@@ -78,7 +77,7 @@ describe('Pipeline Status', () => {
await waitForPromises();
});
- it('query is called with correct variables', async () => {
+ it('query is called with correct variables', () => {
expect(mockPipelineQuery).toHaveBeenCalledTimes(1);
expect(mockPipelineQuery).toHaveBeenCalledWith({
fullPath: mockProjectFullPath,
diff --git a/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
index 0853a6f4ca4..a107a626c6d 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/validation_segment_spec.js
@@ -1,11 +1,10 @@
import VueApollo from 'vue-apollo';
-import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import Vue from 'vue';
import { escape } from 'lodash';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { sprintf } from '~/locale';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import ValidationSegment, {
i18n,
} from '~/ci/pipeline_editor/components/header/validation_segment.vue';
@@ -20,8 +19,8 @@ import {
} from '~/ci/pipeline_editor/constants';
import {
mergeUnwrappedCiConfig,
+ mockCiTroubleshootingPath,
mockCiYml,
- mockLintUnavailableHelpPagePath,
mockYmlHelpPagePath,
} from '../../mock_data';
@@ -43,29 +42,27 @@ describe('Validation segment component', () => {
},
});
- wrapper = extendedWrapper(
- shallowMount(ValidationSegment, {
- apolloProvider: mockApollo,
- provide: {
- ymlHelpPagePath: mockYmlHelpPagePath,
- lintUnavailableHelpPagePath: mockLintUnavailableHelpPagePath,
- },
- propsData: {
- ciConfig: mergeUnwrappedCiConfig(),
- ciFileContent: mockCiYml,
- ...props,
- },
- }),
- );
+ wrapper = shallowMountExtended(ValidationSegment, {
+ apolloProvider: mockApollo,
+ provide: {
+ ymlHelpPagePath: mockYmlHelpPagePath,
+ ciTroubleshootingPath: mockCiTroubleshootingPath,
+ },
+ propsData: {
+ ciConfig: mergeUnwrappedCiConfig(),
+ ciFileContent: mockCiYml,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
};
const findIcon = () => wrapper.findComponent(GlIcon);
- const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink');
- const findValidationMsg = () => wrapper.findByTestId('validationMsg');
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findHelpLink = () => wrapper.findComponent(GlLink);
+ const findValidationMsg = () => wrapper.findComponent(GlSprintf);
+ const findValidationSegment = () => wrapper.findByTestId('validation-segment');
it('shows the loading state', () => {
createComponent({ appStatus: EDITOR_APP_STATUS_LOADING });
@@ -82,8 +79,12 @@ describe('Validation segment component', () => {
expect(findIcon().props('name')).toBe('check');
});
+ it('does not render a link', () => {
+ expect(findHelpLink().exists()).toBe(false);
+ });
+
it('shows a message for empty state', () => {
- expect(findValidationMsg().text()).toBe(i18n.empty);
+ expect(findValidationSegment().text()).toBe(i18n.empty);
});
});
@@ -97,12 +98,15 @@ describe('Validation segment component', () => {
});
it('shows a message for valid state', () => {
- expect(findValidationMsg().text()).toContain(i18n.valid);
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.valid, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
- expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
});
});
@@ -117,13 +121,16 @@ describe('Validation segment component', () => {
expect(findIcon().props('name')).toBe('warning-solid');
});
- it('has message for invalid state', () => {
- expect(findValidationMsg().text()).toBe(i18n.invalid);
+ it('shows a message for invalid state', () => {
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.invalid, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockYmlHelpPagePath);
- expect(findLearnMoreLink().text()).toBe('Learn more');
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
});
describe('with multiple errors', () => {
@@ -140,11 +147,16 @@ describe('Validation segment component', () => {
},
});
});
+
+ it('shows the learn more link', () => {
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findValidationMsg().text()).toBe('Learn more');
+ expect(findHelpLink().attributes('href')).toBe(mockYmlHelpPagePath);
+ });
+
it('shows an invalid state with an error', () => {
- // Test the error is shown _and_ the string matches
- expect(findValidationMsg().text()).toContain(firstError);
- expect(findValidationMsg().text()).toBe(
- sprintf(i18n.invalidWithReason, { reason: firstError }),
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.invalidWithReason, { reason: firstError, linkStart: '', linkEnd: '' }),
);
});
});
@@ -163,10 +175,8 @@ describe('Validation segment component', () => {
});
});
it('shows an invalid state with an error while preventing XSS', () => {
- const { innerHTML } = findValidationMsg().element;
-
- expect(innerHTML).not.toContain(evilError);
- expect(innerHTML).toContain(escape(evilError));
+ expect(findValidationSegment().html()).not.toContain(evilError);
+ expect(findValidationSegment().html()).toContain(escape(evilError));
});
});
});
@@ -182,16 +192,18 @@ describe('Validation segment component', () => {
});
it('show a message that the service is unavailable', () => {
- expect(findValidationMsg().text()).toBe(i18n.unavailableValidation);
+ expect(findValidationSegment().text()).toBe(
+ sprintf(i18n.unavailableValidation, { linkStart: '', linkEnd: '' }),
+ );
});
it('shows the time-out icon', () => {
expect(findIcon().props('name')).toBe('time-out');
});
- it('shows the learn more link', () => {
- expect(findLearnMoreLink().attributes('href')).toBe(mockLintUnavailableHelpPagePath);
- expect(findLearnMoreLink().text()).toBe(i18n.learnMore);
+ it('shows the link to ci troubleshooting', () => {
+ expect(findValidationMsg().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(mockCiTroubleshootingPath);
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js
new file mode 100644
index 00000000000..9046be4a45e
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item_spec.js
@@ -0,0 +1,127 @@
+import ArtifactsAndCacheItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Artifacts and cache item', () => {
+ let wrapper;
+
+ const findArtifactsPathsInputByIndex = (index) =>
+ wrapper.findByTestId(`artifacts-paths-input-${index}`);
+ const findArtifactsExcludeInputByIndex = (index) =>
+ wrapper.findByTestId(`artifacts-exclude-input-${index}`);
+ const findCachePathsInputByIndex = (index) => wrapper.findByTestId(`cache-paths-input-${index}`);
+ const findCacheKeyInput = () => wrapper.findByTestId('cache-key-input');
+ const findDeleteArtifactsPathsButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-artifacts-paths-button-${index}`);
+ const findDeleteArtifactsExcludeButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-artifacts-exclude-button-${index}`);
+ const findDeleteCachePathsButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-cache-paths-button-${index}`);
+ const findAddArtifactsPathsButton = () => wrapper.findByTestId('add-artifacts-paths-button');
+ const findAddArtifactsExcludeButton = () => wrapper.findByTestId('add-artifacts-exclude-button');
+ const findAddCachePathsButton = () => wrapper.findByTestId('add-cache-paths-button');
+
+ const dummyArtifactsPath = 'dummyArtifactsPath';
+ const dummyArtifactsExclude = 'dummyArtifactsExclude';
+ const dummyCachePath = 'dummyCachePath';
+ const dummyCacheKey = 'dummyCacheKey';
+
+ const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => {
+ wrapper = shallowMountExtended(ArtifactsAndCacheItem, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ it('should emit update job event when filling inputs', () => {
+ createComponent();
+
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findArtifactsPathsInputByIndex(0).vm.$emit('input', dummyArtifactsPath);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual([
+ 'artifacts.paths[0]',
+ dummyArtifactsPath,
+ ]);
+
+ findArtifactsExcludeInputByIndex(0).vm.$emit('input', dummyArtifactsExclude);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual([
+ 'artifacts.exclude[0]',
+ dummyArtifactsExclude,
+ ]);
+
+ findCachePathsInputByIndex(0).vm.$emit('input', dummyCachePath);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[0]', dummyCachePath]);
+
+ findCacheKeyInput().vm.$emit('input', dummyCacheKey);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toStrictEqual(['cache.key', dummyCacheKey]);
+ });
+
+ it('should emit update job event when click add item button', () => {
+ createComponent();
+
+ findAddArtifactsPathsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual(['artifacts.paths[1]', '']);
+
+ findAddArtifactsExcludeButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual(['artifacts.exclude[1]', '']);
+
+ findAddCachePathsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[1]', '']);
+ });
+
+ it('should emit update job event when click delete item button', () => {
+ createComponent({
+ job: {
+ artifacts: {
+ paths: ['0', '1'],
+ exclude: ['0', '1'],
+ },
+ cache: {
+ paths: ['0', '1'],
+ key: '',
+ },
+ },
+ });
+
+ findDeleteArtifactsPathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toStrictEqual(['artifacts.paths[0]']);
+
+ findDeleteArtifactsExcludeButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toStrictEqual(['artifacts.exclude[0]']);
+
+ findDeleteCachePathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toStrictEqual(['cache.paths[0]']);
+ });
+
+ it('should not emit update job event when click the only one delete item button', () => {
+ createComponent();
+
+ findDeleteArtifactsPathsButtonByIndex(0).vm.$emit('click');
+ findDeleteArtifactsExcludeButtonByIndex(0).vm.$emit('click');
+ findDeleteCachePathsButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js
new file mode 100644
index 00000000000..f99d7277612
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item_spec.js
@@ -0,0 +1,39 @@
+import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Image item', () => {
+ let wrapper;
+
+ const findImageNameInput = () => wrapper.findByTestId('image-name-input');
+ const findImageEntrypointInput = () => wrapper.findByTestId('image-entrypoint-input');
+
+ const dummyImageName = 'a';
+ const dummyImageEntrypoint = ['b', 'c'];
+
+ const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => {
+ wrapper = shallowMountExtended(ImageItem, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findImageNameInput().vm.$emit('input', dummyImageName);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['image.name', dummyImageName]);
+
+ findImageEntrypointInput().vm.$emit('input', dummyImageEntrypoint.join('\n'));
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual(['image.entrypoint', dummyImageEntrypoint]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
new file mode 100644
index 00000000000..373fb1b70c7
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item_spec.js
@@ -0,0 +1,60 @@
+import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Job setup item', () => {
+ let wrapper;
+
+ const findJobNameInput = () => wrapper.findByTestId('job-name-input');
+ const findJobScriptInput = () => wrapper.findByTestId('job-script-input');
+ const findJobTagsInput = () => wrapper.findByTestId('job-tags-input');
+ const findJobStageInput = () => wrapper.findByTestId('job-stage-input');
+
+ const dummyJobName = 'dummyJobName';
+ const dummyJobScript = 'dummyJobScript';
+ const dummyJobStage = 'dummyJobStage';
+ const dummyJobTags = ['tag1'];
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(JobSetupItem, {
+ propsData: {
+ availableStages: ['.pre', dummyJobStage, '.post'],
+ tagOptions: [
+ { id: 'tag1', name: 'tag1' },
+ { id: 'tag2', name: 'tag2' },
+ ],
+ isNameValid: true,
+ isScriptValid: true,
+ job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findJobNameInput().vm.$emit('input', dummyJobName);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['name', dummyJobName]);
+
+ findJobScriptInput().vm.$emit('input', dummyJobScript);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual(['script', dummyJobScript]);
+
+ findJobStageInput().vm.$emit('input', dummyJobStage);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toEqual(['stage', dummyJobStage]);
+
+ findJobTagsInput().vm.$emit('input', dummyJobTags);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toEqual(['tags', dummyJobTags]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
new file mode 100644
index 00000000000..659ccb25996
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
@@ -0,0 +1,70 @@
+import RulesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ JOB_TEMPLATE,
+ JOB_RULES_WHEN,
+ JOB_RULES_START_IN,
+} from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Rules item', () => {
+ let wrapper;
+
+ const findRulesWhenSelect = () => wrapper.findByTestId('rules-when-select');
+ const findRulesStartInNumberInput = () => wrapper.findByTestId('rules-start-in-number-input');
+ const findRulesStartInUnitSelect = () => wrapper.findByTestId('rules-start-in-unit-select');
+ const findRulesAllowFailureCheckBox = () => wrapper.findByTestId('rules-allow-failure-checkbox');
+
+ const dummyRulesWhen = JOB_RULES_WHEN.delayed.value;
+ const dummyRulesStartInNumber = 2;
+ const dummyRulesStartInUnit = JOB_RULES_START_IN.week.value;
+ const dummyRulesAllowFailure = true;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(RulesItem, {
+ propsData: {
+ isStartValid: true,
+ job: JSON.parse(JSON.stringify(JOB_TEMPLATE)),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit update job event when filling inputs', () => {
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findRulesWhenSelect().vm.$emit('input', dummyRulesWhen);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual([
+ 'rules[0].when',
+ JOB_RULES_WHEN.delayed.value,
+ ]);
+
+ findRulesStartInNumberInput().vm.$emit('input', dummyRulesStartInNumber);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual([
+ 'rules[0].start_in',
+ `2 ${JOB_RULES_START_IN.second.value}s`,
+ ]);
+
+ findRulesStartInUnitSelect().vm.$emit('input', dummyRulesStartInUnit);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toEqual([
+ 'rules[0].start_in',
+ `2 ${dummyRulesStartInUnit}s`,
+ ]);
+
+ findRulesAllowFailureCheckBox().vm.$emit('input', dummyRulesAllowFailure);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toEqual([
+ 'rules[0].allow_failure',
+ dummyRulesAllowFailure,
+ ]);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item_spec.js
new file mode 100644
index 00000000000..284d639c77f
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item_spec.js
@@ -0,0 +1,79 @@
+import ServicesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { JOB_TEMPLATE } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+
+describe('Services item', () => {
+ let wrapper;
+
+ const findServiceNameInputByIndex = (index) =>
+ wrapper.findByTestId(`service-name-input-${index}`);
+ const findServiceEntrypointInputByIndex = (index) =>
+ wrapper.findByTestId(`service-entrypoint-input-${index}`);
+ const findDeleteItemButtonByIndex = (index) =>
+ wrapper.findByTestId(`delete-job-service-button-${index}`);
+ const findAddItemButton = () => wrapper.findByTestId('add-job-service-button');
+
+ const dummyServiceName = 'a';
+ const dummyServiceEntrypoint = ['b', 'c'];
+
+ const createComponent = ({ job = JSON.parse(JSON.stringify(JOB_TEMPLATE)) } = {}) => {
+ wrapper = shallowMountExtended(ServicesItem, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ it('should emit update job event when filling inputs', () => {
+ createComponent();
+
+ expect(wrapper.emitted('update-job')).toBeUndefined();
+
+ findServiceNameInputByIndex(0).vm.$emit('input', dummyServiceName);
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['services[0].name', dummyServiceName]);
+
+ findServiceEntrypointInputByIndex(0).vm.$emit('input', dummyServiceEntrypoint.join('\n'));
+
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
+ expect(wrapper.emitted('update-job')[1]).toEqual([
+ 'services[0].entrypoint',
+ dummyServiceEntrypoint,
+ ]);
+ });
+
+ it('should emit update job event when click add item button', () => {
+ createComponent();
+
+ findAddItemButton().vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual([
+ 'services[1]',
+ { name: '', entrypoint: [''] },
+ ]);
+ });
+
+ it('should emit update job event when click delete item button', () => {
+ createComponent({
+ job: {
+ services: [
+ { name: 'a', entrypoint: ['a'] },
+ { name: 'b', entrypoint: ['b'] },
+ ],
+ },
+ });
+
+ findDeleteItemButtonByIndex(0).vm.$emit('click');
+
+ expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')[0]).toEqual(['services[0]']);
+ });
+
+ it('should not show delete item button when there is only one service', () => {
+ createComponent();
+
+ expect(findDeleteItemButtonByIndex(0).exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
index 79200d92598..0258a1a8c7f 100644
--- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
@@ -1,24 +1,64 @@
import { GlDrawer } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import { stringify } from 'yaml';
import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
+import JobSetupItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/job_setup_item.vue';
+import ImageItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/image_item.vue';
+import ServicesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/services_item.vue';
+import ArtifactsAndCacheItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/artifacts_and_cache_item.vue';
+import RulesItem from '~/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item.vue';
+import { JOB_RULES_WHEN } from '~/ci/pipeline_editor/components/job_assistant_drawer/constants';
+import getRunnerTags from '~/ci/pipeline_editor/graphql/queries/runner_tags.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import eventHub, { SCROLL_EDITOR_TO_BOTTOM } from '~/ci/pipeline_editor/event_hub';
+import { mockRunnersTagsQueryResponse, mockLintResponse, mockCiYml } from '../../mock_data';
Vue.use(VueApollo);
describe('Job assistant drawer', () => {
let wrapper;
+ let mockApollo;
+
+ const dummyJobName = 'dummyJobName';
+ const dummyJobScript = 'dummyJobScript';
+ const dummyImageName = 'dummyImageName';
+ const dummyImageEntrypoint = 'dummyImageEntrypoint';
+ const dummyServicesName = 'dummyServicesName';
+ const dummyServicesEntrypoint = 'dummyServicesEntrypoint';
+ const dummyArtifactsPath = 'dummyArtifactsPath';
+ const dummyArtifactsExclude = 'dummyArtifactsExclude';
+ const dummyCachePath = 'dummyCachePath';
+ const dummyCacheKey = 'dummyCacheKey';
+ const dummyRulesWhen = JOB_RULES_WHEN.delayed.value;
+ const dummyRulesStartIn = '1 second';
+ const dummyRulesAllowFailure = true;
const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findJobSetupItem = () => wrapper.findComponent(JobSetupItem);
+ const findImageItem = () => wrapper.findComponent(ImageItem);
+ const findServicesItem = () => wrapper.findComponent(ServicesItem);
+ const findArtifactsAndCacheItem = () => wrapper.findComponent(ArtifactsAndCacheItem);
+ const findRulesItem = () => wrapper.findComponent(RulesItem);
+ const findConfirmButton = () => wrapper.findByTestId('confirm-button');
const findCancelButton = () => wrapper.findByTestId('cancel-button');
const createComponent = () => {
+ mockApollo = createMockApollo([
+ [getRunnerTags, jest.fn().mockResolvedValue(mockRunnersTagsQueryResponse)],
+ ]);
+
wrapper = mountExtended(JobAssistantDrawer, {
propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
isVisible: true,
},
+ apolloProvider: mockApollo,
});
};
@@ -27,6 +67,35 @@ describe('Job assistant drawer', () => {
await waitForPromises();
});
+ it('should contain job setup accordion', () => {
+ expect(findJobSetupItem().exists()).toBe(true);
+ });
+
+ it('job setup item should have tag options', () => {
+ expect(findJobSetupItem().props('tagOptions')).toEqual([
+ { id: 'tag1', name: 'tag1' },
+ { id: 'tag2', name: 'tag2' },
+ { id: 'tag3', name: 'tag3' },
+ { id: 'tag4', name: 'tag4' },
+ ]);
+ });
+
+ it('should contain image accordion', () => {
+ expect(findImageItem().exists()).toBe(true);
+ });
+
+ it('should contain services accordion', () => {
+ expect(findServicesItem().exists()).toBe(true);
+ });
+
+ it('should contain artifacts and cache item accordion', () => {
+ expect(findArtifactsAndCacheItem().exists()).toBe(true);
+ });
+
+ it('should contain rules accordion', () => {
+ expect(findRulesItem().exists()).toBe(true);
+ });
+
it('should emit close job assistant drawer event when closing the drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
@@ -42,4 +111,185 @@ describe('Job assistant drawer', () => {
expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
});
+
+ it('should block submit if job name is empty', async () => {
+ findJobSetupItem().vm.$emit('update-job', 'script', 'b');
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('isNameValid')).toBe(false);
+ expect(findJobSetupItem().props('isScriptValid')).toBe(true);
+ expect(wrapper.emitted('updateCiConfig')).toBeUndefined();
+ });
+
+ it('should block submit if rules when is delayed and start in is out of range', async () => {
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', JOB_RULES_WHEN.delayed.value);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', '2 weeks');
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(wrapper.emitted('updateCiConfig')).toBeUndefined();
+ });
+
+ describe('when enter valid input', () => {
+ beforeEach(() => {
+ findJobSetupItem().vm.$emit('update-job', 'name', dummyJobName);
+ findJobSetupItem().vm.$emit('update-job', 'script', dummyJobScript);
+ findImageItem().vm.$emit('update-job', 'image.name', dummyImageName);
+ findImageItem().vm.$emit('update-job', 'image.entrypoint', [dummyImageEntrypoint]);
+ findServicesItem().vm.$emit('update-job', 'services[0].name', dummyServicesName);
+ findServicesItem().vm.$emit('update-job', 'services[0].entrypoint', [
+ dummyServicesEntrypoint,
+ ]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'artifacts.paths', [dummyArtifactsPath]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'artifacts.exclude', [
+ dummyArtifactsExclude,
+ ]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'cache.paths', [dummyCachePath]);
+ findArtifactsAndCacheItem().vm.$emit('update-job', 'cache.key', dummyCacheKey);
+ findRulesItem().vm.$emit('update-job', 'rules[0].allow_failure', dummyRulesAllowFailure);
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', dummyRulesWhen);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', dummyRulesStartIn);
+ });
+
+ it('passes correct prop to accordions', () => {
+ const accordions = [
+ findJobSetupItem(),
+ findImageItem(),
+ findServicesItem(),
+ findArtifactsAndCacheItem(),
+ findRulesItem(),
+ ];
+ accordions.forEach((accordion) => {
+ expect(accordion.props('job')).toMatchObject({
+ name: dummyJobName,
+ script: dummyJobScript,
+ image: {
+ name: dummyImageName,
+ entrypoint: [dummyImageEntrypoint],
+ },
+ services: [
+ {
+ name: dummyServicesName,
+ entrypoint: [dummyServicesEntrypoint],
+ },
+ ],
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ rules: [
+ {
+ allow_failure: dummyRulesAllowFailure,
+ when: dummyRulesWhen,
+ start_in: dummyRulesStartIn,
+ },
+ ],
+ });
+ });
+ });
+
+ it('job name and script state should be valid', () => {
+ expect(findJobSetupItem().props('isNameValid')).toBe(true);
+ expect(findJobSetupItem().props('isScriptValid')).toBe(true);
+ });
+
+ it('should clear job data when click confirm button', async () => {
+ findConfirmButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
+ });
+
+ it('should clear job data when click cancel button', async () => {
+ findCancelButton().trigger('click');
+
+ await nextTick();
+
+ expect(findJobSetupItem().props('job')).toMatchObject({ name: '', script: '' });
+ });
+
+ it('should omit keys with default value when click add button', () => {
+ findRulesItem().vm.$emit('update-job', 'rules[0].allow_failure', false);
+ findRulesItem().vm.$emit('update-job', 'rules[0].when', JOB_RULES_WHEN.onSuccess.value);
+ findRulesItem().vm.$emit('update-job', 'rules[0].start_in', dummyRulesStartIn);
+ findConfirmButton().trigger('click');
+
+ expect(wrapper.emitted('updateCiConfig')).toStrictEqual([
+ [
+ `${wrapper.props('ciFileContent')}\n${stringify({
+ [dummyJobName]: {
+ script: dummyJobScript,
+ image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] },
+ services: [
+ {
+ name: dummyServicesName,
+ entrypoint: [dummyServicesEntrypoint],
+ },
+ ],
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ },
+ })}`,
+ ],
+ ]);
+ });
+
+ it('should update correct ci content when click add button', () => {
+ findConfirmButton().trigger('click');
+
+ expect(wrapper.emitted('updateCiConfig')).toStrictEqual([
+ [
+ `${wrapper.props('ciFileContent')}\n${stringify({
+ [dummyJobName]: {
+ script: dummyJobScript,
+ image: { name: dummyImageName, entrypoint: [dummyImageEntrypoint] },
+ services: [
+ {
+ name: dummyServicesName,
+ entrypoint: [dummyServicesEntrypoint],
+ },
+ ],
+ artifacts: {
+ paths: [dummyArtifactsPath],
+ exclude: [dummyArtifactsExclude],
+ },
+ cache: {
+ paths: [dummyCachePath],
+ key: dummyCacheKey,
+ },
+ rules: [
+ {
+ allow_failure: dummyRulesAllowFailure,
+ when: dummyRulesWhen,
+ start_in: dummyRulesStartIn,
+ },
+ ],
+ },
+ })}`,
+ ],
+ ]);
+ });
+
+ it('should emit scroll editor to button event when click add button', () => {
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+
+ findConfirmButton().trigger('click');
+
+ expect(eventHubSpy).toHaveBeenCalledWith(SCROLL_EDITOR_TO_BOTTOM);
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
index d43bdec3a33..cc9a77ae525 100644
--- a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_results_spec.js
@@ -40,10 +40,6 @@ describe('CI Lint Results', () => {
const findAfterScripts = findAllByTestId('after-script');
const filterEmptyScripts = (property) => mockJobs.filter((job) => job[property].length !== 0);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Empty results', () => {
it('renders with no jobs, errors or warnings defined', () => {
createComponent({ jobs: undefined, errors: undefined, warnings: undefined }, shallowMount);
diff --git a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
index b5e3ea06c2c..d09e22898cd 100644
--- a/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/lint/ci_lint_warnings_spec.js
@@ -21,11 +21,6 @@ describe('CI lint warnings', () => {
const findWarnings = () => wrapper.findAll('[data-testid="ci-lint-warning"]');
const findWarningMessage = () => trimText(wrapper.findComponent(GlSprintf).text());
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays the warning alert', () => {
createComponent();
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 f40db50aab7..471b033913b 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
@@ -57,6 +57,7 @@ describe('Pipeline editor tabs component', () => {
isNewCiConfigFile: true,
showDrawer: false,
showJobAssistantDrawer: false,
+ showAiAssistantDrawer: false,
...props,
},
data() {
@@ -65,6 +66,7 @@ describe('Pipeline editor tabs component', () => {
};
},
provide: {
+ aiChatAvailable: false,
ciConfigPath: '/path/to/ci-config',
ciLintPath: mockCiLintPath,
currentBranch: 'main',
@@ -119,6 +121,7 @@ describe('Pipeline editor tabs component', () => {
});
afterEach(() => {
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
});
@@ -313,13 +316,13 @@ describe('Pipeline editor tabs component', () => {
createComponent();
});
- it('shows walkthrough popover', async () => {
+ it('shows walkthrough popover', () => {
expect(findWalkthroughPopover().exists()).toBe(true);
});
});
describe('when isNewCiConfigFile prop is false', () => {
- it('does not show walkthrough popover', async () => {
+ it('does not show walkthrough popover', () => {
createComponent({ props: { isNewCiConfigFile: false } });
expect(findWalkthroughPopover().exists()).toBe(false);
});
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
index 63ebfc0559d..3d84f06967a 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/file_tree_popover_spec.js
@@ -22,11 +22,10 @@ describe('FileTreePopover component', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('default', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({ stubs: { GlSprintf } });
});
@@ -46,7 +45,7 @@ describe('FileTreePopover component', () => {
});
describe('when popover has already been dismissed before', () => {
- it('does not render popover', async () => {
+ it('does not render popover', () => {
localStorage.setItem(FILE_TREE_POPOVER_DISMISSED_KEY, 'true');
createComponent();
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
index cf0b974081e..18eec48ad83 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/validate_pipeline_popover_spec.js
@@ -19,12 +19,8 @@ describe('ValidatePopover component', () => {
const findHelpLink = () => wrapper.findByTestId('help-link');
const findFeedbackLink = () => wrapper.findByTestId('feedback-link');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
stubs: { GlLink, GlSprintf },
});
diff --git a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
index ca6033f2ff5..37339b1c422 100644
--- a/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/popovers/walkthrough_popover_spec.js
@@ -12,17 +12,13 @@ describe('WalkthroughPopover component', () => {
return extendedWrapper(mountFn(WalkthroughPopover));
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('CTA button clicked', () => {
beforeEach(async () => {
wrapper = createComponent(mount);
await wrapper.findByTestId('ctaBtn').trigger('click');
});
- it('emits "walkthrough-popover-cta-clicked" event', async () => {
+ it('emits "walkthrough-popover-cta-clicked" event', () => {
expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toHaveLength(1);
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
index b22c98e5544..8b8dd4d22c2 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
@@ -4,10 +4,9 @@ import ConfirmDialog from '~/ci/pipeline_editor/components/ui/confirm_unsaved_ch
describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
let beforeUnloadEvent;
let setDialogContent;
- let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(ConfirmDialog, {
+ shallowMount(ConfirmDialog, {
propsData,
});
};
@@ -21,7 +20,6 @@ describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
afterEach(() => {
beforeUnloadEvent.preventDefault.mockRestore();
setDialogContent.mockRestore();
- wrapper.destroy();
});
it('shows confirmation dialog when there are unsaved changes', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
index a4e7abba7b0..f02b1f5efbc 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/editor_tab_spec.js
@@ -64,7 +64,7 @@ describe('~/ci/pipeline_editor/components/ui/editor_tab.vue', () => {
mockChildMounted = jest.fn();
});
- it('tabs are mounted lazily', async () => {
+ it('tabs are mounted lazily', () => {
createMockedWrapper();
expect(mockChildMounted).toHaveBeenCalledTimes(0);
@@ -192,7 +192,7 @@ describe('~/ci/pipeline_editor/components/ui/editor_tab.vue', () => {
createMockedWrapper();
});
- it('renders correct number of badges', async () => {
+ it('renders correct number of badges', () => {
expect(findBadges()).toHaveLength(1);
expect(findBadges().at(0).text()).toBe('NEW');
});
diff --git a/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
index 3c68f74af43..e636a89c6d9 100644
--- a/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/ui/pipeline_editor_empty_state_spec.js
@@ -23,10 +23,6 @@ describe('Pipeline editor empty state', () => {
const findConfirmButton = () => wrapper.findComponent(GlButton);
const findDescription = () => wrapper.findComponent(GlSprintf);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when project uses an external CI config', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
index ae25142b455..2349816fa86 100644
--- a/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/validate/ci_validate_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui';
+import { GlAlert, GlDisclosureDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui';
import { nextTick } from 'vue';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
@@ -90,7 +90,7 @@ describe('Pipeline Editor Validate Tab', () => {
const findHelpIcon = () => wrapper.findComponent(GlIcon);
const findIllustration = () => wrapper.findByRole('img');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findPipelineSource = () => wrapper.findComponent(GlDropdown);
+ const findPipelineSource = () => wrapper.findComponent(GlDisclosureDropdown);
const findPopover = () => wrapper.findComponent(GlPopover);
const findCiLintResults = () => wrapper.findComponent(CiLintResults);
const findResultsCta = () => wrapper.findByTestId('resimulate-pipeline-button');
@@ -99,10 +99,6 @@ describe('Pipeline Editor Validate Tab', () => {
mockBlobContentData = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('while initial CI content is loading', () => {
beforeEach(() => {
createComponent({ isBlobLoading: true });
@@ -122,7 +118,7 @@ describe('Pipeline Editor Validate Tab', () => {
it('renders disabled pipeline source dropdown', () => {
expect(findPipelineSource().exists()).toBe(true);
- expect(findPipelineSource().attributes('text')).toBe(i18n.pipelineSourceDefault);
+ expect(findPipelineSource().attributes('toggletext')).toBe(i18n.pipelineSourceDefault);
expect(findPipelineSource().props('disabled')).toBe(true);
});
diff --git a/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
index 6a6cc3a14de..893f6775ac5 100644
--- a/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
@@ -34,7 +34,7 @@ describe('~/ci/pipeline_editor/graphql/resolvers', () => {
});
/* eslint-disable no-underscore-dangle */
- it('lint data has correct type names', async () => {
+ it('lint data has correct type names', () => {
expect(result.__typename).toBe('CiLintContent');
expect(result.jobs[0].__typename).toBe('CiLintJob');
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 541123d7efc..865dd34fbfe 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -12,7 +12,7 @@ export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
export const mockIncludesHelpPagePath = '/-/includes/help';
export const mockLintHelpPagePath = '/-/lint-help';
-export const mockLintUnavailableHelpPagePath = '/-/pipeline-editor/troubleshoot';
+export const mockCiTroubleshootingPath = '/-/pipeline-editor/troubleshoot';
export const mockSimulatePipelineHelpPagePath = '/-/simulate-pipeline-help';
export const mockYmlHelpPagePath = '/-/yml-help';
export const mockCommitMessage = 'My commit message';
@@ -583,6 +583,36 @@ export const mockCommitCreateResponse = {
},
};
+export const mockRunnersTagsQueryResponse = {
+ data: {
+ runners: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Runner/1',
+ tagList: ['tag1', 'tag2'],
+ __typename: 'CiRunner',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/2',
+ tagList: ['tag2', 'tag3'],
+ __typename: 'CiRunner',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/3',
+ tagList: ['tag2', 'tag4'],
+ __typename: 'CiRunner',
+ },
+ {
+ id: 'gid://gitlab/Ci::Runner/4',
+ tagList: [],
+ __typename: 'CiRunner',
+ },
+ ],
+ __typename: 'CiRunnerConnection',
+ },
+ },
+};
+
export const mockCommitCreateResponseNewEtag = {
data: {
commitCreate: {
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index a103acb33bc..cc4a022c2df 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlButton, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -6,7 +6,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
-import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
+import { objectToQuery, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
@@ -96,7 +96,7 @@ describe('Pipeline editor app component', () => {
});
};
- const createComponentWithApollo = async ({
+ const createComponentWithApollo = ({
provide = {},
stubs = {},
withUndefinedBranch = false,
@@ -162,10 +162,6 @@ describe('Pipeline editor app component', () => {
mockPipelineQuery = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('displays a loading icon if the blob query is loading', () => {
createComponent({ blobLoading: true });
@@ -264,7 +260,7 @@ describe('Pipeline editor app component', () => {
expect(findAlert().exists()).toBe(false);
});
- it('ci config query is called with correct variables', async () => {
+ it('ci config query is called with correct variables', () => {
expect(mockCiConfigData).toHaveBeenCalledWith({
content: mockCiYml,
projectPath: mockProjectFullPath,
@@ -291,7 +287,7 @@ describe('Pipeline editor app component', () => {
.mockImplementation(jest.fn());
});
- it('shows an empty state and does not show editor home component', async () => {
+ it('shows an empty state and does not show editor home component', () => {
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
@@ -351,7 +347,9 @@ describe('Pipeline editor app component', () => {
});
it('shows that the lint service is down', () => {
- expect(findValidationSegment().text()).toContain(
+ const validationMessage = findValidationSegment().findComponent(GlSprintf);
+
+ expect(validationMessage.attributes('message')).toContain(
validationSegmenti18n.unavailableValidation,
);
});
@@ -436,7 +434,7 @@ describe('Pipeline editor app component', () => {
'merge_request[target_branch]': mockDefaultBranch,
});
- expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
+ expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
index 4f8f2112abe..576263d5418 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
@@ -41,6 +41,7 @@ describe('Pipeline editor home wrapper', () => {
...props,
},
provide: {
+ aiChatAvailable: false,
projectFullPath: '',
totalBranches: 19,
glFeatures: {
@@ -67,7 +68,6 @@ describe('Pipeline editor home wrapper', () => {
afterEach(() => {
localStorage.clear();
- wrapper.destroy();
});
describe('renders', () => {
diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
index 6f18899ebac..1d4ae33c667 100644
--- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
@@ -4,7 +4,7 @@ import { GlForm, GlDropdownItem, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -13,7 +13,7 @@ import {
HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import PipelineNewForm, {
POLLING_INTERVAL,
} from '~/ci/pipeline_new/components/pipeline_new_form.vue';
@@ -30,8 +30,8 @@ import {
mockQueryParams,
mockPostParams,
mockProjectId,
- mockRefs,
mockYamlVariables,
+ mockPipelineConfigButtonText,
} from '../mock_data';
Vue.use(VueApollo);
@@ -40,8 +40,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
-const projectRefsEndpoint = '/root/project/refs';
const pipelinesPath = '/root/project/-/pipelines';
+const pipelinesEditorPath = '/root/project/-/ci/editor';
const projectPath = '/root/project/-/pipelines/config_variables';
const newPipelinePostResponse = { id: 1 };
const defaultBranch = 'main';
@@ -65,6 +65,7 @@ describe('Pipeline New Form', () => {
wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown');
const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem);
const findErrorAlert = () => wrapper.findByTestId('run-pipeline-error-alert');
+ const findPipelineConfigButton = () => wrapper.findByTestId('ci-cd-pipeline-configuration');
const findWarningAlert = () => wrapper.findByTestId('run-pipeline-warning-alert');
const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf);
const findWarnings = () => wrapper.findAllByTestId('run-pipeline-warning');
@@ -88,24 +89,23 @@ describe('Pipeline New Form', () => {
const changeKeyInputValue = async (keyInputIndex, value) => {
const input = findKeyInputs().at(keyInputIndex);
- input.element.value = value;
- input.trigger('change');
+ input.vm.$emit('input', value);
+ input.vm.$emit('change');
await nextTick();
};
- const createComponentWithApollo = ({ method = shallowMountExtended, props = {} } = {}) => {
+ const createComponentWithApollo = ({ props = {} } = {}) => {
const handlers = [[ciConfigVariablesQuery, mockCiConfigVariables]];
mockApollo = createMockApollo(handlers, resolvers);
- wrapper = method(PipelineNewForm, {
+ wrapper = shallowMountExtended(PipelineNewForm, {
apolloProvider: mockApollo,
- provide: {
- projectRefsEndpoint,
- },
propsData: {
projectId: mockProjectId,
pipelinesPath,
+ pipelinesEditorPath,
+ canViewPipelineEditor: true,
projectPath,
defaultBranch,
refParam: defaultBranch,
@@ -119,7 +119,6 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mockCiConfigVariables = jest.fn();
- mock.onGet(projectRefsEndpoint).reply(HTTP_STATUS_OK, mockRefs);
dummySubmitEvent = {
preventDefault: jest.fn(),
@@ -128,17 +127,16 @@ describe('Pipeline New Form', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('Form', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
+ createComponentWithApollo({ props: mockQueryParams });
await waitForPromises();
});
- it('displays the correct values for the provided query params', async () => {
+ it('displays the correct values for the provided query params', () => {
expect(findVariableTypes().at(0).props('text')).toBe('Variable');
expect(findVariableTypes().at(1).props('text')).toBe('File');
expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' });
@@ -146,13 +144,13 @@ describe('Pipeline New Form', () => {
});
it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(0).element.value).toBe('test_var');
- expect(findValueInputs().at(0).element.value).toBe('test_var_val');
+ expect(findKeyInputs().at(0).attributes('value')).toBe('test_var');
+ expect(findValueInputs().at(0).attributes('value')).toBe('test_var_val');
});
- it('displays an empty variable for the user to fill out', async () => {
- expect(findKeyInputs().at(2).element.value).toBe('');
- expect(findValueInputs().at(2).element.value).toBe('');
+ it('displays an empty variable for the user to fill out', () => {
+ expect(findKeyInputs().at(2).attributes('value')).toBe('');
+ expect(findValueInputs().at(2).attributes('value')).toBe('');
expect(findVariableTypes().at(2).props('text')).toBe('Variable');
});
@@ -161,7 +159,7 @@ describe('Pipeline New Form', () => {
});
it('removes ci variable row on remove icon button click', async () => {
- findRemoveIcons().at(1).trigger('click');
+ findRemoveIcons().at(1).vm.$emit('click');
await nextTick();
@@ -170,24 +168,25 @@ describe('Pipeline New Form', () => {
it('creates blank variable on input change event', async () => {
const input = findKeyInputs().at(2);
- input.element.value = 'test_var_2';
- input.trigger('change');
+
+ input.vm.$emit('input', 'test_var_2');
+ input.vm.$emit('change');
await nextTick();
expect(findVariableRows()).toHaveLength(4);
- expect(findKeyInputs().at(3).element.value).toBe('');
- expect(findValueInputs().at(3).element.value).toBe('');
+ expect(findKeyInputs().at(3).attributes('value')).toBe('');
+ expect(findValueInputs().at(3).attributes('value')).toBe('');
});
});
describe('Pipeline creation', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
mock.onPost(pipelinesPath).reply(HTTP_STATUS_OK, newPipelinePostResponse);
});
- it('does not submit the native HTML form', async () => {
+ it('does not submit the native HTML form', () => {
createComponentWithApollo();
findForm().vm.$emit('submit', dummySubmitEvent);
@@ -213,7 +212,7 @@ describe('Pipeline New Form', () => {
await waitForPromises();
expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`);
- expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
+ expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); // eslint-disable-line import/no-deprecated
});
it('creates a pipeline with short ref and variables from the query params', async () => {
@@ -226,14 +225,14 @@ describe('Pipeline New Form', () => {
await waitForPromises();
expect(getFormPostParams()).toEqual(mockPostParams);
- expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`);
+ expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); // eslint-disable-line import/no-deprecated
});
});
describe('When the ref has been changed', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
});
@@ -247,12 +246,12 @@ describe('Pipeline New Form', () => {
await selectBranch('main');
- expect(findKeyInputs().at(0).element.value).toBe('build_var');
+ expect(findKeyInputs().at(0).attributes('value')).toBe('build_var');
expect(findVariableRows().length).toBe(2);
await selectBranch('branch-1');
- expect(findKeyInputs().at(0).element.value).toBe('deploy_var');
+ expect(findKeyInputs().at(0).attributes('value')).toBe('deploy_var');
expect(findVariableRows().length).toBe(2);
});
@@ -276,7 +275,7 @@ describe('Pipeline New Form', () => {
describe('When there are no variables in the API cache', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockNoCachedCiConfigVariablesResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
});
@@ -324,9 +323,9 @@ describe('Pipeline New Form', () => {
});
const testBehaviorWhenCacheIsPopulated = (queryResponse) => {
- beforeEach(async () => {
+ beforeEach(() => {
mockCiConfigVariables.mockResolvedValue(queryResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
});
it('does not poll for new values', async () => {
@@ -341,6 +340,9 @@ describe('Pipeline New Form', () => {
});
it('loading icon is shown when content is requested and hidden when received', async () => {
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ props: mockQueryParams });
+
expect(findLoadingIcon().exists()).toBe(true);
await waitForPromises();
@@ -354,11 +356,11 @@ describe('Pipeline New Form', () => {
it('displays an empty form', async () => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
- expect(findKeyInputs().at(0).element.value).toBe('');
- expect(findValueInputs().at(0).element.value).toBe('');
+ expect(findKeyInputs().at(0).attributes('value')).toBe('');
+ expect(findValueInputs().at(0).attributes('value')).toBe('');
expect(findVariableTypes().at(0).props('text')).toBe('Variable');
});
});
@@ -369,12 +371,12 @@ describe('Pipeline New Form', () => {
describe('with different predefined values', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
});
it('multi-line strings are added to the value field without removing line breaks', () => {
- expect(findValueInputs().at(1).element.value).toBe(mockYamlVariables[1].value);
+ expect(findValueInputs().at(1).attributes('value')).toBe(mockYamlVariables[1].value);
});
it('multiple predefined values are rendered as a dropdown', () => {
@@ -398,24 +400,24 @@ describe('Pipeline New Form', () => {
describe('with description', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
- createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
+ createComponentWithApollo({ props: mockQueryParams });
await waitForPromises();
});
- it('displays all the variables', async () => {
+ it('displays all the variables', () => {
expect(findVariableRows()).toHaveLength(6);
});
it('displays a variable from yml', () => {
- expect(findKeyInputs().at(0).element.value).toBe(mockYamlVariables[0].key);
- expect(findValueInputs().at(0).element.value).toBe(mockYamlVariables[0].value);
+ expect(findKeyInputs().at(0).attributes('value')).toBe(mockYamlVariables[0].key);
+ expect(findValueInputs().at(0).attributes('value')).toBe(mockYamlVariables[0].value);
});
it('displays a variable from provided query params', () => {
- expect(findKeyInputs().at(3).element.value).toBe(
+ expect(findKeyInputs().at(3).attributes('value')).toBe(
Object.keys(mockQueryParams.variableParams)[0],
);
- expect(findValueInputs().at(3).element.value).toBe(
+ expect(findValueInputs().at(3).attributes('value')).toBe(
Object.values(mockQueryParams.fileParams)[0],
);
});
@@ -425,7 +427,7 @@ describe('Pipeline New Form', () => {
});
it('removes the description when a variable key changes', async () => {
- findKeyInputs().at(0).element.value = 'yml_var_modified';
+ findKeyInputs().at(0).vm.$emit('input', 'yml_var_modified');
findKeyInputs().at(0).trigger('change');
await nextTick();
@@ -437,11 +439,11 @@ describe('Pipeline New Form', () => {
describe('without description', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponseWithoutDesc);
- createComponentWithApollo({ method: mountExtended });
+ createComponentWithApollo();
await waitForPromises();
});
- it('displays variables with description only', async () => {
+ it('displays variables with description only', () => {
expect(findVariableRows()).toHaveLength(2); // extra empty variable is added at the end
});
});
@@ -456,7 +458,7 @@ describe('Pipeline New Form', () => {
describe('when the refs cannot be loaded', () => {
beforeEach(() => {
mock
- .onGet(projectRefsEndpoint, { params: { search: '' } })
+ .onGet('/api/v4/projects/8/repository/branches', { params: { search: '' } })
.reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findRefsDropdown().vm.$emit('loadingError');
@@ -500,6 +502,17 @@ describe('Pipeline New Form', () => {
expect(findSubmitButton().props('disabled')).toBe(false);
});
+ it('shows pipeline configuration button for user who can view', () => {
+ expect(findPipelineConfigButton().exists()).toBe(true);
+ expect(findPipelineConfigButton().text()).toBe(mockPipelineConfigButtonText);
+ });
+
+ it('does not show pipeline configuration button for user who can not view', () => {
+ createComponentWithApollo({ props: { canViewPipelineEditor: false } });
+
+ expect(findPipelineConfigButton().exists()).toBe(false);
+ });
+
it('does not show the credit card validation required alert', () => {
expect(findCCAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
index cf8009e388f..01c7dd7eb84 100644
--- a/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
@@ -1,35 +1,22 @@
-import { GlListbox, GlListboxItem } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { shallowMount } from '@vue/test-utils';
+import RefSelector from '~/ref/components/ref_selector.vue';
import RefsDropdown from '~/ci/pipeline_new/components/refs_dropdown.vue';
+import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
-import { mockBranches, mockRefs, mockFilteredRefs, mockTags } from '../mock_data';
-
-const projectRefsEndpoint = '/root/project/refs';
+const projectId = '8';
const refShortName = 'main';
const refFullName = 'refs/heads/main';
-jest.mock('~/flash');
-
describe('Pipeline New Form', () => {
let wrapper;
- let mock;
- const findDropdown = () => wrapper.findComponent(GlListbox);
- const findRefsDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
- const findSearchBox = () => wrapper.findByTestId('listbox-search-input');
- const findListboxGroups = () => wrapper.findAll('ul[role="group"]');
+ const findRefSelector = () => wrapper.findComponent(RefSelector);
- const createComponent = (props = {}, mountFn = shallowMountExtended) => {
- wrapper = mountFn(RefsDropdown, {
- provide: {
- projectRefsEndpoint,
- },
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(RefsDropdown, {
propsData: {
+ projectId,
value: {
shortName: refShortName,
fullName: refFullName,
@@ -39,163 +26,54 @@ describe('Pipeline New Form', () => {
});
};
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockRefs);
- });
-
- beforeEach(() => {
- createComponent();
- });
-
- it('displays empty dropdown initially', () => {
- findDropdown().vm.$emit('shown');
-
- expect(findRefsDropdownItems()).toHaveLength(0);
- });
-
- it('does not make requests immediately', async () => {
- expect(mock.history.get).toHaveLength(0);
- });
-
describe('when user opens dropdown', () => {
- beforeEach(async () => {
- createComponent({}, mountExtended);
- findDropdown().vm.$emit('shown');
- await waitForPromises();
+ beforeEach(() => {
+ createComponent();
});
- it('requests unfiltered tags and branches', () => {
- expect(mock.history.get).toHaveLength(1);
- expect(mock.history.get[0].url).toBe(projectRefsEndpoint);
- expect(mock.history.get[0].params).toEqual({ search: '' });
+ it('has default selected branch', () => {
+ expect(findRefSelector().props('value')).toBe('main');
});
- it('displays dropdown with branches and tags', () => {
- const refLength = mockRefs.Tags.length + mockRefs.Branches.length;
- expect(findRefsDropdownItems()).toHaveLength(refLength);
- });
-
- it('displays the names of refs', () => {
- // Branches
- expect(findRefsDropdownItems().at(0).text()).toBe(mockRefs.Branches[0]);
-
- // Tags (appear after branches)
- const firstTag = mockRefs.Branches.length;
- expect(findRefsDropdownItems().at(firstTag).text()).toBe(mockRefs.Tags[0]);
- });
-
- it('when user shows dropdown a second time, only one request is done', () => {
- expect(mock.history.get).toHaveLength(1);
+ it('has ref selector for branches and tags', () => {
+ expect(findRefSelector().props('enabledRefTypes')).toEqual([
+ REF_TYPE_BRANCHES,
+ REF_TYPE_TAGS,
+ ]);
});
describe('when user selects a value', () => {
- const selectedIndex = 1;
-
- beforeEach(async () => {
- findRefsDropdownItems().at(selectedIndex).vm.$emit('select', 'refs/heads/branch-1');
- await waitForPromises();
- });
+ const fullName = `refs/heads/conflict-contains-conflict-markers`;
it('component emits @input', () => {
+ findRefSelector().vm.$emit('input', fullName);
+
const inputs = wrapper.emitted('input');
expect(inputs).toHaveLength(1);
- expect(inputs[0]).toEqual([{ shortName: 'branch-1', fullName: 'refs/heads/branch-1' }]);
- });
- });
-
- describe('when user types searches for a tag', () => {
- const mockSearchTerm = 'my-search';
-
- beforeEach(async () => {
- mock
- .onGet(projectRefsEndpoint, { params: { search: mockSearchTerm } })
- .reply(HTTP_STATUS_OK, mockFilteredRefs);
-
- await findSearchBox().vm.$emit('input', mockSearchTerm);
- await waitForPromises();
- });
-
- it('requests filtered tags and branches', async () => {
- expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toEqual({
- search: mockSearchTerm,
- });
- });
-
- it('displays dropdown with branches and tags', async () => {
- const filteredRefLength = mockFilteredRefs.Tags.length + mockFilteredRefs.Branches.length;
-
- expect(findRefsDropdownItems()).toHaveLength(filteredRefLength);
+ expect(inputs[0]).toEqual([
+ {
+ shortName: 'conflict-contains-conflict-markers',
+ fullName: 'refs/heads/conflict-contains-conflict-markers',
+ },
+ ]);
});
});
});
describe('when user has selected a value', () => {
- const selectedIndex = 1;
- const mockShortName = mockRefs.Branches[selectedIndex];
+ const mockShortName = 'conflict-contains-conflict-markers';
const mockFullName = `refs/heads/${mockShortName}`;
- beforeEach(async () => {
- mock
- .onGet(projectRefsEndpoint, {
- params: { ref: mockFullName },
- })
- .reply(HTTP_STATUS_OK, mockRefs);
-
- createComponent(
- {
- value: {
- shortName: mockShortName,
- fullName: mockFullName,
- },
- },
- mountExtended,
- );
- findDropdown().vm.$emit('shown');
- await waitForPromises();
- });
-
it('branch is checked', () => {
- expect(findRefsDropdownItems().at(selectedIndex).props('isSelected')).toBe(true);
- });
- });
-
- describe('when server returns an error', () => {
- beforeEach(async () => {
- mock
- .onGet(projectRefsEndpoint, { params: { search: '' } })
- .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- findDropdown().vm.$emit('shown');
- await waitForPromises();
- });
+ createComponent({
+ value: {
+ shortName: mockShortName,
+ fullName: mockFullName,
+ },
+ });
- it('loading error event is emitted', () => {
- expect(wrapper.emitted('loadingError')).toHaveLength(1);
- expect(wrapper.emitted('loadingError')[0]).toEqual([expect.any(Error)]);
+ expect(findRefSelector().props('value')).toBe(mockShortName);
});
});
-
- describe('should display branches and tags based on its length', () => {
- it.each`
- mockData | expectedGroupLength | expectedListboxItemsLength
- ${{ ...mockBranches, Tags: [] }} | ${1} | ${mockBranches.Branches.length}
- ${{ Branches: [], ...mockTags }} | ${1} | ${mockTags.Tags.length}
- ${{ ...mockRefs }} | ${2} | ${mockBranches.Branches.length + mockTags.Tags.length}
- ${{ Branches: undefined, Tags: undefined }} | ${0} | ${0}
- `(
- 'should render branches and tags based on presence',
- async ({ mockData, expectedGroupLength, expectedListboxItemsLength }) => {
- mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockData);
- createComponent({}, mountExtended);
- findDropdown().vm.$emit('shown');
- await waitForPromises();
-
- expect(findListboxGroups()).toHaveLength(expectedGroupLength);
- expect(findRefsDropdownItems()).toHaveLength(expectedListboxItemsLength);
- },
- );
- });
});
diff --git a/spec/frontend/ci/pipeline_new/mock_data.js b/spec/frontend/ci/pipeline_new/mock_data.js
index 5b935c0c819..76a88f63298 100644
--- a/spec/frontend/ci/pipeline_new/mock_data.js
+++ b/spec/frontend/ci/pipeline_new/mock_data.js
@@ -1,16 +1,3 @@
-export const mockBranches = {
- Branches: ['main', 'branch-1', 'branch-2'],
-};
-
-export const mockTags = {
- Tags: ['1.0.0', '1.1.0', '1.2.0'],
-};
-
-export const mockRefs = {
- ...mockBranches,
- ...mockTags,
-};
-
export const mockFilteredRefs = {
Branches: ['branch-1'],
Tags: ['1.0.0', '1.1.0'],
@@ -133,3 +120,5 @@ export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQue
mockYamlVariablesWithoutDesc,
);
export const mockNoCachedCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(null);
+
+export const mockPipelineConfigButtonText = 'Go to the pipeline editor';
diff --git a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
index ba948f12b33..e48f556c246 100644
--- a/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/delete_pipeline_schedule_modal_spec.js
@@ -20,17 +20,13 @@ describe('Delete pipeline schedule modal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('emits the deleteSchedule event', async () => {
+ it('emits the deleteSchedule event', () => {
findModal().vm.$emit('primary');
expect(wrapper.emitted()).toEqual({ deleteSchedule: [[]] });
});
- it('emits the hideModal event', async () => {
+ it('emits the hideModal event', () => {
findModal().vm.$emit('hide');
expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
index 611993556e3..50008cedd9c 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -16,6 +16,7 @@ import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/g
import {
mockGetPipelineSchedulesGraphQLResponse,
mockPipelineScheduleNodes,
+ mockPipelineScheduleCurrentUser,
deleteMutationResponse,
playMutationResponse,
takeOwnershipMutationResponse,
@@ -79,10 +80,6 @@ describe('Pipeline schedules app', () => {
const findSchedulesCharacteristics = () =>
wrapper.findByTestId('pipeline-schedules-characteristics');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -115,6 +112,7 @@ describe('Pipeline schedules app', () => {
await waitForPromises();
expect(findTable().props('schedules')).toEqual(mockPipelineScheduleNodes);
+ expect(findTable().props('currentUser')).toEqual(mockPipelineScheduleCurrentUser);
});
it('shows query error alert', async () => {
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
index 6fb6a8bc33b..be0052fc7cf 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -3,6 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
import {
mockPipelineScheduleNodes,
+ mockPipelineScheduleCurrentUser,
mockPipelineScheduleAsGuestNodes,
mockTakeOwnershipNodes,
} from '../../../mock_data';
@@ -12,6 +13,7 @@ describe('Pipeline schedule actions', () => {
const defaultProps = {
schedule: mockPipelineScheduleNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
};
const createComponent = (props = defaultProps) => {
@@ -27,18 +29,17 @@ describe('Pipeline schedule actions', () => {
const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn');
const findPlayScheduleBtn = () => wrapper.findByTestId('play-pipeline-schedule-btn');
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays action buttons', () => {
+ it('displays buttons when user is the owner of schedule and has adminPipelineSchedule permissions', () => {
createComponent();
expect(findAllButtons()).toHaveLength(3);
});
- it('does not display action buttons', () => {
- createComponent({ schedule: mockPipelineScheduleAsGuestNodes[0] });
+ it('does not display action buttons when user is not owner and does not have adminPipelineSchedule permission', () => {
+ createComponent({
+ schedule: mockPipelineScheduleAsGuestNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
+ });
expect(findAllButtons()).toHaveLength(0);
});
@@ -54,7 +55,10 @@ describe('Pipeline schedule actions', () => {
});
it('take ownership button emits showTakeOwnershipModal event and schedule id', () => {
- createComponent({ schedule: mockTakeOwnershipNodes[0] });
+ createComponent({
+ schedule: mockTakeOwnershipNodes[0],
+ currentUser: mockPipelineScheduleCurrentUser,
+ });
findTakeOwnershipBtn().vm.$emit('click');
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index 0821c59c8a0..ae069145292 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -21,10 +21,6 @@ describe('Pipeline schedule last pipeline', () => {
const findCIBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text');
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays pipeline status', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
index 1c06c411097..3bdbb371ddc 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run_spec.js
@@ -21,10 +21,6 @@ describe('Pipeline schedule next run', () => {
const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
const findInactive = () => wrapper.findByTestId('pipeline-schedule-inactive');
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays time ago', () => {
createComponent();
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
index 6c1991cb4ac..849bef80f42 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner_spec.js
@@ -25,10 +25,6 @@ describe('Pipeline schedule owner', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays avatar', () => {
expect(findAvatar().exists()).toBe(true);
expect(findAvatar().props('src')).toBe(defaultProps.schedule.owner.avatarUrl);
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 f531f04a736..5cc3829efbd 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
@@ -25,10 +25,6 @@ describe('Pipeline schedule target', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays icon', () => {
expect(findIcon().exists()).toBe(true);
expect(findIcon().props('name')).toBe('fork');
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
index 316b3bcf926..e488a36f3dc 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/pipeline_schedules_table_spec.js
@@ -1,13 +1,14 @@
import { GlTableLite } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
-import { mockPipelineScheduleNodes } from '../../mock_data';
+import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../../mock_data';
describe('Pipeline schedules table', () => {
let wrapper;
const defaultProps = {
schedules: mockPipelineScheduleNodes,
+ currentUser: mockPipelineScheduleCurrentUser,
};
const createComponent = (props = defaultProps) => {
@@ -25,10 +26,6 @@ describe('Pipeline schedules table', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays table', () => {
expect(findTable().exists()).toBe(true);
});
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
index 7e6d4ec4bf8..e4ff9a0545b 100644
--- 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
@@ -25,14 +25,12 @@ describe('Take ownership modal', () => {
const actionPrimary = findModal().props('actionPrimary');
expect(actionPrimary.attributes).toEqual(
- expect.objectContaining([
- {
- category: 'primary',
- variant: 'confirm',
- href: url,
- 'data-method': 'post',
- },
- ]),
+ expect.objectContaining({
+ category: 'primary',
+ variant: 'confirm',
+ href: url,
+ 'data-method': 'post',
+ }),
);
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
index e3965d13c19..7cc254b7653 100644
--- a/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/take_ownership_modal_spec.js
@@ -26,13 +26,13 @@ describe('Take ownership modal', () => {
);
});
- it('emits the takeOwnership event', async () => {
+ it('emits the takeOwnership event', () => {
findModal().vm.$emit('primary');
expect(wrapper.emitted()).toEqual({ takeOwnership: [[]] });
});
- it('emits the hideModal event', async () => {
+ it('emits the hideModal event', () => {
findModal().vm.$emit('hide');
expect(wrapper.emitted()).toEqual({ hideModal: [[]] });
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 2826c054249..1485f6beea4 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -5,6 +5,7 @@ import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/
const {
data: {
+ currentUser,
project: {
pipelineSchedules: { nodes },
},
@@ -28,6 +29,7 @@ const {
} = mockGetPipelineSchedulesTakeOwnershipGraphQLResponse;
export const mockPipelineScheduleNodes = nodes;
+export const mockPipelineScheduleCurrentUser = currentUser;
export const mockPipelineScheduleAsGuestNodes = guestNodes;
diff --git a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
index 90ca2a07266..847862be183 100644
--- a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -30,22 +30,17 @@ describe('code quality issue body issue body', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('severity rating', () => {
it.each`
severity | iconClass | iconName
${'INFO'} | ${'gl-text-blue-400'} | ${'severity-info'}
- ${'MINOR'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'MINOR'} | ${'gl-text-orange-300'} | ${'severity-low'}
${'CRITICAL'} | ${'gl-text-red-600'} | ${'severity-high'}
${'BLOCKER'} | ${'gl-text-red-800'} | ${'severity-critical'}
${'UNKNOWN'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
${'INVALID'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
${'info'} | ${'gl-text-blue-400'} | ${'severity-info'}
- ${'minor'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'minor'} | ${'gl-text-orange-300'} | ${'severity-low'}
${'major'} | ${'gl-text-orange-400'} | ${'severity-medium'}
${'critical'} | ${'gl-text-red-600'} | ${'severity-high'}
${'blocker'} | ${'gl-text-red-800'} | ${'severity-critical'}
diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
index 3e4adfc7794..8beec220802 100644
--- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
+++ b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
@@ -15,10 +15,6 @@ describe('Grouped Issues List', () => {
const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a smart virtual list with the correct props', () => {
createComponent({
propsData: {
diff --git a/spec/frontend/ci/reports/components/issue_status_icon_spec.js b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
index fb13d4407e2..82b655dd598 100644
--- a/spec/frontend/ci/reports/components/issue_status_icon_spec.js
+++ b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
@@ -13,11 +13,6 @@ describe('IssueStatusIcon', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each([STATUS_SUCCESS, STATUS_NEUTRAL, STATUS_FAILED])(
'renders "%s" state correctly',
(status) => {
diff --git a/spec/frontend/ci/reports/components/report_link_spec.js b/spec/frontend/ci/reports/components/report_link_spec.js
index ba541ba0303..4a97afd77df 100644
--- a/spec/frontend/ci/reports/components/report_link_spec.js
+++ b/spec/frontend/ci/reports/components/report_link_spec.js
@@ -4,10 +4,6 @@ import ReportLink from '~/ci/reports/components/report_link.vue';
describe('app/assets/javascripts/ci/reports/components/report_link.vue', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const defaultProps = {
issue: {},
};
diff --git a/spec/frontend/ci/reports/components/report_section_spec.js b/spec/frontend/ci/reports/components/report_section_spec.js
index f032b210184..f4012fe0215 100644
--- a/spec/frontend/ci/reports/components/report_section_spec.js
+++ b/spec/frontend/ci/reports/components/report_section_spec.js
@@ -49,10 +49,6 @@ describe('ReportSection component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('isCollapsible', () => {
const testMatrix = [
diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js
index fb2ae5371d5..b1ae9e26b5b 100644
--- a/spec/frontend/ci/reports/components/summary_row_spec.js
+++ b/spec/frontend/ci/reports/components/summary_row_spec.js
@@ -31,11 +31,6 @@ describe('Summary row', () => {
const findStatusIcon = () => wrapper.findByTestId('summary-row-icon');
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders provided summary', () => {
createComponent();
expect(findSummary().text()).toContain(summary);
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
index edf3d1706cc..4c56dd74f1a 100644
--- a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -1,40 +1,47 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
-import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
+import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
-import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
-import { DEFAULT_PLATFORM } from '~/ci/runner/constants';
+import {
+ PARAM_KEY_PLATFORM,
+ INSTANCE_TYPE,
+ DEFAULT_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { runnerCreateResult } from '../mock_data';
-const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
-Vue.use(VueApollo);
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
describe('AdminNewRunnerApp', () => {
let wrapper;
- const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link');
- const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
- const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+ const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
+ const findRegistrationCompatibilityAlert = () =>
+ wrapper.findComponent(RegistrationCompatibilityAlert);
+ const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
- wrapper = mountFn(AdminNewRunnerApp, {
- propsData: {
- legacyRegistrationToken: mockLegacyRegistrationToken,
- ...props,
- },
- directives: {
- GlModal: createMockDirective(),
- },
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AdminNewRunnerApp, {
stubs: {
GlSprintf,
},
- ...options,
});
};
@@ -42,39 +49,71 @@ describe('AdminNewRunnerApp', () => {
createComponent();
});
- describe('Shows legacy modal', () => {
- it('passes legacy registration to modal', () => {
- expect(findRunnerInstructionsModal().props('registrationToken')).toEqual(
- mockLegacyRegistrationToken,
- );
- });
+ it('shows a registration feedback banner', () => {
+ expect(findRegistrationFeedbackBanner().exists()).toBe(true);
+ });
- it('opens a modal with the legacy instructions', () => {
- const modalId = getBinding(findLegacyInstructionsLink().element, 'gl-modal').value;
+ it('shows a registration compatibility alert', () => {
+ expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(INSTANCE_TYPE);
+ });
- expect(findRunnerInstructionsModal().props('modalId')).toBe(modalId);
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
});
});
- describe('New runner form fields', () => {
- describe('Platform', () => {
- it('shows the platforms radio group', () => {
- expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: INSTANCE_TYPE,
+ groupId: null,
+ projectId: null,
});
});
- describe('Runner', () => {
- it('shows the runners fields', () => {
- expect(findRunnerFormFields().props('value')).toEqual({
- accessLevel: 'NOT_PROTECTED',
- paused: false,
- description: '',
- maintenanceNote: '',
- maximumTimeout: ' ',
- runUntagged: false,
- tagList: '',
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
});
});
+
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
+
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
});
});
});
diff --git a/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
new file mode 100644
index 00000000000..60244ba5bc2
--- /dev/null
+++ b/spec/frontend/ci/runner/admin_register_runner/admin_register_runner_app_spec.js
@@ -0,0 +1,122 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import { updateHistory } from '~/lib/utils/url_utility';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import AdminRegisterRunnerApp from '~/ci/runner/admin_register_runner/admin_register_runner_app.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import { runnerForRegistration } from '../mock_data';
+
+const mockRunnerId = runnerForRegistration.data.runner.id;
+const mockRunnersPath = '/admin/runners';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+describe('AdminRegisterRunnerApp', () => {
+ let wrapper;
+
+ const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
+ const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AdminRegisterRunnerApp, {
+ propsData: {
+ runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
+ },
+ });
+ };
+
+ describe('When showing runner details', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when runner token is available', () => {
+ it('shows registration instructions', () => {
+ expect(findRegistrationInstructions().props()).toEqual({
+ platform: DEFAULT_PLATFORM,
+ runnerId: mockRunnerId,
+ });
+ });
+
+ it('configures platform drawer', () => {
+ expect(findPlatformsDrawer().props()).toEqual({
+ open: false,
+ platform: DEFAULT_PLATFORM,
+ });
+ });
+
+ it('shows runner list button', () => {
+ expect(findBtn().attributes('href')).toBe(mockRunnersPath);
+ expect(findBtn().props('variant')).toBe('confirm');
+ });
+ });
+ });
+
+ describe('When another platform has been selected', () => {
+ beforeEach(() => {
+ setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
+
+ createComponent();
+ });
+
+ it('shows registration instructions for the platform', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+
+ describe('When opening install instructions', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+ });
+
+ it('opens platform drawer', () => {
+ expect(findPlatformsDrawer().props('open')).toBe(true);
+ });
+
+ it('closes platform drawer', async () => {
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ it('closes platform drawer from drawer', async () => {
+ findPlatformsDrawer().vm.$emit('close');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ describe('when selecting a platform', () => {
+ beforeEach(async () => {
+ findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
+ await nextTick();
+ });
+
+ it('updates the url', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
+ });
+ });
+
+ it('updates the registration instructions', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index ed4f43c12d8..9787b1ef83f 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -4,8 +4,8 @@ import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -72,7 +72,6 @@ describe('AdminRunnerShowApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
describe('When showing runner details', () => {
@@ -82,7 +81,7 @@ describe('AdminRunnerShowApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('expect GraphQL ID to be requested', async () => {
+ it('expect GraphQL ID to be requested', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
@@ -90,7 +89,7 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays the runner edit and pause buttons', async () => {
+ it('displays the runner edit and pause buttons', () => {
expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
@@ -100,7 +99,7 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerDetailsTabs().props('runner')).toEqual(mockRunner);
});
- it('shows basic runner details', async () => {
+ it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
Version 1.0.0
@@ -181,7 +180,7 @@ describe('AdminRunnerShowApp', () => {
message: 'Runner deleted',
variant: VARIANT_SUCCESS,
});
- expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath);
+ expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 7fc240e520b..c3d33c88422 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -9,7 +9,7 @@ import {
mountExtended,
} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -57,20 +57,18 @@ import {
allRunnersDataPaginated,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ mockRegistrationToken,
newRunnerPath,
emptyPageInfo,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
} from '../mock_data';
-const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
const mockRunners = allRunnersData.data.runners.nodes;
const mockRunnersCount = runnersCountData.data.runners.count;
const mockRunnersHandler = jest.fn();
const mockRunnersCountHandler = jest.fn();
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -122,8 +120,6 @@ describe('AdminRunnersApp', () => {
localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
...provide,
},
...options,
@@ -143,7 +139,6 @@ describe('AdminRunnersApp', () => {
mockRunnersHandler.mockReset();
mockRunnersCountHandler.mockReset();
showToast.mockReset();
- wrapper.destroy();
});
it('shows the runner setup instructions', () => {
@@ -209,13 +204,13 @@ describe('AdminRunnersApp', () => {
it('runner item links to the runner admin page', async () => {
await createComponent({ mountFn: mountExtended });
- const { id, shortSha } = mockRunners[0];
+ const { id, shortSha, adminUrl } = mockRunners[0];
const numericId = getIdFromGraphQLId(id);
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').findComponent(GlLink);
expect(runnerLink.text()).toBe(`#${numericId} (${shortSha})`);
- expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${numericId}`);
+ expect(runnerLink.attributes('href')).toBe(adminUrl);
});
it('renders runner actions for each runner', async () => {
@@ -265,7 +260,7 @@ describe('AdminRunnersApp', () => {
});
describe('Single runner row', () => {
- const { id: graphqlId, shortSha } = mockRunners[0];
+ const { id: graphqlId, shortSha, adminUrl } = mockRunners[0];
const id = getIdFromGraphQLId(graphqlId);
beforeEach(async () => {
@@ -274,11 +269,11 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('Links to the runner page', async () => {
+ it('Links to the runner page', () => {
const runnerLink = wrapper.find('tr [data-testid="td-summary"]').findComponent(GlLink);
expect(runnerLink.text()).toBe(`#${id} (${shortSha})`);
- expect(runnerLink.attributes('href')).toBe(`http://localhost/admin/runners/${id}`);
+ expect(runnerLink.attributes('href')).toBe(adminUrl);
});
it('Shows job status and links to jobs', () => {
@@ -287,13 +282,10 @@ describe('AdminRunnersApp', () => {
.findComponent(RunnerJobStatusBadge);
expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
-
- const badgeHref = new URL(badge.attributes('href'));
- expect(badgeHref.pathname).toBe(`/admin/runners/${id}`);
- expect(badgeHref.hash).toBe(`#${JOBS_ROUTE_PATH}`);
+ expect(badge.attributes('href')).toBe(`${adminUrl}#${JOBS_ROUTE_PATH}`);
});
- it('When runner is paused or unpaused, some data is refetched', async () => {
+ it('When runner is paused or unpaused, some data is refetched', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('toggledPaused');
@@ -302,7 +294,7 @@ describe('AdminRunnersApp', () => {
expect(showToast).toHaveBeenCalledTimes(0);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ it('When runner is deleted, data is refetched and a toast message is shown', () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
@@ -325,7 +317,7 @@ describe('AdminRunnersApp', () => {
{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } },
{ type: PARAM_KEY_PAUSED, value: { data: 'true', operator: '=' } },
],
- sort: 'CREATED_DESC',
+ sort: DEFAULT_SORT,
pagination: {},
});
});
@@ -392,7 +384,7 @@ describe('AdminRunnersApp', () => {
it('when runners have not loaded, shows a loading state', () => {
createComponent();
expect(findRunnerList().props('loading')).toBe(true);
- expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findRunnerPagination().attributes('disabled')).toBeDefined();
});
describe('Bulk delete', () => {
@@ -411,7 +403,7 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('count data is refetched', async () => {
+ it('count data is refetched', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
@@ -419,7 +411,7 @@ describe('AdminRunnersApp', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES * 2);
});
- it('toast is shown', async () => {
+ it('toast is shown', () => {
expect(showToast).toHaveBeenCalledTimes(0);
findRunnerList().vm.$emit('deleted', { message: 'Runners deleted' });
@@ -452,9 +444,7 @@ describe('AdminRunnersApp', () => {
expect(findRunnerListEmptyState().props()).toEqual({
newRunnerPath,
isSearchFiltered: false,
- filteredSvgPath: emptyStateFilteredSvgPath,
registrationToken: mockRegistrationToken,
- svgPath: emptyStateSvgPath,
});
});
@@ -481,11 +471,11 @@ describe('AdminRunnersApp', () => {
await createComponent();
});
- it('error is shown to the user', async () => {
+ it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
- it('error is reported to sentry', async () => {
+ it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
component: 'AdminRunnersApp',
diff --git a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
index 82e262d1b73..8ac0c5a61f8 100644
--- a/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_actions_cell_spec.js
@@ -31,10 +31,6 @@ describe('RunnerActionsCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Edit Action', () => {
it('Displays the runner edit link with the correct href', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
index 3097e43e583..03f1ace3897 100644
--- a/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_owner_cell_spec.js
@@ -16,7 +16,7 @@ describe('RunnerOwnerCell', () => {
const createComponent = ({ runner } = {}) => {
wrapper = shallowMount(RunnerOwnerCell, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
runner,
@@ -24,10 +24,6 @@ describe('RunnerOwnerCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When its an instance runner', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
index 1ff60ff1a9d..c435dd57de2 100644
--- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import RunnerStatusCell from '~/ci/runner/components/cells/runner_status_cell.vue';
import RunnerStatusBadge from '~/ci/runner/components/runner_status_badge.vue';
@@ -20,7 +20,7 @@ describe('RunnerStatusCell', () => {
const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
const createComponent = ({ runner = {}, ...options } = {}) => {
- wrapper = mount(RunnerStatusCell, {
+ wrapper = shallowMount(RunnerStatusCell, {
propsData: {
runner: {
runnerType: INSTANCE_TYPE,
@@ -30,14 +30,14 @@ describe('RunnerStatusCell', () => {
...runner,
},
},
+ stubs: {
+ RunnerStatusBadge,
+ RunnerPausedBadge,
+ },
...options,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays online status', () => {
createComponent();
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 1711df42491..64e9c11a584 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -1,5 +1,6 @@
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
@@ -11,11 +12,13 @@ import {
I18N_INSTANCE_TYPE,
PROJECT_TYPE,
I18N_NO_DESCRIPTION,
+ I18N_CREATED_AT_LABEL,
+ I18N_CREATED_AT_BY_LABEL,
} from '~/ci/runner/constants';
-import { allRunnersData } from '../../mock_data';
+import { allRunnersWithCreatorData } from '../../mock_data';
-const mockRunner = allRunnersData.data.runners.nodes[0];
+const mockRunner = allRunnersWithCreatorData.data.runners.nodes[0];
describe('RunnerTypeCell', () => {
let wrapper;
@@ -45,10 +48,6 @@ describe('RunnerTypeCell', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays the runner name as id and short token', () => {
expect(wrapper.text()).toContain(
`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
@@ -83,14 +82,15 @@ describe('RunnerTypeCell', () => {
it('Displays the runner description', () => {
expect(wrapper.text()).toContain(mockRunner.description);
+ expect(wrapper.findByText(I18N_NO_DESCRIPTION).exists()).toBe(false);
});
- it('Displays the no runner description', () => {
+ it('Displays "No description" for missing runner description', () => {
createComponent({
description: null,
});
- expect(wrapper.text()).toContain(I18N_NO_DESCRIPTION);
+ expect(wrapper.findByText(I18N_NO_DESCRIPTION).classes()).toContain('gl-text-secondary');
});
it('Displays last contact', () => {
@@ -146,10 +146,42 @@ describe('RunnerTypeCell', () => {
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
});
- it('Displays created at', () => {
- expect(findRunnerSummaryField('calendar').findComponent(TimeAgo).props('time')).toBe(
- mockRunner.createdAt,
- );
+ describe('Displays creation info', () => {
+ const findCreatedTime = () => findRunnerSummaryField('calendar').findComponent(TimeAgo);
+
+ it('Displays created at ...', () => {
+ createComponent({
+ createdBy: null,
+ });
+
+ expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
+ sprintf(I18N_CREATED_AT_LABEL, {
+ timeAgo: findCreatedTime().text(),
+ }),
+ );
+ expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('Displays created at ... by ...', () => {
+ expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
+ sprintf(I18N_CREATED_AT_BY_LABEL, {
+ timeAgo: findCreatedTime().text(),
+ avatar: mockRunner.createdBy.username,
+ }),
+ );
+ expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
+ });
+
+ it('Displays creator avatar', () => {
+ const { name, avatarUrl, webUrl, username } = mockRunner.createdBy;
+
+ expect(wrapper.findComponent(UserAvatarLink).props()).toMatchObject({
+ imgAlt: expect.stringContaining(name),
+ imgSrc: avatarUrl,
+ linkHref: webUrl,
+ tooltipText: username,
+ });
+ });
});
it('Displays tag list', () => {
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
index f536e0dcbcf..7748890cf77 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_field_spec.js
@@ -17,16 +17,12 @@ describe('RunnerSummaryField', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
...options,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows content in slot', () => {
createComponent({
slots: { default: 'content' },
diff --git a/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
new file mode 100644
index 00000000000..5eb7ffaacd6
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/__snapshots__/utils_spec.js.snap
@@ -0,0 +1,201 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`registration utils for "linux" platform commandPrompt is correct 1`] = `"$"`;
+
+exports[`registration utils for "linux" platform installScript is correct for "386" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "amd64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "arm" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform installScript is correct for "arm64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# Create a GitLab Runner user
+sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
+
+# Install and run as a service
+sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
+sudo gitlab-runner start"
+`;
+
+exports[`registration utils for "linux" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "386",
+ "arm",
+ "arm64",
+]
+`;
+
+exports[`registration utils for "linux" platform registerCommand is correct 1`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+ " --token MOCK_AUTHENTICATION_TOKEN",
+]
+`;
+
+exports[`registration utils for "linux" platform registerCommand is correct 2`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "linux" platform runCommand is correct 1`] = `"gitlab-runner run"`;
+
+exports[`registration utils for "osx" platform commandPrompt is correct 1`] = `"$"`;
+
+exports[`registration utils for "osx" platform installScript is correct for "amd64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# The rest of the commands execute as the user who will run the runner
+# Register the runner (steps below), then run
+cd ~
+gitlab-runner install
+gitlab-runner start"
+`;
+
+exports[`registration utils for "osx" platform installScript is correct for "arm64" architecture 1`] = `
+"# Download the binary for your system
+sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64
+
+# Give it permission to execute
+sudo chmod +x /usr/local/bin/gitlab-runner
+
+# The rest of the commands execute as the user who will run the runner
+# Register the runner (steps below), then run
+cd ~
+gitlab-runner install
+gitlab-runner start"
+`;
+
+exports[`registration utils for "osx" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "arm64",
+]
+`;
+
+exports[`registration utils for "osx" platform registerCommand is correct 1`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+ " --token MOCK_AUTHENTICATION_TOKEN",
+]
+`;
+
+exports[`registration utils for "osx" platform registerCommand is correct 2`] = `
+Array [
+ "gitlab-runner register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "osx" platform runCommand is correct 1`] = `"gitlab-runner run"`;
+
+exports[`registration utils for "windows" platform commandPrompt is correct 1`] = `">"`;
+
+exports[`registration utils for "windows" platform installScript is correct for "386" architecture 1`] = `
+"# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator
+# Create a folder somewhere on your system, for example: C:\\\\GitLab-Runner
+New-Item -Path 'C:\\\\GitLab-Runner' -ItemType Directory
+
+# Change to the folder
+cd 'C:\\\\GitLab-Runner'
+
+# Download binary
+Invoke-WebRequest -Uri \\"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe\\" -OutFile \\"gitlab-runner.exe\\"
+
+# Register the runner (steps below), then run
+.\\\\gitlab-runner.exe install
+.\\\\gitlab-runner.exe start"
+`;
+
+exports[`registration utils for "windows" platform installScript is correct for "amd64" architecture 1`] = `
+"# Run PowerShell: https://docs.microsoft.com/en-us/powershell/scripting/windows-powershell/starting-windows-powershell?view=powershell-7#with-administrative-privileges-run-as-administrator
+# Create a folder somewhere on your system, for example: C:\\\\GitLab-Runner
+New-Item -Path 'C:\\\\GitLab-Runner' -ItemType Directory
+
+# Change to the folder
+cd 'C:\\\\GitLab-Runner'
+
+# Download binary
+Invoke-WebRequest -Uri \\"https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe\\" -OutFile \\"gitlab-runner.exe\\"
+
+# Register the runner (steps below), then run
+.\\\\gitlab-runner.exe install
+.\\\\gitlab-runner.exe start"
+`;
+
+exports[`registration utils for "windows" platform platformArchitectures returns correct list of architectures 1`] = `
+Array [
+ "amd64",
+ "386",
+]
+`;
+
+exports[`registration utils for "windows" platform registerCommand is correct 1`] = `
+Array [
+ ".\\\\gitlab-runner.exe register",
+ " --url http://test.host",
+ " --token MOCK_AUTHENTICATION_TOKEN",
+]
+`;
+
+exports[`registration utils for "windows" platform registerCommand is correct 2`] = `
+Array [
+ ".\\\\gitlab-runner.exe register",
+ " --url http://test.host",
+]
+`;
+
+exports[`registration utils for "windows" platform runCommand is correct 1`] = `".\\\\gitlab-runner.exe run"`;
diff --git a/spec/frontend/ci/runner/components/registration/cli_command_spec.js b/spec/frontend/ci/runner/components/registration/cli_command_spec.js
new file mode 100644
index 00000000000..78c2b94c3ea
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/cli_command_spec.js
@@ -0,0 +1,39 @@
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('CliCommand', () => {
+ let wrapper;
+
+ // use .textContent instead of .text() to capture whitespace that's visible in <pre>
+ const getPreTextContent = () => wrapper.find('pre').element.textContent;
+ const getClipboardText = () => wrapper.findComponent(ClipboardButton).props('text');
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(CliCommand, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ it('when rendering a command', () => {
+ createComponent({
+ prompt: '#',
+ command: 'echo hi',
+ });
+
+ expect(getPreTextContent()).toBe('# echo hi');
+ expect(getClipboardText()).toBe('echo hi');
+ });
+
+ it('when rendering a multi-line command', () => {
+ createComponent({
+ prompt: '#',
+ command: ['git', ' --version'],
+ });
+
+ expect(getPreTextContent()).toBe('# git --version');
+ expect(getClipboardText()).toBe('git --version');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js b/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js
new file mode 100644
index 00000000000..0b438455b5b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/platforms_drawer_spec.js
@@ -0,0 +1,108 @@
+import { nextTick } from 'vue';
+import { GlDrawer, GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import {
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ INSTALL_HELP_URL,
+} from '~/ci/runner/constants';
+import { installScript, platformArchitectures } from '~/ci/runner/components/registration/utils';
+
+const MOCK_WRAPPER_HEIGHT = '99px';
+const LINUX_ARCHS = platformArchitectures({ platform: LINUX_PLATFORM });
+const MACOS_ARCHS = platformArchitectures({ platform: MACOS_PLATFORM });
+
+jest.mock('~/lib/utils/dom_utils', () => ({
+ getContentWrapperHeight: () => MOCK_WRAPPER_HEIGHT,
+}));
+
+describe('RegistrationInstructions', () => {
+ let wrapper;
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findEnvironmentOptions = () =>
+ wrapper.findByLabelText(s__('Runners|Environment')).findAll('option');
+ const findArchitectureOptions = () =>
+ wrapper.findByLabelText(s__('Runners|Architecture')).findAll('option');
+ const findCliCommand = () => wrapper.findComponent(CliCommand);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(PlatformsDrawer, {
+ propsData: {
+ open: true,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ it('shows drawer', () => {
+ createComponent();
+
+ expect(findDrawer().props()).toMatchObject({
+ open: true,
+ headerHeight: MOCK_WRAPPER_HEIGHT,
+ });
+ });
+
+ it('closes drawer', () => {
+ createComponent();
+ findDrawer().vm.$emit('close');
+
+ expect(wrapper.emitted('close')).toHaveLength(1);
+ });
+
+ it('shows selection options', () => {
+ createComponent({ mountFn: mountExtended });
+
+ expect(findEnvironmentOptions().wrappers.map((w) => w.attributes('value'))).toEqual([
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ ]);
+
+ expect(findArchitectureOptions().wrappers.map((w) => w.attributes('value'))).toEqual(
+ LINUX_ARCHS,
+ );
+ });
+
+ it('shows script', () => {
+ createComponent();
+
+ expect(findCliCommand().props('command')).toBe(
+ installScript({ platform: LINUX_PLATFORM, architecture: LINUX_ARCHS[0] }),
+ );
+ });
+
+ it('shows selection options for another platform', async () => {
+ createComponent({ mountFn: mountExtended });
+
+ findEnvironmentOptions().at(1).setSelected(); // macos
+ await nextTick();
+
+ expect(wrapper.emitted('selectPlatform')).toEqual([[MACOS_PLATFORM]]);
+
+ expect(findArchitectureOptions().wrappers.map((w) => w.attributes('value'))).toEqual(
+ MACOS_ARCHS,
+ );
+
+ expect(findCliCommand().props('command')).toBe(
+ installScript({ platform: MACOS_PLATFORM, architecture: MACOS_ARCHS[0] }),
+ );
+ });
+
+ it('shows external link for more information', () => {
+ createComponent();
+
+ expect(findLink().attributes('href')).toBe(INSTALL_HELP_URL);
+ expect(findLink().findComponent(GlIcon).props('name')).toBe('external-link');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_compatibility_alert_spec.js b/spec/frontend/ci/runner/components/registration/registration_compatibility_alert_spec.js
new file mode 100644
index 00000000000..75658270104
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_compatibility_alert_spec.js
@@ -0,0 +1,53 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import DismissibleFeedbackAlert from '~/vue_shared/components/dismissible_feedback_alert.vue';
+import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
+import { CHANGELOG_URL } from '~/ci/runner/constants';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+
+const ALERT_KEY = 'ALERT_KEY';
+
+describe('RegistrationCompatibilityAlert', () => {
+ let wrapper;
+
+ const findDismissibleFeedbackAlert = () => wrapper.findComponent(DismissibleFeedbackAlert);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = ({ mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RegistrationCompatibilityAlert, {
+ propsData: {
+ alertKey: ALERT_KEY,
+ },
+ ...options,
+ });
+ };
+
+ it('configures a featureName', () => {
+ createComponent();
+
+ expect(findDismissibleFeedbackAlert().props('featureName')).toBe(
+ `new_runner_compatibility_${ALERT_KEY}`,
+ );
+ });
+
+ it('alert has warning appearance', () => {
+ createComponent({
+ stubs: {
+ DismissibleFeedbackAlert,
+ },
+ });
+
+ expect(findAlert().props()).toMatchObject({
+ dismissible: true,
+ variant: 'warning',
+ title: expect.any(String),
+ });
+ });
+
+ it('shows alert content and link', () => {
+ createComponent({ mountFn: mountExtended });
+
+ expect(findAlert().text()).not.toBe('');
+ expect(findLink().attributes('href')).toBe(CHANGELOG_URL);
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 0daaca9c4ff..e564cf49ca0 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -1,10 +1,10 @@
-import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
-import { mount, shallowMount, createWrapper } from '@vue/test-utils';
+import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm, GlIcon } from '@gitlab/ui';
+import { createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { s__ } from '~/locale';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -12,7 +12,14 @@ import RegistrationDropdown from '~/ci/runner/components/registration/registrati
import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_REGISTER_INSTANCE_TYPE,
+ I18N_REGISTER_GROUP_TYPE,
+ I18N_REGISTER_PROJECT_TYPE,
+} from '~/ci/runner/constants';
import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql';
import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql';
@@ -21,9 +28,7 @@ import {
mockRunnerPlatforms,
mockInstructions,
} from 'jest/vue_shared/components/runner_instructions/mock_data';
-
-const mockToken = '0123456789';
-const maskToken = '**********';
+import { mockRegistrationToken } from '../../mock_data';
Vue.use(VueApollo);
@@ -31,7 +36,7 @@ describe('RegistrationDropdown', () => {
let wrapper;
const findDropdown = () => wrapper.findComponent(GlDropdown);
-
+ const findDropdownBtn = () => findDropdown().find('button');
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
@@ -53,17 +58,15 @@ describe('RegistrationDropdown', () => {
await waitForPromises();
};
- const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
- wrapper = extendedWrapper(
- mountFn(RegistrationDropdown, {
- propsData: {
- registrationToken: mockToken,
- type: INSTANCE_TYPE,
- ...props,
- },
- ...options,
- }),
- );
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(RegistrationDropdown, {
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ type: INSTANCE_TYPE,
+ ...props,
+ },
+ ...options,
+ });
};
const createComponentWithModal = () => {
@@ -79,27 +82,40 @@ describe('RegistrationDropdown', () => {
// Use `attachTo` to find the modal
attachTo: document.body,
},
- mount,
+ mountExtended,
);
};
it.each`
type | text
- ${INSTANCE_TYPE} | ${s__('Runners|Register an instance runner')}
- ${GROUP_TYPE} | ${s__('Runners|Register a group runner')}
- ${PROJECT_TYPE} | ${s__('Runners|Register a project runner')}
- `('Dropdown text for type $type is "$text"', () => {
- createComponent({ props: { type: INSTANCE_TYPE } }, mount);
+ ${INSTANCE_TYPE} | ${I18N_REGISTER_INSTANCE_TYPE}
+ ${GROUP_TYPE} | ${I18N_REGISTER_GROUP_TYPE}
+ ${PROJECT_TYPE} | ${I18N_REGISTER_PROJECT_TYPE}
+ `('Dropdown text for type $type is "$text"', ({ type, text }) => {
+ createComponent({ props: { type } }, mountExtended);
- expect(wrapper.text()).toContain('Register an instance runner');
+ expect(wrapper.text()).toContain(text);
});
- it('Passes attributes to the dropdown component', () => {
+ it('Passes attributes to dropdown', () => {
createComponent({ attrs: { right: true } });
expect(findDropdown().attributes()).toMatchObject({ right: 'true' });
});
+ it('Passes default props and attributes to dropdown', () => {
+ createComponent();
+
+ expect(findDropdown().props()).toMatchObject({
+ category: 'primary',
+ variant: 'confirm',
+ });
+
+ expect(findDropdown().attributes()).toMatchObject({
+ toggleclass: '',
+ });
+ });
+
describe('Instructions dropdown item', () => {
it('Displays "Show runner" dropdown item', () => {
createComponent();
@@ -111,15 +127,11 @@ describe('RegistrationDropdown', () => {
describe('When the dropdown item is clicked', () => {
beforeEach(async () => {
- createComponentWithModal({}, mount);
+ createComponentWithModal({}, mountExtended);
await openModal();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('opens the modal with contents', () => {
const modalText = findModalContent();
@@ -146,7 +158,15 @@ describe('RegistrationDropdown', () => {
});
it('Displays masked value by default', () => {
- createComponent({}, mount);
+ const mockToken = '0123456789';
+ const maskToken = '**********';
+
+ createComponent(
+ {
+ props: { registrationToken: mockToken },
+ },
+ mountExtended,
+ );
expect(findRegistrationTokenInput().element.value).toBe(maskToken);
});
@@ -175,7 +195,7 @@ describe('RegistrationDropdown', () => {
};
it('Updates token input', async () => {
- createComponent({}, mount);
+ createComponent({}, mountExtended);
expect(findRegistrationToken().props('value')).not.toBe(newToken);
@@ -185,15 +205,72 @@ describe('RegistrationDropdown', () => {
});
it('Updates token in modal', async () => {
- createComponentWithModal({}, mount);
+ createComponentWithModal({}, mountExtended);
await openModal();
- expect(findModalContent()).toContain(mockToken);
+ expect(findModalContent()).toContain(mockRegistrationToken);
await resetToken();
expect(findModalContent()).toContain(newToken);
});
});
+
+ describe.each([
+ { createRunnerWorkflowForAdmin: true },
+ { createRunnerWorkflowForNamespace: true },
+ ])('When showing a "deprecated" warning', (glFeatures) => {
+ it('passes deprecated variant props and attributes to dropdown', () => {
+ createComponent({
+ provide: { glFeatures },
+ });
+
+ expect(findDropdown().props()).toMatchObject({
+ category: 'tertiary',
+ variant: 'default',
+ text: '',
+ });
+
+ expect(findDropdown().attributes()).toMatchObject({
+ toggleclass: 'gl-px-3!',
+ });
+ });
+
+ it.each`
+ type | text
+ ${INSTANCE_TYPE} | ${I18N_REGISTER_INSTANCE_TYPE}
+ ${GROUP_TYPE} | ${I18N_REGISTER_GROUP_TYPE}
+ ${PROJECT_TYPE} | ${I18N_REGISTER_PROJECT_TYPE}
+ `('dropdown text for type $type is "$text"', ({ type, text }) => {
+ createComponent({ props: { type } }, mountExtended);
+
+ expect(wrapper.text()).toContain(text);
+ });
+
+ it('shows warning text', () => {
+ createComponent(
+ {
+ provide: { glFeatures },
+ },
+ mountExtended,
+ );
+
+ const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated'));
+
+ expect(text.exists()).toBe(true);
+ });
+
+ it('button shows ellipsis icon', () => {
+ createComponent(
+ {
+ provide: { glFeatures },
+ },
+ mountExtended,
+ );
+
+ expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
+ expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js b/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js
new file mode 100644
index 00000000000..fa6b7ad7c63
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_feedback_banner_spec.js
@@ -0,0 +1,52 @@
+import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
+
+describe('Runner registration feeback banner', () => {
+ let wrapper;
+ let userCalloutDismissSpy;
+
+ const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
+ const findBanner = () => wrapper.findComponent(GlBanner);
+
+ const createComponent = ({ shouldShowCallout = true } = {}) => {
+ userCalloutDismissSpy = jest.fn();
+
+ wrapper = shallowMount(RegistrationFeedbackBanner, {
+ stubs: {
+ UserCalloutDismisser: makeMockUserCalloutDismisser({
+ dismiss: userCalloutDismissSpy,
+ shouldShowCallout,
+ }),
+ },
+ });
+ };
+
+ it('banner is shown', () => {
+ createComponent();
+
+ expect(findBanner().exists()).toBe(true);
+ });
+
+ it('dismisses the callout when closed', () => {
+ createComponent();
+
+ findBanner().vm.$emit('close');
+
+ expect(userCalloutDismissSpy).toHaveBeenCalled();
+ });
+
+ it('sets feature name to create_runner_workflow_banner', () => {
+ createComponent();
+
+ expect(findUserCalloutDismisser().props('featureName')).toBe('create_runner_workflow_banner');
+ });
+
+ it('is not displayed once it has been dismissed', () => {
+ createComponent({ shouldShowCallout: false });
+
+ expect(findBanner().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
new file mode 100644
index 00000000000..8c196d7b5e3
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
@@ -0,0 +1,326 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { extendedWrapper, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import runnerForRegistrationQuery from '~/ci/runner/graphql/register/runner_for_registration.query.graphql';
+import CliCommand from '~/ci/runner/components/registration/cli_command.vue';
+import {
+ DEFAULT_PLATFORM,
+ EXECUTORS_HELP_URL,
+ SERVICE_COMMANDS_HELP_URL,
+ STATUS_NEVER_CONTACTED,
+ STATUS_ONLINE,
+ RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
+ I18N_REGISTRATION_SUCCESS,
+} from '~/ci/runner/constants';
+import { runnerForRegistration, mockAuthenticationToken } from '../../mock_data';
+
+Vue.use(VueApollo);
+
+const mockRunner = {
+ ...runnerForRegistration.data.runner,
+ ephemeralAuthenticationToken: mockAuthenticationToken,
+};
+const mockRunnerWithoutToken = {
+ ...runnerForRegistration.data.runner,
+ ephemeralAuthenticationToken: null,
+};
+
+const mockRunnerId = `${getIdFromGraphQLId(mockRunner.id)}`;
+
+describe('RegistrationInstructions', () => {
+ let wrapper;
+ let mockRunnerQuery;
+
+ const findHeading = () => wrapper.find('h1');
+ const findStepAt = (i) => extendedWrapper(wrapper.findAll('section').at(i));
+ const findByText = (text, container = wrapper) => container.findByText(text);
+
+ const waitForPolling = async () => {
+ jest.advanceTimersByTime(RUNNER_REGISTRATION_POLLING_INTERVAL_MS);
+ await waitForPromises();
+ };
+
+ const mockBeforeunload = () => {
+ const event = new Event('beforeunload');
+ const preventDefault = jest.spyOn(event, 'preventDefault');
+ const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
+
+ return {
+ event,
+ preventDefault,
+ returnValueSetter,
+ };
+ };
+
+ const mockResolvedRunner = (runner = mockRunner) => {
+ mockRunnerQuery.mockResolvedValue({
+ data: {
+ runner,
+ },
+ });
+ };
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(RegistrationInstructions, {
+ apolloProvider: createMockApollo([[runnerForRegistrationQuery, mockRunnerQuery]]),
+ propsData: {
+ runnerId: mockRunnerId,
+ platform: DEFAULT_PLATFORM,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockRunnerQuery = jest.fn();
+ mockResolvedRunner();
+ });
+
+ beforeEach(() => {
+ window.gon.gitlab_url = TEST_HOST;
+ });
+
+ it('loads runner with id', () => {
+ createComponent();
+
+ expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunner.id });
+ });
+
+ describe('heading', () => {
+ it('when runner is loaded, shows heading', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findHeading().text()).toContain(mockRunner.description);
+ });
+
+ it('when runner is loaded, shows heading safely', async () => {
+ mockResolvedRunner({
+ ...mockRunner,
+ description: '<script>hacked();</script>',
+ });
+
+ createComponent();
+ await waitForPromises();
+
+ expect(findHeading().text()).toBe('Register "<script>hacked();</script>" runner');
+ expect(findHeading().element.innerHTML).toBe(
+ 'Register "&lt;script&gt;hacked();&lt;/script&gt;" runner',
+ );
+ });
+
+ it('when runner is loading, shows default heading', () => {
+ createComponent();
+
+ expect(findHeading().text()).toBe(s__('Runners|Register runner'));
+ });
+ });
+
+ it('renders legacy instructions', () => {
+ createComponent();
+
+ findByText('How do I install GitLab Runner?').vm.$emit('click');
+
+ expect(wrapper.emitted('toggleDrawer')).toHaveLength(1);
+ });
+
+ describe('step 1', () => {
+ it('renders step 1', async () => {
+ createComponent();
+ await waitForPromises();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(CliCommand).props()).toEqual({
+ command: [
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --token ${mockAuthenticationToken}`,
+ ],
+ prompt: '$',
+ });
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
+ });
+
+ it('renders step 1 in loading state', () => {
+ createComponent();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(GlSkeletonLoader).exists()).toBe(true);
+ expect(step1.find('code').exists()).toBe(false);
+ expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
+ });
+
+ it('render step 1 after token is not visible', async () => {
+ mockResolvedRunner(mockRunnerWithoutToken);
+
+ createComponent();
+ await waitForPromises();
+
+ const step1 = findStepAt(0);
+
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ]);
+ expect(step1.findByTestId('runner-token').exists()).toBe(false);
+ expect(step1.findComponent(ClipboardButton).exists()).toBe(false);
+ });
+
+ describe('polling for changes', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('fetches data', () => {
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(1);
+ });
+
+ it('polls', async () => {
+ await waitForPolling();
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+
+ await waitForPolling();
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(3);
+ });
+
+ it('when runner is online, stops polling', async () => {
+ mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
+ await waitForPolling();
+
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+ await waitForPolling();
+
+ expect(mockRunnerQuery).toHaveBeenCalledTimes(2);
+ });
+
+ it('when token is no longer visible in the API, it is still visible in the UI', async () => {
+ mockResolvedRunner(mockRunnerWithoutToken);
+ await waitForPolling();
+
+ const step1 = findStepAt(0);
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --token ${mockAuthenticationToken}`,
+ ]);
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
+ });
+
+ it('when runner is not available (e.g. deleted), the UI does not update', async () => {
+ mockResolvedRunner(null);
+ await waitForPolling();
+
+ const step1 = findStepAt(0);
+ expect(step1.findComponent(CliCommand).props('command')).toEqual([
+ 'gitlab-runner register',
+ ` --url ${TEST_HOST}`,
+ ` --token ${mockAuthenticationToken}`,
+ ]);
+ expect(step1.findByTestId('runner-token').text()).toBe(mockAuthenticationToken);
+ expect(step1.findComponent(ClipboardButton).props('text')).toBe(mockAuthenticationToken);
+ });
+ });
+ });
+
+ it('renders step 2', () => {
+ createComponent();
+ const step2 = findStepAt(1);
+
+ expect(findByText('Not sure which one to select?', step2).attributes('href')).toBe(
+ EXECUTORS_HELP_URL,
+ );
+ });
+
+ it('renders step 3', () => {
+ createComponent();
+ const step3 = findStepAt(2);
+
+ expect(step3.findComponent(CliCommand).props()).toEqual({
+ command: 'gitlab-runner run',
+ prompt: '$',
+ });
+
+ expect(findByText('system or user service', step3).attributes('href')).toBe(
+ SERVICE_COMMANDS_HELP_URL,
+ );
+ });
+
+ describe('success state', () => {
+ describe('when the runner has not been registered', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPolling();
+
+ mockResolvedRunner({ ...mockRunner, status: STATUS_NEVER_CONTACTED });
+
+ await waitForPolling();
+ });
+
+ it('does not show success message', () => {
+ expect(wrapper.text()).not.toContain(I18N_REGISTRATION_SUCCESS);
+ });
+
+ describe('when the page is closing', () => {
+ it('warns the user against closing', () => {
+ const { event, preventDefault, returnValueSetter } = mockBeforeunload();
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+
+ window.dispatchEvent(event);
+
+ expect(preventDefault).toHaveBeenCalledWith();
+ expect(returnValueSetter).toHaveBeenCalledWith(expect.any(String));
+ });
+ });
+ });
+
+ describe('when the runner has been registered', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPolling();
+
+ mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
+ await waitForPolling();
+ });
+
+ it('shows success message', () => {
+ expect(wrapper.text()).toContain('🎉');
+ expect(wrapper.text()).toContain(I18N_REGISTRATION_SUCCESS);
+ });
+
+ describe('when the page is closing', () => {
+ it('does not warn the user against closing', () => {
+ const { event, preventDefault, returnValueSetter } = mockBeforeunload();
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+
+ window.dispatchEvent(event);
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index 783a4d9252a..bfdde922e17 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -5,20 +5,20 @@ 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 '~/flash';
+import { createAlert } from '~/alert';
import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/ci/runner/sentry_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);
Vue.use(GlToast);
-const mockNewToken = 'NEW_TOKEN';
+const mockNewRegistrationToken = 'MOCK_NEW_REGISTRATION_TOKEN';
const modalID = 'token-reset-modal';
describe('RegistrationTokenResetDropdownItem', () => {
@@ -43,7 +43,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
[runnersRegistrationTokenResetMutation, runnersRegistrationTokenResetMutationHandler],
]),
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
});
@@ -54,7 +54,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
runnersRegistrationTokenResetMutationHandler = jest.fn().mockResolvedValue({
data: {
runnersRegistrationTokenReset: {
- token: mockNewToken,
+ token: mockNewRegistrationToken,
errors: [],
},
},
@@ -63,10 +63,6 @@ describe('RegistrationTokenResetDropdownItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays reset button', () => {
expect(findDropdownItem().exists()).toBe(true);
});
@@ -113,7 +109,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
it('emits result', () => {
expect(wrapper.emitted('tokenReset')).toHaveLength(1);
- expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewToken]);
+ expect(wrapper.emitted('tokenReset')[0]).toEqual([mockNewRegistrationToken]);
});
it('does not show a loading state', () => {
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index d2a51c0d910..869c032c0b5 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -3,9 +3,7 @@ import Vue from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
-
-const mockToken = '01234567890';
-const mockMasked = '***********';
+import { mockRegistrationToken } from '../../mock_data';
describe('RegistrationToken', () => {
let wrapper;
@@ -15,26 +13,23 @@ describe('RegistrationToken', () => {
const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(RegistrationToken, {
propsData: {
- value: mockToken,
+ value: mockRegistrationToken,
inputId: 'token-value',
...props,
},
+ ...options,
});
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays value and copy button', () => {
createComponent();
- expect(findInputCopyToggleVisibility().props('value')).toBe(mockToken);
+ expect(findInputCopyToggleVisibility().props('value')).toBe(mockRegistrationToken);
expect(findInputCopyToggleVisibility().props('copyButtonTitle')).toBe(
'Copy registration token',
);
@@ -42,9 +37,17 @@ describe('RegistrationToken', () => {
// Component integration test to ensure secure masking
it('Displays masked value by default', () => {
- createComponent({ mountFn: mountExtended });
+ const mockToken = '0123456789';
+ const maskToken = '**********';
+
+ createComponent({
+ props: {
+ value: mockToken,
+ },
+ mountFn: mountExtended,
+ });
- expect(wrapper.find('input').element.value).toBe(mockMasked);
+ expect(wrapper.find('input').element.value).toBe(maskToken);
});
describe('When the copy to clipboard button is clicked', () => {
@@ -59,4 +62,23 @@ describe('RegistrationToken', () => {
expect(showToast).toHaveBeenCalledWith('Registration token copied!');
});
});
+
+ describe('When slots are used', () => {
+ const slotName = 'label-description';
+ const slotContent = 'Label Description';
+
+ beforeEach(() => {
+ createComponent({
+ slots: {
+ [slotName]: slotContent,
+ },
+ });
+ });
+
+ it('passes slots to the input component', () => {
+ const slot = findInputCopyToggleVisibility().vm.$scopedSlots[slotName];
+
+ expect(slot()[0].text).toBe(slotContent);
+ });
+ });
});
diff --git a/spec/frontend/ci/runner/components/registration/utils_spec.js b/spec/frontend/ci/runner/components/registration/utils_spec.js
new file mode 100644
index 00000000000..997cc5769ee
--- /dev/null
+++ b/spec/frontend/ci/runner/components/registration/utils_spec.js
@@ -0,0 +1,94 @@
+import { TEST_HOST } from 'helpers/test_constants';
+import {
+ DEFAULT_PLATFORM,
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+
+import {
+ commandPrompt,
+ registerCommand,
+ runCommand,
+ installScript,
+ platformArchitectures,
+} from '~/ci/runner/components/registration/utils';
+
+import { mockAuthenticationToken } from '../../mock_data';
+
+describe('registration utils', () => {
+ beforeEach(() => {
+ window.gon.gitlab_url = TEST_HOST;
+ });
+
+ describe.each([LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM])(
+ 'for "%s" platform',
+ (platform) => {
+ it('commandPrompt is correct', () => {
+ expect(commandPrompt({ platform })).toMatchSnapshot();
+ });
+
+ it('registerCommand is correct', () => {
+ expect(
+ registerCommand({
+ platform,
+ token: mockAuthenticationToken,
+ }),
+ ).toMatchSnapshot();
+
+ expect(registerCommand({ platform })).toMatchSnapshot();
+ });
+
+ it('runCommand is correct', () => {
+ expect(runCommand({ platform })).toMatchSnapshot();
+ });
+ },
+ );
+
+ describe('for missing platform', () => {
+ it('commandPrompt uses the default', () => {
+ const expected = commandPrompt({ platform: DEFAULT_PLATFORM });
+
+ expect(commandPrompt({ platform: null })).toEqual(expected);
+ expect(commandPrompt({ platform: undefined })).toEqual(expected);
+ });
+
+ it('registerCommand uses the default', () => {
+ const expected = registerCommand({
+ platform: DEFAULT_PLATFORM,
+ token: mockAuthenticationToken,
+ });
+
+ expect(registerCommand({ platform: null, token: mockAuthenticationToken })).toEqual(expected);
+ expect(registerCommand({ platform: undefined, token: mockAuthenticationToken })).toEqual(
+ expected,
+ );
+ });
+
+ it('runCommand uses the default', () => {
+ const expected = runCommand({ platform: DEFAULT_PLATFORM });
+
+ expect(runCommand({ platform: null })).toEqual(expected);
+ expect(runCommand({ platform: undefined })).toEqual(expected);
+ });
+ });
+
+ describe.each([LINUX_PLATFORM, MACOS_PLATFORM, WINDOWS_PLATFORM])(
+ 'for "%s" platform',
+ (platform) => {
+ describe('platformArchitectures', () => {
+ it('returns correct list of architectures', () => {
+ expect(platformArchitectures({ platform })).toMatchSnapshot();
+ });
+ });
+
+ describe('installScript', () => {
+ const architectures = platformArchitectures({ platform });
+
+ it.each(architectures)('is correct for "%s" architecture', (architecture) => {
+ expect(installScript({ platform, architecture })).toMatchSnapshot();
+ });
+ });
+ },
+ );
+});
diff --git a/spec/frontend/ci/runner/components/runner_assigned_item_spec.js b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
index 5df2e04c340..a1fd9e4c1aa 100644
--- a/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
+++ b/spec/frontend/ci/runner/components/runner_assigned_item_spec.js
@@ -33,10 +33,6 @@ describe('RunnerAssignedItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Shows an avatar', () => {
const avatar = findAvatar();
diff --git a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
index 0dc5a90fb83..7bd4b701002 100644
--- a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
@@ -2,12 +2,13 @@ import Vue from 'vue';
import { makeVar } from '@apollo/client/core';
import { GlModal, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
import RunnerBulkDelete from '~/ci/runner/components/runner_bulk_delete.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import BulkRunnerDeleteMutation from '~/ci/runner/graphql/list/bulk_runner_delete.mutation.graphql';
import { createLocalState } from '~/ci/runner/graphql/list/local_state';
import waitForPromises from 'helpers/wait_for_promises';
@@ -15,7 +16,7 @@ import { allRunnersData } from '../mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RunnerBulkDelete', () => {
let wrapper;
@@ -34,7 +35,7 @@ describe('RunnerBulkDelete', () => {
const bulkRunnerDeleteHandler = jest.fn();
- const createComponent = () => {
+ const createComponent = ({ stubs } = {}) => {
const { cacheConfig, localMutations } = mockState;
const apolloProvider = createMockApollo(
[[BulkRunnerDeleteMutation, bulkRunnerDeleteHandler]],
@@ -51,11 +52,12 @@ describe('RunnerBulkDelete', () => {
runners: mockRunners,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
GlSprintf,
GlModal,
+ ...stubs,
},
});
@@ -135,11 +137,15 @@ describe('RunnerBulkDelete', () => {
beforeEach(() => {
mockCheckedRunnerIds([mockId1, mockId2]);
+ mockHideModal = jest.fn();
- createComponent();
+ createComponent({
+ stubs: {
+ GlModal: stubComponent(GlModal, { methods: { hide: mockHideModal } }),
+ },
+ });
jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
- mockHideModal = jest.spyOn(findModal().vm, 'hide').mockImplementation(() => {});
});
describe('when deletion is confirmed', () => {
diff --git a/spec/frontend/ci/runner/components/runner_create_form_spec.js b/spec/frontend/ci/runner/components/runner_create_form_spec.js
new file mode 100644
index 00000000000..329dd2f73ee
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_create_form_spec.js
@@ -0,0 +1,189 @@
+import Vue from 'vue';
+import { GlForm } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import {
+ DEFAULT_ACCESS_LEVEL,
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+} from '~/ci/runner/constants';
+import runnerCreateMutation from '~/ci/runner/graphql/new/runner_create.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { runnerCreateResult } from '../mock_data';
+
+jest.mock('~/ci/runner/sentry_utils');
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
+const defaultRunnerModel = {
+ description: '',
+ accessLevel: DEFAULT_ACCESS_LEVEL,
+ paused: false,
+ maintenanceNote: '',
+ maximumTimeout: '',
+ runUntagged: false,
+ tagList: '',
+};
+
+Vue.use(VueApollo);
+
+describe('RunnerCreateForm', () => {
+ let wrapper;
+ let runnerCreateHandler;
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+ const findSubmitBtn = () => wrapper.find('[type="submit"]');
+
+ const createComponent = ({ props } = {}) => {
+ wrapper = shallowMountExtended(RunnerCreateForm, {
+ propsData: {
+ runnerType: INSTANCE_TYPE,
+ ...props,
+ },
+ apolloProvider: createMockApollo([[runnerCreateMutation, runnerCreateHandler]]),
+ });
+ };
+
+ beforeEach(() => {
+ runnerCreateHandler = jest.fn().mockResolvedValue(runnerCreateResult);
+ });
+
+ it('shows default runner values', () => {
+ createComponent();
+
+ expect(findRunnerFormFields().props('value')).toEqual(defaultRunnerModel);
+ });
+
+ it('shows a submit button', () => {
+ createComponent();
+
+ expect(findSubmitBtn().exists()).toBe(true);
+ });
+
+ describe.each`
+ typeName | props | scopeData
+ ${'an instance runner'} | ${{ runnerType: INSTANCE_TYPE }} | ${{ runnerType: INSTANCE_TYPE }}
+ ${'a group runner'} | ${{ runnerType: GROUP_TYPE, groupId: 'gid://gitlab/Group/72' }} | ${{ runnerType: GROUP_TYPE, groupId: 'gid://gitlab/Group/72' }}
+ ${'a project runner'} | ${{ runnerType: PROJECT_TYPE, projectId: 'gid://gitlab/Project/42' }} | ${{ runnerType: PROJECT_TYPE, projectId: 'gid://gitlab/Project/42' }}
+ `('when user submits $typeName', ({ props, scopeData }) => {
+ let preventDefault;
+
+ beforeEach(() => {
+ createComponent({ props });
+
+ preventDefault = jest.fn();
+
+ findRunnerFormFields().vm.$emit('input', {
+ ...defaultRunnerModel,
+ description: 'My runner',
+ maximumTimeout: 0,
+ tagList: 'tag1, tag2',
+ });
+ });
+
+ describe('immediately after submit', () => {
+ beforeEach(() => {
+ findForm().vm.$emit('submit', { preventDefault });
+ });
+
+ it('prevents default form submission', () => {
+ expect(preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(true);
+ });
+
+ it('saves runner', () => {
+ expect(runnerCreateHandler).toHaveBeenCalledWith({
+ input: {
+ ...defaultRunnerModel,
+ ...scopeData,
+ description: 'My runner',
+ maximumTimeout: 0,
+ tagList: ['tag1', 'tag2'],
+ },
+ });
+ });
+ });
+
+ describe('when saved successfully', () => {
+ beforeEach(async () => {
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "saved" result', () => {
+ expect(wrapper.emitted('saved')[0]).toEqual([mockCreatedRunner]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+ });
+
+ describe('when a server error occurs', () => {
+ const error = new Error('Error!');
+
+ beforeEach(async () => {
+ runnerCreateHandler.mockRejectedValue(error);
+
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "error" result', () => {
+ expect(wrapper.emitted('error')[0]).toEqual([error]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+
+ it('reports error', () => {
+ expect(captureException).toHaveBeenCalledTimes(1);
+ expect(captureException).toHaveBeenCalledWith({
+ component: 'RunnerCreateForm',
+ error,
+ });
+ });
+ });
+
+ describe('when a validation error occurs', () => {
+ const errorMsg1 = 'Issue1!';
+ const errorMsg2 = 'Issue2!';
+
+ beforeEach(async () => {
+ runnerCreateHandler.mockResolvedValue({
+ data: {
+ runnerCreate: {
+ errors: [errorMsg1, errorMsg2],
+ runner: null,
+ },
+ },
+ });
+
+ findForm().vm.$emit('submit', { preventDefault });
+ await waitForPromises();
+ });
+
+ it('emits "error" results', () => {
+ expect(wrapper.emitted('error')[0]).toEqual([new Error(`${errorMsg1} ${errorMsg2}`)]);
+ });
+
+ it('does not show a saving state', () => {
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+
+ it('does not report error', () => {
+ expect(captureException).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
index 02960ad427e..3123f2894fb 100644
--- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
@@ -8,7 +8,7 @@ import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutat
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { I18N_DELETE_RUNNER } from '~/ci/runner/constants';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
@@ -21,7 +21,7 @@ const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
@@ -53,8 +53,8 @@ describe('RunnerDeleteButton', () => {
},
apolloProvider,
directives: {
- GlTooltip: createMockDirective(),
- GlModal: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlModal: createMockDirective('gl-modal'),
},
});
};
@@ -83,10 +83,6 @@ describe('RunnerDeleteButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays a delete button without an icon', () => {
expect(findBtn().props()).toMatchObject({
loading: false,
@@ -128,15 +124,15 @@ describe('RunnerDeleteButton', () => {
});
describe('Immediately after the delete button is clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
findModal().vm.$emit('primary');
});
- it('The button has a loading state', async () => {
+ it('The button has a loading state', () => {
expect(findBtn().props('loading')).toBe(true);
});
- it('The stale tooltip is removed', async () => {
+ it('The stale tooltip is removed', () => {
expect(getTooltip()).toBe('');
});
});
@@ -259,15 +255,15 @@ describe('RunnerDeleteButton', () => {
});
describe('Immediately after the button is clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
findModal().vm.$emit('primary');
});
- it('The button has a loading state', async () => {
+ it('The button has a loading state', () => {
expect(findBtn().props('loading')).toBe(true);
});
- it('The stale tooltip is removed', async () => {
+ it('The stale tooltip is removed', () => {
expect(getTooltip()).toBe('');
});
});
diff --git a/spec/frontend/ci/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js
index 65a81973869..c2d9e86aa91 100644
--- a/spec/frontend/ci/runner/components/runner_details_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_spec.js
@@ -37,10 +37,6 @@ describe('RunnerDetails', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Details tab', () => {
describe.each`
field | runner | expectedValue
diff --git a/spec/frontend/ci/runner/components/runner_edit_button_spec.js b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
index 907cdc90100..5cc1ee049f4 100644
--- a/spec/frontend/ci/runner/components/runner_edit_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
@@ -11,7 +11,7 @@ describe('RunnerEditButton', () => {
wrapper = mountFn(RunnerEditButton, {
attrs,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -20,10 +20,6 @@ describe('RunnerEditButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays Edit text', () => {
expect(wrapper.attributes('aria-label')).toBe('Edit');
});
diff --git a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
index 408750e646f..7572122a5f3 100644
--- a/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
+++ b/spec/frontend/ci/runner/components/runner_filtered_search_bar_spec.js
@@ -1,5 +1,6 @@
import { GlFilteredSearch, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { assertProps } from 'helpers/assert_props';
import RunnerFilteredSearchBar from '~/ci/runner/components/runner_filtered_search_bar.vue';
import { statusTokenConfig } from '~/ci/runner/components/search_tokens/status_token_config';
import TagToken from '~/ci/runner/components/search_tokens/tag_token.vue';
@@ -43,12 +44,12 @@ describe('RunnerList', () => {
expect(inputs[inputs.length - 1][0]).toEqual(value);
};
+ const defaultProps = { namespace: 'runners', tokens: [], value: mockSearch };
+
const createComponent = ({ props = {}, options = {} } = {}) => {
wrapper = shallowMountExtended(RunnerFilteredSearchBar, {
propsData: {
- namespace: 'runners',
- tokens: [],
- value: mockSearch,
+ ...defaultProps,
...props,
},
stubs: {
@@ -65,10 +66,6 @@ describe('RunnerList', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('binds a namespace to the filtered search', () => {
expect(findFilteredSearch().props('namespace')).toBe('runners');
});
@@ -113,11 +110,14 @@ describe('RunnerList', () => {
it('fails validation for v-model with the wrong shape', () => {
expect(() => {
- createComponent({ props: { value: { filters: 'wrong_filters', sort: 'sort' } } });
+ assertProps(RunnerFilteredSearchBar, {
+ ...defaultProps,
+ value: { filters: 'wrong_filters', sort: 'sort' },
+ });
}).toThrow('Invalid prop: custom validator check failed');
expect(() => {
- createComponent({ props: { value: { sort: 'sort' } } });
+ assertProps(RunnerFilteredSearchBar, { ...defaultProps, value: { sort: 'sort' } });
}).toThrow('Invalid prop: custom validator check failed');
});
diff --git a/spec/frontend/ci/runner/components/runner_groups_spec.js b/spec/frontend/ci/runner/components/runner_groups_spec.js
index 0991feb2e55..e4f5f55ab4b 100644
--- a/spec/frontend/ci/runner/components/runner_groups_spec.js
+++ b/spec/frontend/ci/runner/components/runner_groups_spec.js
@@ -23,10 +23,6 @@ describe('RunnerGroups', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Shows a heading', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index abe3b47767e..c851966431d 100644
--- a/spec/frontend/ci/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -42,10 +42,6 @@ describe('RunnerHeader', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the runner status', () => {
createComponent({
mountFn: mountExtended,
diff --git a/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js
new file mode 100644
index 00000000000..59c9383cb31
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_jobs_empty_state_spec.js
@@ -0,0 +1,35 @@
+import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/pipelines_empty.svg?url';
+
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState } from '@gitlab/ui';
+import RunnerJobsEmptyState from '~/ci/runner/components/runner_jobs_empty_state.vue';
+
+const DEFAULT_PROPS = {
+ emptyTitle: 'This runner has not run any jobs',
+ emptyDescription:
+ 'Make sure the runner is online and available to run jobs (not paused). Jobs display here when the runner picks them up.',
+};
+
+describe('RunnerJobsEmptyStateComponent', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMount(RunnerJobsEmptyState);
+ };
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ describe('empty', () => {
+ it('should show an empty state if it is empty', () => {
+ const emptyState = findEmptyState();
+
+ expect(emptyState.props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
+ expect(emptyState.props('title')).toBe(DEFAULT_PROPS.emptyTitle);
+ expect(emptyState.text()).toContain(DEFAULT_PROPS.emptyDescription);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_jobs_spec.js b/spec/frontend/ci/runner/components/runner_jobs_spec.js
index bdb8a4a31a3..179b37cfa21 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_spec.js
@@ -4,18 +4,19 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
+import RunnerJobsEmptyState from '~/ci/runner/components/runner_jobs_empty_state.vue';
import { captureException } from '~/ci/runner/sentry_utils';
-import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/ci/runner/constants';
+import { RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/ci/runner/constants';
import runnerJobsQuery from '~/ci/runner/graphql/show/runner_jobs.query.graphql';
import { runnerData, runnerJobsData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
@@ -31,7 +32,7 @@ describe('RunnerJobs', () => {
const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader);
const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable);
const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
-
+ const findEmptyState = () => wrapper.findComponent(RunnerJobsEmptyState);
const createComponent = ({ mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RunnerJobs, {
apolloProvider: createMockApollo([[runnerJobsQuery, mockRunnerJobsQuery]]),
@@ -47,7 +48,6 @@ describe('RunnerJobs', () => {
afterEach(() => {
mockRunnerJobsQuery.mockReset();
- wrapper.destroy();
});
it('Requests runner jobs', async () => {
@@ -100,7 +100,7 @@ describe('RunnerJobs', () => {
expect(findGlSkeletonLoading().exists()).toBe(true);
expect(findRunnerJobsTable().exists()).toBe(false);
- expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findRunnerPagination().attributes('disabled')).toBeDefined();
});
});
@@ -128,8 +128,8 @@ describe('RunnerJobs', () => {
await waitForPromises();
});
- it('Shows a "None" label', () => {
- expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND);
+ it('should render empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
index 281aa1aeb77..694c5a6ed17 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
@@ -37,10 +37,6 @@ describe('RunnerJobsTable', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Sets job id as a row key', () => {
createComponent();
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index 6aea3ddf58c..0de2759ea8a 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -1,19 +1,15 @@
+import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/pipelines_empty.svg?url';
+import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/magnifying-glass.svg?url';
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import {
- newRunnerPath,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
-} from 'jest/ci/runner/mock_data';
+import { mockRegistrationToken, newRunnerPath } from 'jest/ci/runner/mock_data';
import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
-const mockRegistrationToken = 'REGISTRATION_TOKEN';
-
describe('RunnerListEmptyState', () => {
let wrapper;
@@ -24,14 +20,12 @@ describe('RunnerListEmptyState', () => {
const createComponent = ({ props, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(RunnerListEmptyState, {
propsData: {
- svgPath: emptyStateSvgPath,
- filteredSvgPath: emptyStateFilteredSvgPath,
registrationToken: mockRegistrationToken,
newRunnerPath,
...props,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
stubs: {
GlEmptyState,
@@ -51,7 +45,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
it('displays "no results" text with instructions', () => {
@@ -62,44 +56,52 @@ describe('RunnerListEmptyState', () => {
expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
});
- describe('when create_runner_workflow is enabled', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: { createRunnerWorkflow: true },
- },
+ describe.each([
+ { createRunnerWorkflowForAdmin: true },
+ { createRunnerWorkflowForNamespace: true },
+ ])('when %o', (glFeatures) => {
+ describe('when newRunnerPath is defined', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures,
+ },
+ });
});
- });
- it('shows a link to the new runner page', () => {
- expect(findLink().attributes('href')).toBe(newRunnerPath);
+ it('shows a link to the new runner page', () => {
+ expect(findLink().attributes('href')).toBe(newRunnerPath);
+ });
});
- });
- describe('when create_runner_workflow is enabled and newRunnerPath not defined', () => {
- beforeEach(() => {
- createComponent({
- props: {
- newRunnerPath: null,
- },
- provide: {
- glFeatures: { createRunnerWorkflow: true },
- },
+ describe('when newRunnerPath not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures,
+ },
+ });
});
- });
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
});
});
- describe('when create_runner_workflow is disabled', () => {
+ describe.each([
+ { createRunnerWorkflowForAdmin: false },
+ { createRunnerWorkflowForNamespace: false },
+ ])('when %o', (glFeatures) => {
beforeEach(() => {
createComponent({
provide: {
- glFeatures: { createRunnerWorkflow: false },
+ glFeatures,
},
});
});
@@ -118,7 +120,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
it('displays "no results" text', () => {
@@ -141,7 +143,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders a "filtered search" illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(emptyStateFilteredSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(FILTERED_SVG_URL);
});
it('displays "no filtered results" text', () => {
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 2e5d1dbd063..0f4ec717c3e 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -57,10 +57,6 @@ describe('RunnerList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays headers', () => {
createComponent(
{
@@ -168,7 +164,7 @@ describe('RunnerList', () => {
});
});
- it('Emits a deleted event', async () => {
+ it('Emits a deleted event', () => {
const event = { message: 'Deleted!' };
findRunnerBulkDelete().vm.$emit('deleted', event);
diff --git a/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
index f089becd400..7ff3ec92042 100644
--- a/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
+++ b/spec/frontend/ci/runner/components/runner_membership_toggle_spec.js
@@ -18,10 +18,6 @@ describe('RunnerMembershipToggle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays text', () => {
createComponent({ mountFn: mount });
diff --git a/spec/frontend/ci/runner/components/runner_pagination_spec.js b/spec/frontend/ci/runner/components/runner_pagination_spec.js
index f835ee4514d..6d84eb810f8 100644
--- a/spec/frontend/ci/runner/components/runner_pagination_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pagination_spec.js
@@ -16,10 +16,6 @@ describe('RunnerPagination', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When in between pages', () => {
const mockPageInfo = {
startCursor: mockStartCursor,
diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
index 12680e01b98..350d029f3fc 100644
--- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
@@ -7,7 +7,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
I18N_PAUSE,
I18N_PAUSE_TOOLTIP,
@@ -22,7 +22,7 @@ const mockRunner = allRunnersData.data.runners.nodes[0];
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerPauseButton', () => {
@@ -46,7 +46,7 @@ describe('RunnerPauseButton', () => {
},
apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]),
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -74,10 +74,6 @@ describe('RunnerPauseButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Pause/Resume action', () => {
describe.each`
runnerState | icon | content | tooltip | isActive | newActiveValue
@@ -138,7 +134,7 @@ describe('RunnerPauseButton', () => {
await clickAndWait();
});
- it(`The mutation to that sets active to ${newActiveValue} is called`, async () => {
+ it(`The mutation to that sets active to ${newActiveValue} is called`, () => {
expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1);
expect(runnerToggleActiveHandler).toHaveBeenCalledWith({
input: {
diff --git a/spec/frontend/ci/runner/components/runner_paused_badge_spec.js b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
index b051ebe99a7..54768ea50da 100644
--- a/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_paused_badge_spec.js
@@ -16,7 +16,7 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -25,10 +25,6 @@ describe('RunnerTypeBadge', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders paused state', () => {
expect(wrapper.text()).toBe(I18N_PAUSED);
expect(findBadge().props('variant')).toBe('warning');
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
index db6fd2c369b..eddc1438fff 100644
--- a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
@@ -6,19 +6,12 @@ import {
LINUX_PLATFORM,
MACOS_PLATFORM,
WINDOWS_PLATFORM,
- AWS_PLATFORM,
DOCKER_HELP_URL,
KUBERNETES_HELP_URL,
} from '~/ci/runner/constants';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
-const mockProvide = {
- awsImgPath: 'awsLogo.svg',
- dockerImgPath: 'dockerLogo.svg',
- kubernetesImgPath: 'kubernetesLogo.svg',
-};
-
describe('RunnerPlatformsRadioGroup', () => {
let wrapper;
@@ -35,7 +28,6 @@ describe('RunnerPlatformsRadioGroup', () => {
value: null,
...props,
},
- provide: mockProvide,
...options,
});
};
@@ -48,10 +40,9 @@ describe('RunnerPlatformsRadioGroup', () => {
const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
expect(labels).toEqual([
- ['Linux', null],
+ ['Linux', expect.any(String)],
['macOS', null],
['Windows', null],
- ['AWS', expect.any(String)],
['Docker', expect.any(String)],
['Kubernetes', expect.any(String)],
]);
@@ -69,7 +60,6 @@ describe('RunnerPlatformsRadioGroup', () => {
${'Linux'} | ${LINUX_PLATFORM}
${'macOS'} | ${MACOS_PLATFORM}
${'Windows'} | ${WINDOWS_PLATFORM}
- ${'AWS'} | ${AWS_PLATFORM}
`('user can select "$text"', async ({ text, value }) => {
const radio = findFormRadioByText(text);
expect(radio.props('value')).toBe(value);
@@ -84,7 +74,7 @@ describe('RunnerPlatformsRadioGroup', () => {
text | href
${'Docker'} | ${DOCKER_HELP_URL}
${'Kubernetes'} | ${KUBERNETES_HELP_URL}
- `('provides link to "$text" docs', async ({ text, href }) => {
+ `('provides link to "$text" docs', ({ text, href }) => {
const radio = findFormRadioByText(text);
expect(radio.findComponent(GlLink).attributes()).toEqual({
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
index fb81edd1ae2..340b04637f8 100644
--- a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
@@ -41,7 +41,7 @@ describe('RunnerPlatformsRadio', () => {
expect(findFormRadio().attributes('value')).toBe(mockValue);
});
- it('emits when item is clicked', async () => {
+ it('emits when item is clicked', () => {
findDiv().trigger('click');
expect(wrapper.emitted('input')).toEqual([[mockValue]]);
@@ -94,7 +94,7 @@ describe('RunnerPlatformsRadio', () => {
expect(wrapper.classes('gl-cursor-pointer')).toBe(false);
});
- it('does not emit when item is clicked', async () => {
+ it('does not emit when item is clicked', () => {
findDiv().trigger('click');
expect(wrapper.emitted('input')).toBe(undefined);
diff --git a/spec/frontend/ci/runner/components/runner_projects_spec.js b/spec/frontend/ci/runner/components/runner_projects_spec.js
index 17517c4db66..736a1f7d3ce 100644
--- a/spec/frontend/ci/runner/components/runner_projects_spec.js
+++ b/spec/frontend/ci/runner/components/runner_projects_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { sprintf } from '~/locale';
import {
I18N_ASSIGNED_PROJECTS,
@@ -22,7 +22,7 @@ import runnerProjectsQuery from '~/ci/runner/graphql/show/runner_projects.query.
import { runnerData, runnerProjectsData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;
@@ -56,7 +56,6 @@ describe('RunnerProjects', () => {
afterEach(() => {
mockRunnerProjectsQuery.mockReset();
- wrapper.destroy();
});
it('Requests runner projects', async () => {
@@ -90,7 +89,7 @@ describe('RunnerProjects', () => {
await waitForPromises();
});
- it('Shows a heading', async () => {
+ it('Shows a heading', () => {
const expected = sprintf(I18N_ASSIGNED_PROJECTS, { projectCount: mockProjects.length });
expect(findHeading().text()).toBe(expected);
@@ -195,7 +194,7 @@ describe('RunnerProjects', () => {
expect(wrapper.findByText(I18N_NO_PROJECTS_FOUND).exists()).toBe(false);
expect(findRunnerAssignedItems().length).toBe(0);
- expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findRunnerPagination().attributes('disabled')).toBeDefined();
expect(findGlSearchBoxByType().props('isLoading')).toBe(true);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
index 45b410df2d4..e1eb81f2d23 100644
--- a/spec/frontend/ci/runner/components/runner_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_status_badge_spec.js
@@ -31,7 +31,7 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -43,8 +43,6 @@ describe('RunnerTypeBadge', () => {
afterEach(() => {
jest.useFakeTimers({ legacyFakeTimers: true });
-
- wrapper.destroy();
});
it('renders online state', () => {
diff --git a/spec/frontend/ci/runner/components/runner_tag_spec.js b/spec/frontend/ci/runner/components/runner_tag_spec.js
index 7bcb046ae43..e3d46e5d6df 100644
--- a/spec/frontend/ci/runner/components/runner_tag_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tag_spec.js
@@ -29,8 +29,8 @@ describe('RunnerTag', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
- GlResizeObserver: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
});
};
@@ -39,10 +39,6 @@ describe('RunnerTag', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays tag text', () => {
expect(wrapper.text()).toBe(mockTag);
});
diff --git a/spec/frontend/ci/runner/components/runner_tags_spec.js b/spec/frontend/ci/runner/components/runner_tags_spec.js
index 96bec00302b..bcb1d1f9e13 100644
--- a/spec/frontend/ci/runner/components/runner_tags_spec.js
+++ b/spec/frontend/ci/runner/components/runner_tags_spec.js
@@ -21,10 +21,6 @@ describe('RunnerTags', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays tags text', () => {
expect(wrapper.text()).toMatchInterpolatedText('tag1 tag2');
diff --git a/spec/frontend/ci/runner/components/runner_type_badge_spec.js b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
index 58f09362759..f7ecd108967 100644
--- a/spec/frontend/ci/runner/components/runner_type_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_badge_spec.js
@@ -2,6 +2,7 @@ import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RunnerTypeBadge from '~/ci/runner/components/runner_type_badge.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { assertProps } from 'helpers/assert_props';
import {
INSTANCE_TYPE,
GROUP_TYPE,
@@ -23,15 +24,11 @@ describe('RunnerTypeBadge', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
type | text
${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE}
@@ -54,7 +51,7 @@ describe('RunnerTypeBadge', () => {
it('validation fails for an incorrect type', () => {
expect(() => {
- createComponent({ props: { type: 'AN_UNKNOWN_VALUE' } });
+ assertProps(RunnerTypeBadge, { type: 'AN_UNKNOWN_VALUE' });
}).toThrow();
});
diff --git a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
index 3347c190083..71dcc5b4226 100644
--- a/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_type_tabs_spec.js
@@ -8,6 +8,7 @@ import {
PROJECT_TYPE,
DEFAULT_MEMBERSHIP,
DEFAULT_SORT,
+ STATUS_ONLINE,
} from '~/ci/runner/constants';
const mockSearch = {
@@ -63,10 +64,6 @@ describe('RunnerTypeTabs', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Renders all options to filter runners by default', () => {
createComponent();
@@ -115,7 +112,7 @@ describe('RunnerTypeTabs', () => {
it('Renders a count next to each tab', () => {
const mockVariables = {
paused: true,
- status: 'ONLINE',
+ status: STATUS_ONLINE,
};
createComponent({
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 a0e51ebf958..db4c236bfff 100644
--- a/spec/frontend/ci/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js
@@ -5,8 +5,8 @@ import { __ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
import {
INSTANCE_TYPE,
@@ -21,7 +21,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerFormData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -86,7 +86,7 @@ describe('RunnerUpdateForm', () => {
variant: VARIANT_SUCCESS,
}),
);
- expect(redirectTo).toHaveBeenCalledWith(mockRunnerPath);
+ expect(redirectTo).toHaveBeenCalledWith(mockRunnerPath); // eslint-disable-line import/no-deprecated
};
beforeEach(() => {
@@ -107,10 +107,6 @@ describe('RunnerUpdateForm', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Form has a submit button', () => {
expect(findSubmit().exists()).toBe(true);
});
@@ -282,7 +278,7 @@ describe('RunnerUpdateForm', () => {
expect(captureException).not.toHaveBeenCalled();
expect(saveAlertToLocalStorage).not.toHaveBeenCalled();
- expect(redirectTo).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index b7d9d3ad23e..e9f2e888b9a 100644
--- a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -3,14 +3,14 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+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 TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
...jest.requireActual('~/vue_shared/components/filtered_search_bar/filtered_search_utils'),
@@ -90,7 +90,6 @@ describe('TagToken', () => {
afterEach(() => {
getRecentlyUsedSuggestions.mockReset();
- wrapper.destroy();
});
describe('when the tags token is displayed', () => {
diff --git a/spec/frontend/ci/runner/components/stat/runner_count_spec.js b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
index 42d8c9a1080..df774ba3e57 100644
--- a/spec/frontend/ci/runner/components/stat/runner_count_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_count_spec.js
@@ -2,7 +2,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import RunnerCount from '~/ci/runner/components/stat/runner_count.vue';
-import { INSTANCE_TYPE, GROUP_TYPE } from '~/ci/runner/constants';
+import { INSTANCE_TYPE, GROUP_TYPE, STATUS_ONLINE } from '~/ci/runner/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
@@ -47,7 +47,7 @@ describe('RunnerCount', () => {
});
describe('in admin scope', () => {
- const mockVariables = { status: 'ONLINE' };
+ const mockVariables = { status: STATUS_ONLINE };
beforeEach(async () => {
await createComponent({ props: { scope: INSTANCE_TYPE } });
@@ -67,7 +67,7 @@ describe('RunnerCount', () => {
expect(wrapper.html()).toBe(`<strong>${runnersCountData.data.runners.count}</strong>`);
});
- it('does not fetch from the group query', async () => {
+ it('does not fetch from the group query', () => {
expect(mockGroupRunnersCountHandler).not.toHaveBeenCalled();
});
@@ -89,7 +89,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: INSTANCE_TYPE, skip: true } });
});
- it('does not fetch data', async () => {
+ it('does not fetch data', () => {
expect(mockRunnersCountHandler).not.toHaveBeenCalled();
expect(mockGroupRunnersCountHandler).not.toHaveBeenCalled();
@@ -106,7 +106,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: INSTANCE_TYPE } });
});
- it('data is not shown and error is reported', async () => {
+ it('data is not shown and error is reported', () => {
expect(wrapper.html()).toBe('<strong></strong>');
expect(captureException).toHaveBeenCalledWith({
@@ -121,7 +121,7 @@ describe('RunnerCount', () => {
await createComponent({ props: { scope: GROUP_TYPE } });
});
- it('fetches data from the group query', async () => {
+ it('fetches data from the group query', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes(1);
expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({});
@@ -141,7 +141,7 @@ describe('RunnerCount', () => {
wrapper.vm.refetch();
});
- it('data is not shown and error is reported', async () => {
+ it('data is not shown and error is reported', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(2);
});
});
diff --git a/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
index cad61f26012..f30b75ee614 100644
--- a/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_single_stat_spec.js
@@ -32,10 +32,6 @@ describe('RunnerStats', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
case | count | value
${'number'} | ${99} | ${'99'}
diff --git a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
index 3d45674d106..13366a788d5 100644
--- a/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
+++ b/spec/frontend/ci/runner/components/stat/runner_stats_spec.js
@@ -47,10 +47,6 @@ describe('RunnerStats', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Displays all the stats', () => {
createComponent({
mountFn: mount,
diff --git a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
new file mode 100644
index 00000000000..1c052b00fc3
--- /dev/null
+++ b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
@@ -0,0 +1,124 @@
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+
+import GroupRunnerRunnerApp from '~/ci/runner/group_new_runner/group_new_runner_app.vue';
+import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
+import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import {
+ PARAM_KEY_PLATFORM,
+ GROUP_TYPE,
+ DEFAULT_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { runnerCreateResult } from '../mock_data';
+
+const mockGroupId = 'gid://gitlab/Group/72';
+
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
+describe('GroupRunnerRunnerApp', () => {
+ let wrapper;
+
+ const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
+ const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
+ const findRegistrationCompatibilityAlert = () =>
+ wrapper.findComponent(RegistrationCompatibilityAlert);
+ const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupRunnerRunnerApp, {
+ propsData: {
+ groupId: mockGroupId,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a registration feedback banner', () => {
+ expect(findRegistrationFeedbackBanner().exists()).toBe(true);
+ });
+
+ it('shows a registration compatibility alert', () => {
+ expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockGroupId);
+ });
+
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: GROUP_TYPE,
+ groupId: mockGroupId,
+ projectId: null,
+ });
+ });
+
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
+ });
+ });
+
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
+
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js b/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js
new file mode 100644
index 00000000000..2f0807c700c
--- /dev/null
+++ b/spec/frontend/ci/runner/group_register_runner_app/group_register_runner_app_spec.js
@@ -0,0 +1,120 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import { updateHistory } from '~/lib/utils/url_utility';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import GroupRegisterRunnerApp from '~/ci/runner/group_register_runner/group_register_runner_app.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import { runnerForRegistration } from '../mock_data';
+
+const mockRunnerId = runnerForRegistration.data.runner.id;
+const mockRunnersPath = '/groups/group1/-/runners';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+describe('GroupRegisterRunnerApp', () => {
+ let wrapper;
+
+ const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
+ const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupRegisterRunnerApp, {
+ propsData: {
+ runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
+ },
+ });
+ };
+
+ describe('When showing runner details', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when runner token is available', () => {
+ it('shows registration instructions', () => {
+ expect(findRegistrationInstructions().props()).toEqual({
+ platform: DEFAULT_PLATFORM,
+ runnerId: mockRunnerId,
+ });
+ });
+
+ it('configures platform drawer', () => {
+ expect(findPlatformsDrawer().props()).toEqual({
+ open: false,
+ platform: DEFAULT_PLATFORM,
+ });
+ });
+
+ it('shows runner list button', () => {
+ expect(findBtn().attributes('href')).toBe(mockRunnersPath);
+ expect(findBtn().props('variant')).toBe('confirm');
+ });
+ });
+ });
+
+ describe('When another platform has been selected', () => {
+ beforeEach(() => {
+ setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
+
+ createComponent();
+ });
+
+ it('shows registration instructions for the platform', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+
+ describe('When opening install instructions', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ });
+
+ it('opens platform drawer', () => {
+ expect(findPlatformsDrawer().props('open')).toBe(true);
+ });
+
+ it('closes platform drawer', async () => {
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ it('closes platform drawer from drawer', async () => {
+ findPlatformsDrawer().vm.$emit('close');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ describe('when selecting a platform', () => {
+ beforeEach(() => {
+ findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
+ });
+
+ it('updates the url', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
+ });
+ });
+
+ it('updates the registration instructions', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index 2ad31dea774..0c594e8005c 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -4,8 +4,8 @@ import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');
@@ -74,7 +74,6 @@ describe('GroupRunnerShowApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
describe('When showing runner details', () => {
@@ -84,7 +83,7 @@ describe('GroupRunnerShowApp', () => {
await createComponent({ mountFn: mountExtended });
});
- it('expect GraphQL ID to be requested', async () => {
+ it('expect GraphQL ID to be requested', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
@@ -92,7 +91,7 @@ describe('GroupRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays the runner edit and pause buttons', async () => {
+ it('displays the runner edit and pause buttons', () => {
expect(findRunnerEditButton().attributes('href')).toBe(mockEditGroupRunnerPath);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
@@ -186,7 +185,7 @@ describe('GroupRunnerShowApp', () => {
message: 'Runner deleted',
variant: VARIANT_SUCCESS,
});
- expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath);
+ expect(redirectTo).toHaveBeenCalledWith(mockRunnersPath); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 39ea5cade28..41be72b1645 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -9,7 +9,7 @@ import {
mountExtended,
} from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -58,23 +58,22 @@ import {
groupRunnersCountData,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ mockRegistrationToken,
+ newRunnerPath,
emptyPageInfo,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
} from '../mock_data';
Vue.use(VueApollo);
Vue.use(GlToast);
const mockGroupFullPath = 'group1';
-const mockRegistrationToken = 'AABBCC';
const mockGroupRunnersEdges = groupRunnersData.data.group.runners.edges;
const mockGroupRunnersCount = mockGroupRunnersEdges.length;
const mockGroupRunnersHandler = jest.fn();
const mockGroupRunnersCountHandler = jest.fn();
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -87,6 +86,7 @@ describe('GroupRunnersApp', () => {
const findRunnerStats = () => wrapper.findComponent(RunnerStats);
const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell);
const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown);
+ const findNewRunnerBtn = () => wrapper.findByText(s__('Runners|New group runner'));
const findRunnerTypeTabs = () => wrapper.findComponent(RunnerTypeTabs);
const findRunnerList = () => wrapper.findComponent(RunnerList);
const findRunnerListEmptyState = () => wrapper.findComponent(RunnerListEmptyState);
@@ -114,14 +114,13 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
+ newRunnerPath,
...props,
},
provide: {
localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
- emptyStateSvgPath,
- emptyStateFilteredSvgPath,
...provide,
},
...options,
@@ -138,7 +137,6 @@ describe('GroupRunnersApp', () => {
afterEach(() => {
mockGroupRunnersHandler.mockReset();
mockGroupRunnersCountHandler.mockReset();
- wrapper.destroy();
});
it('shows the runner tabs with a runner count for each type', async () => {
@@ -288,7 +286,7 @@ describe('GroupRunnersApp', () => {
});
});
- it('When runner is paused or unpaused, some data is refetched', async () => {
+ it('When runner is paused or unpaused, some data is refetched', () => {
expect(mockGroupRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
findRunnerActionsCell().vm.$emit('toggledPaused');
@@ -300,7 +298,7 @@ describe('GroupRunnersApp', () => {
expect(showToast).toHaveBeenCalledTimes(0);
});
- it('When runner is deleted, data is refetched and a toast message is shown', async () => {
+ it('When runner is deleted, data is refetched and a toast message is shown', () => {
findRunnerActionsCell().vm.$emit('deleted', { message: 'Runner deleted' });
expect(showToast).toHaveBeenCalledTimes(1);
@@ -389,7 +387,7 @@ describe('GroupRunnersApp', () => {
it('when runners have not loaded, shows a loading state', () => {
createComponent();
expect(findRunnerList().props('loading')).toBe(true);
- expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ expect(findRunnerPagination().attributes('disabled')).toBeDefined();
});
it('runners can be deleted in bulk', () => {
@@ -417,8 +415,12 @@ describe('GroupRunnersApp', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('shows an empty state', async () => {
- expect(findRunnerListEmptyState().exists()).toBe(true);
+ it('shows an empty state', () => {
+ expect(findRunnerListEmptyState().props()).toMatchObject({
+ isSearchFiltered: false,
+ newRunnerPath,
+ registrationToken: mockRegistrationToken,
+ });
});
});
@@ -428,11 +430,11 @@ describe('GroupRunnersApp', () => {
await createComponent();
});
- it('error is shown to the user', async () => {
+ it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
});
- it('error is reported to sentry', async () => {
+ it('error is reported to sentry', () => {
expect(captureException).toHaveBeenCalledWith({
error: new Error('Error!'),
component: 'GroupRunnersApp',
@@ -469,32 +471,69 @@ describe('GroupRunnersApp', () => {
});
describe('when user has permission to register group runner', () => {
- beforeEach(() => {
+ it('shows the register group runner button', () => {
createComponent({
- propsData: {
+ props: {
registrationToken: mockRegistrationToken,
- groupFullPath: mockGroupFullPath,
},
});
+ expect(findRegistrationDropdown().exists()).toBe(true);
});
- it('shows the register group runner button', () => {
- expect(findRegistrationDropdown().exists()).toBe(true);
+ it('when create_runner_workflow_for_namespace is enabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: true,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
+ });
+
+ it('when create_runner_workflow_for_namespace is disabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: false,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().exists()).toBe(false);
});
});
describe('when user has no permission to register group runner', () => {
- beforeEach(() => {
+ it('does not show the register group runner button', () => {
createComponent({
- propsData: {
+ props: {
registrationToken: null,
- groupFullPath: mockGroupFullPath,
},
});
+ expect(findRegistrationDropdown().exists()).toBe(false);
});
- it('does not show the register group runner button', () => {
- expect(findRegistrationDropdown().exists()).toBe(false);
+ it('when create_runner_workflow_for_namespace is enabled', () => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures: {
+ createRunnerWorkflowForNamespace: true,
+ },
+ },
+ });
+
+ expect(findNewRunnerBtn().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
index 03908891cfd..30e49fc7644 100644
--- a/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
+++ b/spec/frontend/ci/runner/local_storage_alert/show_alert_from_local_storage_spec.js
@@ -2,9 +2,9 @@ import AccessorUtilities from '~/lib/utils/accessor';
import { showAlertFromLocalStorage } from '~/ci/runner/local_storage_alert/show_alert_from_local_storage';
import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('showAlertFromLocalStorage', () => {
useLocalStorageSpy();
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index 5cdf0ea4e3b..223a156795c 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -1,6 +1,19 @@
// Fixtures generated by: spec/frontend/fixtures/runner.rb
+// List queries
+import allRunnersWithCreatorData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.with_creator.json';
+import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
+import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
+import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json';
+import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
+import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
+import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
+
+// Register runner queries
+import runnerForRegistration from 'test_fixtures/graphql/ci/runner/register/runner_for_registration.query.graphql.json';
+
// Show runner queries
+import runnerCreateResult from 'test_fixtures/graphql/ci/runner/new/runner_create.mutation.graphql.json';
import runnerData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.json';
import runnerWithGroupData from 'test_fixtures/graphql/ci/runner/show/runner.query.graphql.with_group.json';
import runnerProjectsData from 'test_fixtures/graphql/ci/runner/show/runner_projects.query.graphql.json';
@@ -9,15 +22,16 @@ import runnerJobsData from 'test_fixtures/graphql/ci/runner/show/runner_jobs.que
// Edit runner queries
import runnerFormData from 'test_fixtures/graphql/ci/runner/edit/runner_form.query.graphql.json';
-// List queries
-import allRunnersData from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.json';
-import allRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/all_runners.query.graphql.paginated.json';
-import runnersCountData from 'test_fixtures/graphql/ci/runner/list/all_runners_count.query.graphql.json';
-import groupRunnersData from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.json';
-import groupRunnersDataPaginated from 'test_fixtures/graphql/ci/runner/list/group_runners.query.graphql.paginated.json';
-import groupRunnersCountData from 'test_fixtures/graphql/ci/runner/list/group_runners_count.query.graphql.json';
-
-import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/ci/runner/constants';
+// New runner queries
+import {
+ DEFAULT_MEMBERSHIP,
+ INSTANCE_TYPE,
+ CREATED_DESC,
+ CREATED_ASC,
+ STATUS_ONLINE,
+ STATUS_STALE,
+ RUNNER_PAGE_SIZE,
+} from '~/ci/runner/constants';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
const emptyPageInfo = {
@@ -40,29 +54,29 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
isDefault: true,
},
{
name: 'a single status',
- urlQuery: '?status[]=ACTIVE',
+ urlQuery: '?status[]=ONLINE',
search: {
runnerType: null,
membership: DEFAULT_MEMBERSHIP,
- filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
+ filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- status: 'ACTIVE',
- sort: 'CREATED_DESC',
+ status: STATUS_ONLINE,
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -79,12 +93,12 @@ export const mockSearchExamples = [
},
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
search: 'something',
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -105,12 +119,12 @@ export const mockSearchExamples = [
},
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
search: 'something else',
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -118,54 +132,54 @@ export const mockSearchExamples = [
name: 'single instance type',
urlQuery: '?runner_type[]=INSTANCE_TYPE',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
- type: 'INSTANCE_TYPE',
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'multiple runner status',
- urlQuery: '?status[]=ACTIVE&status[]=PAUSED',
+ urlQuery: '?status[]=ONLINE&status[]=STALE',
search: {
runnerType: null,
membership: DEFAULT_MEMBERSHIP,
filters: [
- { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
- { type: 'status', value: { data: 'PAUSED', operator: '=' } },
+ { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
+ { type: 'status', value: { data: STATUS_STALE, operator: '=' } },
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
- status: 'ACTIVE',
+ status: STATUS_ONLINE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
{
name: 'multiple status, a single instance type and a non default sort',
- urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
+ urlQuery: '?status[]=ONLINE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }],
+ filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }],
pagination: {},
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
},
graphqlVariables: {
- status: 'ACTIVE',
- type: 'INSTANCE_TYPE',
+ status: STATUS_ONLINE,
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -177,13 +191,13 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1'],
- first: 20,
- sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ sort: CREATED_DESC,
},
},
{
@@ -197,13 +211,13 @@ export const mockSearchExamples = [
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1', 'tag-2'],
- first: 20,
- sort: 'CREATED_DESC',
+ first: RUNNER_PAGE_SIZE,
+ sort: CREATED_DESC,
},
},
{
@@ -214,11 +228,11 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: { after: 'AFTER_CURSOR' },
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
},
@@ -231,11 +245,11 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [],
pagination: { before: 'BEFORE_CURSOR' },
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
before: 'BEFORE_CURSOR',
last: RUNNER_PAGE_SIZE,
},
@@ -243,24 +257,24 @@ export const mockSearchExamples = [
{
name: 'the next page filtered by a status, an instance type, tags and a non default sort',
urlQuery:
- '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR',
+ '?status[]=ONLINE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR',
search: {
- runnerType: 'INSTANCE_TYPE',
+ runnerType: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
filters: [
- { type: 'status', value: { data: 'ACTIVE', operator: '=' } },
+ { type: 'status', value: { data: STATUS_ONLINE, operator: '=' } },
{ type: 'tag', value: { data: 'tag-1', operator: '=' } },
{ type: 'tag', value: { data: 'tag-2', operator: '=' } },
],
pagination: { after: 'AFTER_CURSOR' },
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
},
graphqlVariables: {
- status: 'ACTIVE',
- type: 'INSTANCE_TYPE',
+ status: STATUS_ONLINE,
+ type: INSTANCE_TYPE,
membership: DEFAULT_MEMBERSHIP,
tagList: ['tag-1', 'tag-2'],
- sort: 'CREATED_ASC',
+ sort: CREATED_ASC,
after: 'AFTER_CURSOR',
first: RUNNER_PAGE_SIZE,
},
@@ -273,12 +287,12 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
paused: true,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -290,12 +304,12 @@ export const mockSearchExamples = [
membership: DEFAULT_MEMBERSHIP,
filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }],
pagination: {},
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
},
graphqlVariables: {
paused: false,
membership: DEFAULT_MEMBERSHIP,
- sort: 'CREATED_DESC',
+ sort: CREATED_DESC,
first: RUNNER_PAGE_SIZE,
},
},
@@ -304,12 +318,14 @@ export const mockSearchExamples = [
export const onlineContactTimeoutSecs = 2 * 60 * 60;
export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
+export const mockRegistrationToken = 'MOCK_REGISTRATION_TOKEN';
+export const mockAuthenticationToken = 'MOCK_AUTHENTICATION_TOKEN';
+
export const newRunnerPath = '/runners/new';
-export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
-export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';
export {
allRunnersData,
+ allRunnersWithCreatorData,
allRunnersDataPaginated,
runnersCountData,
groupRunnersData,
@@ -321,4 +337,6 @@ export {
runnerProjectsData,
runnerJobsData,
runnerFormData,
+ runnerCreateResult,
+ runnerForRegistration,
};
diff --git a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
new file mode 100644
index 00000000000..5bfbbfdc074
--- /dev/null
+++ b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
@@ -0,0 +1,125 @@
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+
+import ProjectRunnerRunnerApp from '~/ci/runner/project_new_runner/project_new_runner_app.vue';
+import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
+import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
+import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import {
+ PARAM_KEY_PLATFORM,
+ PROJECT_TYPE,
+ DEFAULT_PLATFORM,
+ WINDOWS_PLATFORM,
+} from '~/ci/runner/constants';
+import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { runnerCreateResult, mockRegistrationToken } from '../mock_data';
+
+const mockProjectId = 'gid://gitlab/Project/72';
+
+jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+const mockCreatedRunner = runnerCreateResult.data.runnerCreate.runner;
+
+describe('ProjectRunnerRunnerApp', () => {
+ let wrapper;
+
+ const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
+ const findRegistrationFeedbackBanner = () => wrapper.findComponent(RegistrationFeedbackBanner);
+ const findRegistrationCompatibilityAlert = () =>
+ wrapper.findComponent(RegistrationCompatibilityAlert);
+ const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProjectRunnerRunnerApp, {
+ propsData: {
+ projectId: mockProjectId,
+ legacyRegistrationToken: mockRegistrationToken,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows a registration feedback banner', () => {
+ expect(findRegistrationFeedbackBanner().exists()).toBe(true);
+ });
+
+ it('shows a registration compatibility alert', () => {
+ expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockProjectId);
+ });
+
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: PROJECT_TYPE,
+ projectId: mockProjectId,
+ groupId: null,
+ });
+ });
+
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
+ });
+ });
+
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+
+ expect(redirectTo).toHaveBeenCalledWith(url); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
+
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/project_register_runner_app/project_register_runner_app_spec.js b/spec/frontend/ci/runner/project_register_runner_app/project_register_runner_app_spec.js
new file mode 100644
index 00000000000..240fd82fb3b
--- /dev/null
+++ b/spec/frontend/ci/runner/project_register_runner_app/project_register_runner_app_spec.js
@@ -0,0 +1,120 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import { updateHistory } from '~/lib/utils/url_utility';
+import { PARAM_KEY_PLATFORM, DEFAULT_PLATFORM, WINDOWS_PLATFORM } from '~/ci/runner/constants';
+import ProjectRegisterRunnerApp from '~/ci/runner/project_register_runner/project_register_runner_app.vue';
+import RegistrationInstructions from '~/ci/runner/components/registration/registration_instructions.vue';
+import PlatformsDrawer from '~/ci/runner/components/registration/platforms_drawer.vue';
+import { runnerForRegistration } from '../mock_data';
+
+const mockRunnerId = runnerForRegistration.data.runner.id;
+const mockRunnersPath = '/group1/project1/-/settings/ci_cd';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ updateHistory: jest.fn(),
+}));
+
+describe('ProjectRegisterRunnerApp', () => {
+ let wrapper;
+
+ const findRegistrationInstructions = () => wrapper.findComponent(RegistrationInstructions);
+ const findPlatformsDrawer = () => wrapper.findComponent(PlatformsDrawer);
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProjectRegisterRunnerApp, {
+ propsData: {
+ runnerId: mockRunnerId,
+ runnersPath: mockRunnersPath,
+ },
+ });
+ };
+
+ describe('When showing runner details', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when runner token is available', () => {
+ it('shows registration instructions', () => {
+ expect(findRegistrationInstructions().props()).toEqual({
+ platform: DEFAULT_PLATFORM,
+ runnerId: mockRunnerId,
+ });
+ });
+
+ it('configures platform drawer', () => {
+ expect(findPlatformsDrawer().props()).toEqual({
+ open: false,
+ platform: DEFAULT_PLATFORM,
+ });
+ });
+
+ it('shows runner list button', () => {
+ expect(findBtn().attributes('href')).toBe(mockRunnersPath);
+ expect(findBtn().props('variant')).toBe('confirm');
+ });
+ });
+ });
+
+ describe('When another platform has been selected', () => {
+ beforeEach(() => {
+ setWindowLocation(`?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`);
+
+ createComponent();
+ });
+
+ it('shows registration instructions for the platform', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+
+ describe('When opening install instructions', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ });
+
+ it('opens platform drawer', () => {
+ expect(findPlatformsDrawer().props('open')).toBe(true);
+ });
+
+ it('closes platform drawer', async () => {
+ findRegistrationInstructions().vm.$emit('toggleDrawer');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ it('closes platform drawer from drawer', async () => {
+ findPlatformsDrawer().vm.$emit('close');
+ await nextTick();
+
+ expect(findPlatformsDrawer().props('open')).toBe(false);
+ });
+
+ describe('when selecting a platform', () => {
+ beforeEach(() => {
+ findPlatformsDrawer().vm.$emit('selectPlatform', WINDOWS_PLATFORM);
+ });
+
+ it('updates the url', () => {
+ expect(updateHistory).toHaveBeenCalledTimes(1);
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`,
+ });
+ });
+
+ it('updates the registration instructions', () => {
+ expect(findRegistrationInstructions().props('platform')).toBe(WINDOWS_PLATFORM);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
index a9369a5e626..79bbf95f8f0 100644
--- a/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
+++ b/spec/frontend/ci/runner/runner_edit/runner_edit_app_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
@@ -15,7 +15,7 @@ import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/ci/runner/con
import { runnerFormData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerFormData.data.runner;
@@ -51,7 +51,6 @@ describe('RunnerEditApp', () => {
afterEach(() => {
mockRunnerQuery.mockReset();
- wrapper.destroy();
});
it('expect GraphQL ID to be requested', async () => {
diff --git a/spec/frontend/ci/runner/runner_search_utils_spec.js b/spec/frontend/ci/runner/runner_search_utils_spec.js
index f64b89d47fd..9a4a6139198 100644
--- a/spec/frontend/ci/runner/runner_search_utils_spec.js
+++ b/spec/frontend/ci/runner/runner_search_utils_spec.js
@@ -7,6 +7,7 @@ import {
isSearchFiltered,
} from 'ee_else_ce/ci/runner/runner_search_utils';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import { DEFAULT_SORT } from '~/ci/runner/constants';
import { mockSearchExamples } from './mock_data';
describe('search_params.js', () => {
@@ -68,7 +69,7 @@ describe('search_params.js', () => {
'http://test.host/?paused[]=true',
'http://test.host/?search=my_text',
])('When a filter is removed, it is removed from the URL', (initialUrl) => {
- const search = { filters: [], sort: 'CREATED_DESC' };
+ const search = { filters: [], sort: DEFAULT_SORT };
const expectedUrl = `http://test.host/`;
expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
@@ -76,7 +77,7 @@ describe('search_params.js', () => {
it('When unrelated search parameter is present, it does not get removed', () => {
const initialUrl = `http://test.host/?unrelated=UNRELATED&status[]=ACTIVE`;
- const search = { filters: [], sort: 'CREATED_DESC' };
+ const search = { filters: [], sort: DEFAULT_SORT };
const expectedUrl = `http://test.host/?unrelated=UNRELATED`;
expect(fromSearchToUrl(search, initialUrl)).toBe(expectedUrl);
diff --git a/spec/frontend/ci/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index f7b689272ce..2f17cc43ac5 100644
--- a/spec/frontend/ci/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -6,7 +6,7 @@ jest.mock('@sentry/browser');
describe('~/ci/runner/sentry_utils', () => {
let mockSetTag;
- beforeEach(async () => {
+ beforeEach(() => {
mockSetTag = jest.fn();
Sentry.withScope.mockImplementation((fn) => {
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 b2084e3a7de..79194c20ff5 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
@@ -15,7 +15,7 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
>
<table
- aria-busy="false"
+ aria-busy=""
aria-colcount="2"
class="table b-table gl-table"
role="table"
@@ -168,7 +168,7 @@ exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the
role="cell"
>
- April 26, 2022 at 7:20:40 PM GMT
+ April 26, 2023 at 7:20:39 PM GMT
</td>
</tr>
@@ -196,7 +196,7 @@ exports[`Secure File Metadata Modal when a .mobileprovision file is supplied mat
>
<table
- aria-busy="false"
+ aria-busy=""
aria-colcount="2"
class="table b-table gl-table"
role="table"
diff --git a/spec/frontend/ci_secure_files/components/metadata/button_spec.js b/spec/frontend/ci_secure_files/components/metadata/button_spec.js
index 4ac5b3325d4..5bd4bab25af 100644
--- a/spec/frontend/ci_secure_files/components/metadata/button_spec.js
+++ b/spec/frontend/ci_secure_files/components/metadata/button_spec.js
@@ -12,10 +12,6 @@ describe('Secure File Metadata Button', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
const createWrapper = (secureFile = {}, admin = false) => {
wrapper = mount(Button, {
propsData: {
diff --git a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
index 230507d32d7..e181d15f2f9 100644
--- a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
+++ b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
@@ -37,7 +37,6 @@ describe('Secure File Metadata Modal', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
});
describe('when a .cer file is supplied', () => {
diff --git a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
index ab6200ca6f4..17b5fdc4dde 100644
--- a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
+++ b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
@@ -15,11 +15,6 @@ const dummyApiVersion = 'v3000';
const dummyProjectId = 1;
const fileSizeLimit = 5;
const dummyUrlRoot = '/gitlab';
-const dummyGon = {
- api_version: dummyApiVersion,
- relative_url_root: dummyUrlRoot,
-};
-let originalGon;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${dummyProjectId}/secure_files`;
describe('SecureFilesList', () => {
@@ -28,16 +23,16 @@ describe('SecureFilesList', () => {
let trackingSpy;
beforeEach(() => {
- originalGon = window.gon;
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+ };
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
unmockTracking();
- window.gon = originalGon;
});
const createWrapper = (admin = true, props = {}) => {
diff --git a/spec/frontend/ci_secure_files/mock_data.js b/spec/frontend/ci_secure_files/mock_data.js
index f532b468fb9..b3db0f7dd64 100644
--- a/spec/frontend/ci_secure_files/mock_data.js
+++ b/spec/frontend/ci_secure_files/mock_data.js
@@ -24,7 +24,7 @@ export const secureFiles = [
checksum_algorithm: 'sha256',
created_at: '2022-02-22T22:22:22.222Z',
file_extension: 'cer',
- expires_at: '2022-04-26T19:20:40.000Z',
+ expires_at: '2023-04-26T19:20:39.000Z',
metadata: {
id: '33669367788748363528491290218354043267',
issuer: {
@@ -40,7 +40,7 @@ export const secureFiles = [
OU: 'ABC123XYZ',
UID: 'ABC123XYZ',
},
- expires_at: '2022-04-26T19:20:40.000Z',
+ expires_at: '2023-04-26T19:20:39.000Z',
},
},
{
diff --git a/spec/frontend/clusters/agents/components/activity_events_list_spec.js b/spec/frontend/clusters/agents/components/activity_events_list_spec.js
index 6b374b6620d..770815a9403 100644
--- a/spec/frontend/clusters/agents/components/activity_events_list_spec.js
+++ b/spec/frontend/clusters/agents/components/activity_events_list_spec.js
@@ -44,10 +44,6 @@ describe('ActivityEvents', () => {
const findAllActivityHistoryItems = () => wrapper.findAllComponents(ActivityHistoryItem);
const findSectionTitle = (at) => wrapper.findAllByTestId('activity-section-title').at(at);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('while the agentEvents query is loading', () => {
it('displays a loading icon', async () => {
createWrapper();
diff --git a/spec/frontend/clusters/agents/components/activity_history_item_spec.js b/spec/frontend/clusters/agents/components/activity_history_item_spec.js
index 68f6f11aa8f..48460519c6c 100644
--- a/spec/frontend/clusters/agents/components/activity_history_item_spec.js
+++ b/spec/frontend/clusters/agents/components/activity_history_item_spec.js
@@ -25,10 +25,6 @@ describe('ActivityHistoryItem', () => {
const findHistoryItem = () => wrapper.findComponent(HistoryItem);
const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
kind | icon | title | lineNumber
${'token_created'} | ${EVENT_DETAILS.token_created.eventTypeIcon} | ${sprintf(EVENT_DETAILS.token_created.title, { tokenName: agentName })} | ${0}
diff --git a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
index db1219ccb41..ac0ce89f334 100644
--- a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
+++ b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
@@ -25,10 +25,6 @@ describe('IntegrationStatus', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findBadge = () => wrapper.findComponent(GlBadge);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('icon', () => {
const icon = 'status-success';
const iconClass = 'gl-text-green-500';
diff --git a/spec/frontend/clusters/agents/components/create_token_button_spec.js b/spec/frontend/clusters/agents/components/create_token_button_spec.js
index 73856b74a8d..2bbde33d6f4 100644
--- a/spec/frontend/clusters/agents/components/create_token_button_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_button_spec.js
@@ -21,7 +21,7 @@ describe('CreateTokenButton', () => {
...provideData,
},
directives: {
- GlModalDirective: createMockDirective(),
+ GlModalDirective: createMockDirective('gl-modal-directive'),
},
stubs: {
GlTooltip,
@@ -29,10 +29,6 @@ describe('CreateTokenButton', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when user can create token', () => {
beforeEach(() => {
createWrapper();
@@ -59,7 +55,7 @@ describe('CreateTokenButton', () => {
});
it('disabled the button', () => {
- expect(findButton().attributes('disabled')).toBe('true');
+ expect(findButton().attributes('disabled')).toBeDefined();
});
it('shows a disabled tooltip', () => {
diff --git a/spec/frontend/clusters/agents/components/create_token_modal_spec.js b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
index 0d10801e80e..f0fded7b7b2 100644
--- a/spec/frontend/clusters/agents/components/create_token_modal_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
@@ -9,7 +9,6 @@ import {
EVENT_LABEL_MODAL,
EVENT_ACTIONS_OPEN,
TOKEN_NAME_LIMIT,
- TOKEN_STATUS_ACTIVE,
MAX_LIST_COUNT,
CREATE_TOKEN_MODAL,
} from '~/clusters/agents/constants';
@@ -62,7 +61,7 @@ describe('CreateTokenModal', () => {
const expectDisabledAttribute = (element, disabled) => {
if (disabled) {
- expect(element.attributes('disabled')).toBe('true');
+ expect(element.attributes('disabled')).toBeDefined();
} else {
expect(element.attributes('disabled')).toBeUndefined();
}
@@ -81,7 +80,6 @@ describe('CreateTokenModal', () => {
variables: {
agentName,
projectPath,
- tokenStatus: TOKEN_STATUS_ACTIVE,
...cursor,
},
});
@@ -119,7 +117,6 @@ describe('CreateTokenModal', () => {
});
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
createResponse = null;
});
@@ -214,7 +211,7 @@ describe('CreateTokenModal', () => {
await mockCreatedResponse(createAgentTokenErrorResponse);
});
- it('displays the error message', async () => {
+ it('displays the error message', () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
diff --git a/spec/frontend/clusters/agents/components/integration_status_spec.js b/spec/frontend/clusters/agents/components/integration_status_spec.js
index 36f0e622452..28a59391578 100644
--- a/spec/frontend/clusters/agents/components/integration_status_spec.js
+++ b/spec/frontend/clusters/agents/components/integration_status_spec.js
@@ -27,10 +27,6 @@ describe('IntegrationStatus', () => {
const findAgentStatus = () => wrapper.findByTestId('agent-status');
const findAgentIntegrationStatusRows = () => wrapper.findAllComponents(AgentIntegrationStatusRow);
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
lastUsedAt | status | iconName
${null} | ${'Never connected'} | ${'status-neutral'}
diff --git a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
index 6521221cbd7..970782a8e58 100644
--- a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
+++ b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
@@ -8,7 +8,7 @@ import { ENTER_KEY } from '~/lib/utils/keys';
import RevokeTokenButton from '~/clusters/agents/components/revoke_token_button.vue';
import getClusterAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql';
import revokeTokenMutation from '~/clusters/agents/graphql/mutations/revoke_token.mutation.graphql';
-import { TOKEN_STATUS_ACTIVE, MAX_LIST_COUNT } from '~/clusters/agents/constants';
+import { MAX_LIST_COUNT } from '~/clusters/agents/constants';
import { getTokenResponse, mockRevokeResponse, mockErrorRevokeResponse } from '../../mock_data';
Vue.use(VueApollo);
@@ -45,7 +45,7 @@ describe('RevokeTokenButton', () => {
const findInput = () => wrapper.findComponent(GlFormInput);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findPrimaryAction = () => findModal().props('actionPrimary');
- const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr];
const createMockApolloProvider = ({ mutationResponse }) => {
revokeSpy = jest.fn().mockResolvedValue(mutationResponse);
@@ -59,7 +59,6 @@ describe('RevokeTokenButton', () => {
variables: {
agentName,
projectPath,
- tokenStatus: TOKEN_STATUS_ACTIVE,
...cursor,
},
data: getTokenResponse.data,
@@ -105,7 +104,6 @@ describe('RevokeTokenButton', () => {
});
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
revokeSpy = null;
});
@@ -121,7 +119,7 @@ describe('RevokeTokenButton', () => {
});
it('disabled the button', () => {
- expect(findRevokeBtn().attributes('disabled')).toBe('true');
+ expect(findRevokeBtn().attributes('disabled')).toBeDefined();
});
it('shows a disabled tooltip', () => {
@@ -219,7 +217,7 @@ describe('RevokeTokenButton', () => {
it('reenables the button', async () => {
expect(findPrimaryActionAttributes('loading')).toBe(true);
- expect(findRevokeBtn().attributes('disabled')).toBe('true');
+ expect(findRevokeBtn().attributes('disabled')).toBeDefined();
await findModal().vm.$emit('hide');
diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js
index efa85136b17..019f789d875 100644
--- a/spec/frontend/clusters/agents/components/show_spec.js
+++ b/spec/frontend/clusters/agents/components/show_spec.js
@@ -12,7 +12,7 @@ import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { MAX_LIST_COUNT, TOKEN_STATUS_ACTIVE } from '~/clusters/agents/constants';
+import { MAX_LIST_COUNT } from '~/clusters/agents/constants';
const localVue = createLocalVue();
localVue.use(VueApollo);
@@ -79,10 +79,6 @@ describe('ClusterAgentShow', () => {
const findActivity = () => wrapper.findComponent(ActivityEvents);
const findIntegrationStatus = () => wrapper.findComponent(IntegrationStatus);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default behaviour', () => {
beforeEach(async () => {
createWrapper({ clusterAgent: defaultClusterAgent });
@@ -93,7 +89,6 @@ describe('ClusterAgentShow', () => {
const variables = {
agentName: provide.agentName,
projectPath: provide.projectPath,
- tokenStatus: TOKEN_STATUS_ACTIVE,
first: MAX_LIST_COUNT,
last: null,
};
diff --git a/spec/frontend/clusters/agents/components/token_table_spec.js b/spec/frontend/clusters/agents/components/token_table_spec.js
index 334615f1818..1a6aeedb694 100644
--- a/spec/frontend/clusters/agents/components/token_table_spec.js
+++ b/spec/frontend/clusters/agents/components/token_table_spec.js
@@ -57,10 +57,6 @@ describe('ClusterAgentTokenTable', () => {
return createComponent(defaultTokens);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the create token button', () => {
expect(findCreateTokenBtn().exists()).toBe(true);
});
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index a2ec19c5b4a..d657566713f 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlShowCluster from 'test_fixtures/clusters/show_cluster.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import Clusters from '~/clusters/clusters_bundle';
import axios from '~/lib/utils/axios_utils';
@@ -24,7 +25,7 @@ describe('Clusters', () => {
};
beforeEach(() => {
- loadHTMLFixture('clusters/show_cluster.html');
+ setHTMLFixture(htmlShowCluster);
mockGetClusterStatusRequest();
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 656e72baf77..21ffda8578a 100644
--- a/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/new_cluster_spec.js.snap
@@ -3,7 +3,7 @@
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\\" event=\\"click\\" routertag=\\"a\\" class=\\"gl-link\\">How do I use a certificate to connect to my cluster?</b-link-stub>
+ <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>
</p>
</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 46ee123a12d..67b0ecdf7eb 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
@@ -43,3 +43,212 @@ exports[`Remove cluster confirmation modal renders buttons with modal included 1
<!---->
</div>
`;
+
+exports[`Remove cluster confirmation modal two buttons open modal with "cleanup" option 1`] = `
+<div
+ class="gl-display-flex"
+>
+ <button
+ class="btn gl-mr-3 btn-danger btn-md gl-button"
+ data-testid="remove-integration-and-resources-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Remove integration and resources
+
+ </span>
+ </button>
+
+ <button
+ class="btn btn-danger btn-md gl-button btn-danger-secondary"
+ data-testid="remove-integration-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ 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
+ </code>
+ </li>
+ </ul>
+ </div>
+
+ <strong>
+ To remove your integration and resources, type
+ <code>
+ my-test-cluster
+ </code>
+ to confirm:
+ </strong>
+
+ <form
+ action="clusterPath"
+ class="gl-mb-5"
+ method="post"
+ >
+ <input
+ name="_method"
+ 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"
+ 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>
+ </div>
+</div>
+`;
+
+exports[`Remove cluster confirmation modal two buttons open modal without "cleanup" option 1`] = `
+<div
+ class="gl-display-flex"
+>
+ <button
+ class="btn gl-mr-3 btn-danger btn-md gl-button"
+ data-testid="remove-integration-and-resources-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Remove integration and resources
+
+ </span>
+ </button>
+
+ <button
+ class="btn btn-danger btn-md gl-button btn-danger-secondary"
+ data-testid="remove-integration-button"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Remove integration
+
+ </span>
+ </button>
+
+ <div
+ kind="danger"
+ >
+ <p>
+ You are about to remove your cluster integration.
+ </p>
+
+ <!---->
+
+ <strong>
+ To remove your integration, type
+ <code>
+ my-test-cluster
+ </code>
+ to confirm:
+ </strong>
+
+ <form
+ action="clusterPath"
+ class="gl-mb-5"
+ method="post"
+ >
+ <input
+ name="_method"
+ 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"
+ name="confirm_cluster_name_input"
+ type="text"
+ />
+ </form>
+
+ <!---->
+ </div>
+</div>
+`;
diff --git a/spec/frontend/clusters/components/new_cluster_spec.js b/spec/frontend/clusters/components/new_cluster_spec.js
index ef39c90aaef..398b472a3a7 100644
--- a/spec/frontend/clusters/components/new_cluster_spec.js
+++ b/spec/frontend/clusters/components/new_cluster_spec.js
@@ -20,10 +20,6 @@ describe('NewCluster', () => {
return createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the cluster component correctly', () => {
expect(wrapper.html()).toMatchSnapshot();
});
diff --git a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
index 53683af893a..04b7909b534 100644
--- a/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
+++ b/spec/frontend/clusters/components/remove_cluster_confirmation_spec.js
@@ -6,6 +6,7 @@ import RemoveClusterConfirmation from '~/clusters/components/remove_cluster_conf
describe('Remove cluster confirmation modal', () => {
let wrapper;
+ const showMock = jest.fn();
const createComponent = ({ props = {}, stubs = {} } = {}) => {
wrapper = mount(RemoveClusterConfirmation, {
@@ -18,11 +19,6 @@ describe('Remove cluster confirmation modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders buttons with modal included', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
@@ -38,9 +34,13 @@ describe('Remove cluster confirmation modal', () => {
beforeEach(() => {
createComponent({
props: { clusterName: 'my-test-cluster' },
- stubs: { GlSprintf, GlModal: stubComponent(GlModal) },
+ stubs: {
+ GlSprintf,
+ GlModal: stubComponent(GlModal, {
+ methods: { show: showMock },
+ }),
+ },
});
- jest.spyOn(findModal().vm, 'show').mockReturnValue();
});
it('open modal with "cleanup" option', async () => {
@@ -48,8 +48,8 @@ describe('Remove cluster confirmation modal', () => {
await nextTick();
- expect(findModal().vm.show).toHaveBeenCalled();
- expect(wrapper.vm.confirmCleanup).toEqual(true);
+ expect(showMock).toHaveBeenCalled();
+ expect(wrapper.element).toMatchSnapshot();
expect(findModal().html()).toContain(
'<strong>To remove your integration and resources, type <code>my-test-cluster</code> to confirm:</strong>',
);
@@ -60,8 +60,8 @@ describe('Remove cluster confirmation modal', () => {
await nextTick();
- expect(findModal().vm.show).toHaveBeenCalled();
- expect(wrapper.vm.confirmCleanup).toEqual(false);
+ expect(showMock).toHaveBeenCalled();
+ expect(wrapper.element).toMatchSnapshot();
expect(findModal().html()).toContain(
'<strong>To remove your integration, type <code>my-test-cluster</code> to confirm:</strong>',
);
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
index b17886a5826..396f8215b9f 100644
--- a/spec/frontend/clusters/forms/components/integration_form_spec.js
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -1,6 +1,6 @@
import { GlToggle, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
import IntegrationForm from '~/clusters/forms/components/integration_form.vue';
import { createStore } from '~/clusters/forms/stores/index';
@@ -27,17 +27,9 @@ describe('ClusterIntegrationForm', () => {
});
};
- const destroyWrapper = () => {
- wrapper.destroy();
- wrapper = null;
- };
-
const findSubmitButton = () => wrapper.findComponent(GlButton);
const findGlToggle = () => wrapper.findComponent(GlToggle);
-
- afterEach(() => {
- destroyWrapper();
- });
+ const findClusterEnvironmentScopeInput = () => wrapper.find('[id="cluster_environment_scope"]');
describe('rendering', () => {
beforeEach(() => createWrapper());
@@ -50,7 +42,9 @@ describe('ClusterIntegrationForm', () => {
});
it('sets the envScope to default', () => {
- expect(wrapper.find('[id="cluster_environment_scope"]').attributes('value')).toBe('*');
+ expect(findClusterEnvironmentScopeInput().attributes('value')).toBe(
+ defaultStoreValues.environmentScope,
+ );
});
it('sets the baseDomain to default', () => {
@@ -76,20 +70,15 @@ describe('ClusterIntegrationForm', () => {
beforeEach(() => createWrapper());
it('enables the submit button on changing toggle to different value', async () => {
- await nextTick();
- // setData is a bad approach because it changes the internal implementation which we should not touch
- // but our GlFormInput lacks the ability to set a new value.
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
+ await findGlToggle().vm.$emit('change', false);
expect(findSubmitButton().props('disabled')).toBe(false);
});
it('enables the submit button on changing input values', async () => {
- await nextTick();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
+ await findClusterEnvironmentScopeInput().vm.$emit(
+ 'input',
+ `${defaultStoreValues.environmentScope}1`,
+ );
expect(findSubmitButton().props('disabled')).toBe(false);
});
});
diff --git a/spec/frontend/clusters_list/components/agent_empty_state_spec.js b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
index 22775aa6603..2e52d16c739 100644
--- a/spec/frontend/clusters_list/components/agent_empty_state_spec.js
+++ b/spec/frontend/clusters_list/components/agent_empty_state_spec.js
@@ -22,12 +22,6 @@ describe('AgentEmptyStateComponent', () => {
});
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
it('renders the empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js
index 9cbb83eedd2..0f68a69458e 100644
--- a/spec/frontend/clusters_list/components/agent_table_spec.js
+++ b/spec/frontend/clusters_list/components/agent_table_spec.js
@@ -13,9 +13,9 @@ const defaultConfigHelpUrl =
const provideData = {
gitlabVersion: '14.8',
- kasVersion: '14.8',
+ kasVersion: '14.8.0',
};
-const propsData = {
+const defaultProps = {
agents: clusterAgents,
};
@@ -26,9 +26,6 @@ const DeleteAgentButtonStub = stubComponent(DeleteAgentButton, {
const outdatedTitle = I18N_AGENT_TABLE.versionOutdatedTitle;
const mismatchTitle = I18N_AGENT_TABLE.versionMismatchTitle;
const mismatchOutdatedTitle = I18N_AGENT_TABLE.versionMismatchOutdatedTitle;
-const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
- version: provideData.kasVersion,
-});
const mismatchText = I18N_AGENT_TABLE.versionMismatchText;
describe('AgentTable', () => {
@@ -39,127 +36,150 @@ describe('AgentTable', () => {
const findStatusIcon = (at) => findStatusText(at).findComponent(GlIcon);
const findLastContactText = (at) => wrapper.findAllByTestId('cluster-agent-last-contact').at(at);
const findVersionText = (at) => wrapper.findAllByTestId('cluster-agent-version').at(at);
+ const findAgentId = (at) => wrapper.findAllByTestId('cluster-agent-id').at(at);
const findConfiguration = (at) =>
wrapper.findAllByTestId('cluster-agent-configuration-link').at(at);
const findDeleteAgentButton = () => wrapper.findAllComponents(DeleteAgentButton);
- beforeEach(() => {
+ const createWrapper = ({ provide = provideData, propsData = defaultProps } = {}) => {
wrapper = mountExtended(AgentTable, {
propsData,
- provide: provideData,
+ provide,
stubs: {
DeleteAgentButton: DeleteAgentButtonStub,
},
});
- });
-
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
+ };
describe('agent table', () => {
- it.each`
- agentName | link | lineNumber
- ${'agent-1'} | ${'/agent-1'} | ${0}
- ${'agent-2'} | ${'/agent-2'} | ${1}
- `('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
- expect(findAgentLink(lineNumber).text()).toBe(agentName);
- expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it.each`
+ agentName | link | lineNumber
+ ${'agent-1'} | ${'/agent-1'} | ${0}
+ ${'agent-2'} | ${'/agent-2'} | ${1}
+ `('displays agent link for $agentName', ({ agentName, link, lineNumber }) => {
+ expect(findAgentLink(lineNumber).text()).toBe(agentName);
+ expect(findAgentLink(lineNumber).attributes('href')).toBe(link);
+ });
+
+ it.each`
+ agentGraphQLId | agentId | lineNumber
+ ${'gid://gitlab/Clusters::Agent/1'} | ${'1'} | ${0}
+ ${'gid://gitlab/Clusters::Agent/2'} | ${'2'} | ${1}
+ `(
+ 'displays agent id as "$agentId" for "$agentGraphQLId" at line $lineNumber',
+ ({ agentId, lineNumber }) => {
+ expect(findAgentId(lineNumber).text()).toBe(agentId);
+ },
+ );
+
+ it.each`
+ status | iconName | lineNumber
+ ${'Never connected'} | ${'status-neutral'} | ${0}
+ ${'Connected'} | ${'status-success'} | ${1}
+ ${'Not connected'} | ${'status-alert'} | ${2}
+ `(
+ 'displays agent connection status as "$status" at line $lineNumber',
+ ({ status, iconName, lineNumber }) => {
+ expect(findStatusText(lineNumber).text()).toBe(status);
+ expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
+ },
+ );
+
+ it.each`
+ lastContact | lineNumber
+ ${'Never'} | ${0}
+ ${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
+ ${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
+ `(
+ 'displays agent last contact time as "$lastContact" at line $lineNumber',
+ ({ lastContact, lineNumber }) => {
+ expect(findLastContactText(lineNumber).text()).toBe(lastContact);
+ },
+ );
+
+ it.each`
+ agentConfig | link | lineNumber
+ ${'.gitlab/agents/agent-1'} | ${'/agent/full/path'} | ${0}
+ ${'Default configuration'} | ${defaultConfigHelpUrl} | ${1}
+ `(
+ 'displays config file path as "$agentPath" at line $lineNumber',
+ ({ agentConfig, link, lineNumber }) => {
+ const findLink = findConfiguration(lineNumber).findComponent(GlLink);
+
+ expect(findLink.attributes('href')).toBe(link);
+ expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
+ },
+ );
+
+ it('displays actions menu for each agent', () => {
+ expect(findDeleteAgentButton()).toHaveLength(clusterAgents.length);
+ });
});
- it.each`
- status | iconName | lineNumber
- ${'Never connected'} | ${'status-neutral'} | ${0}
- ${'Connected'} | ${'status-success'} | ${1}
- ${'Not connected'} | ${'status-alert'} | ${2}
+ describe.each`
+ agentMockIdx | agentVersion | kasVersion | versionMismatch | versionOutdated | title
+ ${0} | ${''} | ${'14.8.0'} | ${false} | ${false} | ${''}
+ ${1} | ${'14.8.0'} | ${'14.8.0'} | ${false} | ${false} | ${''}
+ ${2} | ${'14.6.0'} | ${'14.8.0'} | ${false} | ${true} | ${outdatedTitle}
+ ${3} | ${'14.7.0'} | ${'14.8.0'} | ${true} | ${false} | ${mismatchTitle}
+ ${4} | ${'14.3.0'} | ${'14.8.0'} | ${true} | ${true} | ${mismatchOutdatedTitle}
+ ${5} | ${'14.6.0'} | ${'14.8.0-rc1'} | ${false} | ${false} | ${''}
+ ${6} | ${'14.8.0'} | ${'15.0.0'} | ${false} | ${true} | ${outdatedTitle}
+ ${7} | ${'14.8.0'} | ${'15.0.0-rc1'} | ${false} | ${true} | ${outdatedTitle}
+ ${8} | ${'14.8.0'} | ${'14.8.10'} | ${false} | ${false} | ${''}
`(
- 'displays agent connection status as "$status" at line $lineNumber',
- ({ status, iconName, lineNumber }) => {
- expect(findStatusText(lineNumber).text()).toBe(status);
- expect(findStatusIcon(lineNumber).props('name')).toBe(iconName);
- },
- );
+ 'when agent version is "$agentVersion", KAS version is "$kasVersion" and version mismatch is "$versionMismatch"',
+ ({ agentMockIdx, agentVersion, kasVersion, versionMismatch, versionOutdated, title }) => {
+ const currentAgent = clusterAgents[agentMockIdx];
- it.each`
- lastContact | lineNumber
- ${'Never'} | ${0}
- ${timeagoMixin.methods.timeFormatted(connectedTimeNow)} | ${1}
- ${timeagoMixin.methods.timeFormatted(connectedTimeInactive)} | ${2}
- `(
- 'displays agent last contact time as "$lastContact" at line $lineNumber',
- ({ lastContact, lineNumber }) => {
- expect(findLastContactText(lineNumber).text()).toBe(lastContact);
- },
- );
+ const findIcon = () => findVersionText(0).findComponent(GlIcon);
+ const findPopover = () => wrapper.findByTestId(`popover-${currentAgent.name}`);
- describe.each`
- agent | version | podsNumber | versionMismatch | versionOutdated | title | texts | lineNumber
- ${'agent-1'} | ${''} | ${1} | ${false} | ${false} | ${''} | ${''} | ${0}
- ${'agent-2'} | ${'14.8'} | ${2} | ${false} | ${false} | ${''} | ${''} | ${1}
- ${'agent-3'} | ${'14.5'} | ${1} | ${false} | ${true} | ${outdatedTitle} | ${[outdatedText]} | ${2}
- ${'agent-4'} | ${'14.7'} | ${2} | ${true} | ${false} | ${mismatchTitle} | ${[mismatchText]} | ${3}
- ${'agent-5'} | ${'14.3'} | ${2} | ${true} | ${true} | ${mismatchOutdatedTitle} | ${[mismatchText, outdatedText]} | ${4}
- `(
- 'agent version column at line $lineNumber',
- ({
- agent,
- version,
- podsNumber,
- versionMismatch,
- versionOutdated,
- title,
- texts,
- lineNumber,
- }) => {
- const findIcon = () => findVersionText(lineNumber).findComponent(GlIcon);
- const findPopover = () => wrapper.findByTestId(`popover-${agent}`);
const versionWarning = versionMismatch || versionOutdated;
+ const outdatedText = sprintf(I18N_AGENT_TABLE.versionOutdatedText, {
+ version: kasVersion,
+ });
- it('shows the correct agent version', () => {
- expect(findVersionText(lineNumber).text()).toBe(version);
+ beforeEach(() => {
+ createWrapper({
+ provide: { gitlabVersion: '14.8', kasVersion },
+ propsData: { agents: [currentAgent] },
+ });
+ });
+
+ it('shows the correct agent version text', () => {
+ expect(findVersionText(0).text()).toBe(agentVersion);
});
if (versionWarning) {
- it(`shows a warning icon when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
+ it('shows a warning icon', () => {
expect(findIcon().props('name')).toBe('warning');
});
-
it(`renders correct title for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
expect(findPopover().props('title')).toBe(title);
});
-
- it(`renders correct text for the popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
- texts.forEach((text) => {
- expect(findPopover().text()).toContain(text);
+ if (versionMismatch) {
+ it(`renders correct text for the popover when agent versions mismatch is ${versionMismatch}`, () => {
+ expect(findPopover().text()).toContain(mismatchText);
});
- });
+ }
+ if (versionOutdated) {
+ it(`renders correct text for the popover when agent versions outdated is ${versionOutdated}`, () => {
+ expect(findPopover().text()).toContain(outdatedText);
+ });
+ }
} else {
- it(`doesn't show a warning icon with a popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated} and the number of pods is ${podsNumber}`, () => {
+ it(`doesn't show a warning icon with a popover when agent versions mismatch is ${versionMismatch} and outdated is ${versionOutdated}`, () => {
expect(findIcon().exists()).toBe(false);
expect(findPopover().exists()).toBe(false);
});
}
},
);
-
- it.each`
- agentConfig | link | lineNumber
- ${'.gitlab/agents/agent-1'} | ${'/agent/full/path'} | ${0}
- ${'Default configuration'} | ${defaultConfigHelpUrl} | ${1}
- `(
- 'displays config file path as "$agentPath" at line $lineNumber',
- ({ agentConfig, link, lineNumber }) => {
- const findLink = findConfiguration(lineNumber).findComponent(GlLink);
-
- expect(findLink.attributes('href')).toBe(link);
- expect(findConfiguration(lineNumber).text()).toBe(agentConfig);
- },
- );
-
- it('displays actions menu for each agent', () => {
- expect(findDeleteAgentButton()).toHaveLength(5);
- });
});
});
diff --git a/spec/frontend/clusters_list/components/agent_token_spec.js b/spec/frontend/clusters_list/components/agent_token_spec.js
index a92a03fedb6..edb8b22d79e 100644
--- a/spec/frontend/clusters_list/components/agent_token_spec.js
+++ b/spec/frontend/clusters_list/components/agent_token_spec.js
@@ -53,10 +53,6 @@ describe('InstallAgentModal', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('initial state', () => {
it('shows basic agent installation instructions', () => {
expect(wrapper.text()).toContain(I18N_AGENT_TOKEN.basicInstallTitle);
diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js
index 2372ab30300..d91245ba9b4 100644
--- a/spec/frontend/clusters_list/components/agents_spec.js
+++ b/spec/frontend/clusters_list/components/agents_spec.js
@@ -83,8 +83,6 @@ describe('Agents', () => {
const findBanner = () => wrapper.findComponent(GlBanner);
afterEach(() => {
- wrapper.destroy();
-
localStorage.removeItem(AGENT_FEEDBACK_KEY);
});
diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
index 758f6586e1a..4a2effa3463 100644
--- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js
+++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
@@ -18,10 +18,6 @@ describe('ClustersAncestorNotice', () => {
return createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when cluster does not have ancestors', () => {
beforeEach(async () => {
store.state.hasAncestorClusters = false;
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
index f4ee3f93cb5..e4e1986f705 100644
--- a/spec/frontend/clusters_list/components/clusters_actions_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js
@@ -35,7 +35,7 @@ describe('ClustersActionsComponent', () => {
...provideData,
},
directives: {
- GlModalDirective: createMockDirective(),
+ GlModalDirective: createMockDirective('gl-modal-directive'),
},
});
};
@@ -44,9 +44,6 @@ describe('ClustersActionsComponent', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
describe('when the certificate based clusters are enabled', () => {
it('renders actions menu', () => {
expect(findDropdown().exists()).toBe(true);
diff --git a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
index 2c3a224f3c8..5a5006d24c4 100644
--- a/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_empty_state_spec.js
@@ -21,10 +21,6 @@ describe('ClustersEmptyStateComponent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the help text is not provided', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/clusters_list/components/clusters_main_view_spec.js b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
index 6f23ed47d2a..af8d3b59869 100644
--- a/spec/frontend/clusters_list/components/clusters_main_view_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_main_view_spec.js
@@ -40,10 +40,6 @@ describe('ClustersMainViewComponent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTabs = () => wrapper.findComponent(GlTabs);
const findAllTabs = () => wrapper.findAllComponents(GlTab);
const findGlTabAtIndex = (index) => findAllTabs().at(index);
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 20dbff9df15..207bfddcb4f 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -75,7 +75,6 @@ describe('Clusters', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
captureException.mockRestore();
});
@@ -271,9 +270,7 @@ describe('Clusters', () => {
describe('when updating currentPage', () => {
beforeEach(() => {
mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalSecondPage, perPage, 2));
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ currentPage: 2 });
+ findPaginatedButtons().vm.$emit('input', 2);
return axios.waitForAll();
});
diff --git a/spec/frontend/clusters_list/components/clusters_view_all_spec.js b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
index b4eb9242003..e81b242dd90 100644
--- a/spec/frontend/clusters_list/components/clusters_view_all_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_view_all_spec.js
@@ -60,10 +60,6 @@ describe('ClustersViewAllComponent', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when agents and clusters are not loaded', () => {
const initialState = {
loadingClusters: true,
diff --git a/spec/frontend/clusters_list/components/delete_agent_button_spec.js b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
index 82850b9dea4..2c9a6b11671 100644
--- a/spec/frontend/clusters_list/components/delete_agent_button_spec.js
+++ b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
@@ -33,7 +33,7 @@ describe('DeleteAgentButton', () => {
const findDeleteBtn = () => wrapper.findComponent(GlButton);
const findInput = () => wrapper.findComponent(GlFormInput);
const findPrimaryAction = () => findModal().props('actionPrimary');
- const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr];
const findDeleteAgentButtonTooltip = () => wrapper.findByTestId('delete-agent-button-tooltip');
const getTooltipText = (el) => {
const binding = getBinding(el, 'gl-tooltip');
@@ -84,7 +84,7 @@ describe('DeleteAgentButton', () => {
...provideData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData,
mocks: { $toast: { show: toast } },
@@ -108,7 +108,6 @@ describe('DeleteAgentButton', () => {
});
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
deleteResponse = null;
toast = null;
@@ -141,7 +140,7 @@ describe('DeleteAgentButton', () => {
});
it('disables the button', () => {
- expect(findDeleteBtn().attributes('disabled')).toBe('true');
+ expect(findDeleteBtn().attributes('disabled')).toBeDefined();
});
it('shows a disabled tooltip', () => {
@@ -231,7 +230,7 @@ describe('DeleteAgentButton', () => {
it('reenables the button', async () => {
expect(findPrimaryActionAttributes('loading')).toBe(true);
- expect(findDeleteBtn().attributes('disabled')).toBe('true');
+ expect(findDeleteBtn().attributes('disabled')).toBeDefined();
await findModal().vm.$emit('hide');
diff --git a/spec/frontend/clusters_list/components/install_agent_modal_spec.js b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
index 10264d6a011..e1306e2738f 100644
--- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js
+++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js
@@ -74,7 +74,7 @@ describe('InstallAgentModal', () => {
const expectDisabledAttribute = (element, disabled) => {
if (disabled) {
- expect(element.attributes('disabled')).toBe('true');
+ expect(element.attributes('disabled')).toBeDefined();
} else {
expect(element.attributes('disabled')).toBeUndefined();
}
@@ -139,7 +139,6 @@ describe('InstallAgentModal', () => {
});
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
});
@@ -257,7 +256,7 @@ describe('InstallAgentModal', () => {
return mockSelectedAgentResponse();
});
- it('displays the error message', async () => {
+ it('displays the error message', () => {
expect(findAlert().text()).toBe(
createAgentTokenErrorResponse.data.clusterAgentTokenCreate.errors[0],
);
diff --git a/spec/frontend/clusters_list/components/mock_data.js b/spec/frontend/clusters_list/components/mock_data.js
index 3d18b22d727..af1fb496118 100644
--- a/spec/frontend/clusters_list/components/mock_data.js
+++ b/spec/frontend/clusters_list/components/mock_data.js
@@ -19,7 +19,7 @@ export const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIV
export const clusterAgents = [
{
name: 'agent-1',
- id: 'agent-1-id',
+ id: 'gid://gitlab/Clusters::Agent/1',
configFolder: {
webPath: '/agent/full/path',
},
@@ -30,17 +30,17 @@ export const clusterAgents = [
},
{
name: 'agent-2',
- id: 'agent-2-id',
+ id: 'gid://gitlab/Clusters::Agent/2',
webPath: '/agent-2',
status: 'active',
lastContact: connectedTimeNow.getTime(),
connections: {
nodes: [
{
- metadata: { version: 'v14.8' },
+ metadata: { version: 'v14.8.0' },
},
{
- metadata: { version: 'v14.8' },
+ metadata: { version: 'v14.8.0' },
},
],
},
@@ -54,14 +54,14 @@ export const clusterAgents = [
},
{
name: 'agent-3',
- id: 'agent-3-id',
+ id: 'gid://gitlab/Clusters::Agent/3',
webPath: '/agent-3',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
- metadata: { version: 'v14.5' },
+ metadata: { version: 'v14.6.0' },
},
],
},
@@ -75,17 +75,17 @@ export const clusterAgents = [
},
{
name: 'agent-4',
- id: 'agent-4-id',
+ id: 'gid://gitlab/Clusters::Agent/4',
webPath: '/agent-4',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
- metadata: { version: 'v14.7' },
+ metadata: { version: 'v14.7.0' },
},
{
- metadata: { version: 'v14.8' },
+ metadata: { version: 'v14.8.0' },
},
],
},
@@ -99,17 +99,101 @@ export const clusterAgents = [
},
{
name: 'agent-5',
- id: 'agent-5-id',
+ id: 'gid://gitlab/Clusters::Agent/5',
webPath: '/agent-5',
status: 'inactive',
lastContact: connectedTimeInactive.getTime(),
connections: {
nodes: [
{
- metadata: { version: 'v14.5' },
+ metadata: { version: 'v14.5.0' },
},
{
- metadata: { version: 'v14.3' },
+ metadata: { version: 'v14.3.0' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-6',
+ id: 'gid://gitlab/Clusters::Agent/6',
+ webPath: '/agent-6',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.6.0' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-7',
+ id: 'gid://gitlab/Clusters::Agent/7',
+ webPath: '/agent-7',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.8.0' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-8',
+ id: 'gid://gitlab/Clusters::Agent/8',
+ webPath: '/agent-8',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.8.0' },
+ },
+ ],
+ },
+ tokens: {
+ nodes: [
+ {
+ lastUsedAt: connectedTimeInactive,
+ },
+ ],
+ },
+ },
+ {
+ name: 'agent-9',
+ id: 'gid://gitlab/Clusters::Agent/9',
+ webPath: '/agent-9',
+ status: 'inactive',
+ lastContact: connectedTimeInactive.getTime(),
+ connections: {
+ nodes: [
+ {
+ metadata: { version: 'v14.8.0' },
},
],
},
diff --git a/spec/frontend/clusters_list/components/node_error_help_text_spec.js b/spec/frontend/clusters_list/components/node_error_help_text_spec.js
index 3211ba44eff..a3dfc848fc8 100644
--- a/spec/frontend/clusters_list/components/node_error_help_text_spec.js
+++ b/spec/frontend/clusters_list/components/node_error_help_text_spec.js
@@ -13,10 +13,6 @@ describe('NodeErrorHelpText', () => {
const findPopover = () => wrapper.findComponent(GlPopover);
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
errorType | wrapperText | popoverText
${'authentication_error'} | ${'Unable to Authenticate'} | ${'GitLab failed to authenticate'}
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 360fd3b2842..6d23db0517d 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -5,13 +5,13 @@ import waitForPromises from 'helpers/wait_for_promises';
import { MAX_REQUESTS } from '~/clusters_list/constants';
import * as actions from '~/clusters_list/store/actions';
import * as types from '~/clusters_list/store/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { apiData } from '../mock_data';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Clusters store actions', () => {
let captureException;
@@ -81,7 +81,7 @@ describe('Clusters store actions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index b9be262efd0..88861b0d08a 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -32,10 +32,6 @@ function factory(initialState = {}, props = {}) {
}
describe('Code navigation app component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('sets initial data on mount if the correct props are passed', () => {
const codeNavigationPath = 'code/nav/path.js';
const path = 'blob/path.js';
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
index 874263e046a..1bfaf7e959e 100644
--- a/spec/frontend/code_navigation/components/popover_spec.js
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -61,10 +61,6 @@ function factory({ position, data, definitionPathPrefix, blobPath = 'index.js' }
}
describe('Code navigation popover component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders popover', () => {
factory({
position: { x: 0, y: 0, height: 0 },
diff --git a/spec/frontend/code_review/signals_spec.js b/spec/frontend/code_review/signals_spec.js
new file mode 100644
index 00000000000..03c3580860e
--- /dev/null
+++ b/spec/frontend/code_review/signals_spec.js
@@ -0,0 +1,145 @@
+import { start } from '~/code_review/signals';
+
+import diffsEventHub from '~/diffs/event_hub';
+import { EVT_MR_PREPARED } from '~/diffs/constants';
+import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
+
+jest.mock('~/diffs/utils/merge_request');
+
+describe('~/code_review', () => {
+ const io = diffsEventHub;
+
+ beforeAll(() => {
+ getDerivedMergeRequestInformation.mockImplementation(() => ({
+ namespace: 'x',
+ project: 'y',
+ id: '1',
+ }));
+ });
+
+ describe('start', () => {
+ it.each`
+ description | argument
+ ${'no event hub is provided'} | ${{}}
+ ${'no parameters are provided'} | ${undefined}
+ `('throws an error if $description', async ({ argument }) => {
+ await expect(() => start(argument)).rejects.toThrow('signalBus is a required argument');
+ });
+
+ describe('observeMergeRequestFinishingPreparation', () => {
+ const callArgs = {};
+ const apollo = {};
+ let querySpy;
+ let apolloSubscribeSpy;
+ let subscribeSpy;
+ let nextSpy;
+ let unsubscribeSpy;
+ let observable;
+
+ beforeEach(() => {
+ querySpy = jest.fn();
+ apolloSubscribeSpy = jest.fn();
+ subscribeSpy = jest.fn();
+ unsubscribeSpy = jest.fn();
+ nextSpy = jest.fn();
+ observable = {
+ next: nextSpy,
+ subscribe: subscribeSpy.mockReturnValue({
+ unsubscribe: unsubscribeSpy,
+ }),
+ };
+
+ querySpy.mockResolvedValue({
+ data: { project: { mergeRequest: { id: 'gql:id:1', preparedAt: 'x' } } },
+ });
+ apolloSubscribeSpy.mockReturnValue(observable);
+
+ apollo.query = querySpy;
+ apollo.subscribe = apolloSubscribeSpy;
+
+ callArgs.signalBus = io;
+ callArgs.apolloClient = apollo;
+ });
+
+ it('does not query at all if the page does not seem like a merge request', async () => {
+ getDerivedMergeRequestInformation.mockImplementationOnce(() => ({}));
+
+ await start(callArgs);
+
+ expect(querySpy).not.toHaveBeenCalled();
+ expect(apolloSubscribeSpy).not.toHaveBeenCalled();
+ });
+
+ describe('on a merge request page', () => {
+ it('requests the preparedAt (and id) for the current merge request', async () => {
+ await start(callArgs);
+
+ expect(querySpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ projectPath: 'x/y',
+ iid: '1',
+ },
+ }),
+ );
+ });
+
+ it('does not subscribe to any updates if the preparedAt value is already populated', async () => {
+ await start(callArgs);
+
+ expect(apolloSubscribeSpy).not.toHaveBeenCalled();
+ });
+
+ describe('if the merge request is still asynchronously preparing', () => {
+ beforeEach(() => {
+ querySpy.mockResolvedValue({
+ data: { project: { mergeRequest: { id: 'gql:id:1', preparedAt: null } } },
+ });
+ });
+
+ it('subscribes to updates', async () => {
+ await start(callArgs);
+
+ expect(apolloSubscribeSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ variables: { issuableId: 'gql:id:1' } }),
+ );
+ expect(observable.subscribe).toHaveBeenCalled();
+ });
+
+ describe('when the MR has been updated', () => {
+ let emitSpy;
+ let behavior;
+
+ beforeEach(() => {
+ emitSpy = jest.spyOn(diffsEventHub, '$emit');
+ nextSpy.mockImplementation((data) => behavior?.(data));
+ subscribeSpy.mockImplementation((handler) => {
+ behavior = handler;
+
+ return { unsubscribe: unsubscribeSpy };
+ });
+ });
+
+ it('does nothing if the MR has not yet finished preparing', async () => {
+ await start(callArgs);
+
+ observable.next({ data: { mergeRequestMergeStatusUpdated: { preparedAt: null } } });
+
+ expect(unsubscribeSpy).not.toHaveBeenCalled();
+ expect(emitSpy).not.toHaveBeenCalled();
+ });
+
+ it('emits an event and unsubscribes when the MR is prepared', async () => {
+ await start(callArgs);
+
+ observable.next({ data: { mergeRequestMergeStatusUpdated: { preparedAt: 'x' } } });
+
+ expect(unsubscribeSpy).toHaveBeenCalled();
+ expect(emitSpy).toHaveBeenCalledWith(EVT_MR_PREPARED);
+ });
+ });
+ });
+ });
+ });
+ });
+});
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
new file mode 100644
index 00000000000..0f158df6c05
--- /dev/null
+++ b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
@@ -0,0 +1,140 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Comment templates list item component renders list item 1`] = `
+<li
+ class="gl-pt-4 gl-pb-5 gl-border-b"
+>
+ <div
+ class="gl-display-flex gl-align-items-center"
+ >
+ <h6
+ class="gl-mr-3 gl-my-0"
+ data-testid="comment-template-name"
+ >
+ test
+ </h6>
+
+ <div
+ class="gl-ml-auto"
+ >
+ <div
+ class="gl-new-dropdown gl-disclosure-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"
+ data-testid="base-dropdown-toggle"
+ id="actions-toggle-3"
+ listeners="[object Object]"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="ellipsis_v-icon"
+ role="img"
+ >
+ <use
+ href="file-mock#ellipsis_v"
+ />
+ </svg>
+
+ <span
+ class="gl-button-text"
+ >
+ <span
+ class="gl-new-dropdown-button-text gl-sr-only"
+ >
+
+ Comment template actions
+
+ </span>
+
+ <!---->
+ </span>
+ </button>
+
+ <div
+ class="gl-new-dropdown-panel gl-w-31"
+ data-testid="base-dropdown-menu"
+ id="base-dropdown-7"
+ >
+ <div
+ class="gl-new-dropdown-inner"
+ >
+
+ <ul
+ aria-labelledby="actions-toggle-3"
+ class="gl-new-dropdown-contents"
+ data-testid="disclosure-content"
+ id="disclosure-4"
+ tabindex="-1"
+ >
+ <li
+ class="gl-new-dropdown-item"
+ data-testid="disclosure-dropdown-item"
+ tabindex="0"
+ >
+ <button
+ class="gl-new-dropdown-item-content"
+ data-testid="comment-template-edit-btn"
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="gl-new-dropdown-item-text-wrapper"
+ >
+
+ Edit
+
+ </span>
+ </button>
+ </li>
+ <li
+ class="gl-new-dropdown-item"
+ data-testid="disclosure-dropdown-item"
+ tabindex="0"
+ >
+ <button
+ class="gl-new-dropdown-item-content gl-text-red-500!"
+ data-testid="comment-template-delete-btn"
+ tabindex="-1"
+ type="button"
+ >
+ <span
+ class="gl-new-dropdown-item-text-wrapper"
+ >
+
+ Delete
+
+ </span>
+ </button>
+ </li>
+ </ul>
+
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-tooltip"
+ >
+
+ Comment template actions
+
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-mt-3 gl-font-monospace"
+ >
+ /assign_reviewer
+ </div>
+
+ <!---->
+</li>
+`;
diff --git a/spec/frontend/comment_templates/components/form_spec.js b/spec/frontend/comment_templates/components/form_spec.js
new file mode 100644
index 00000000000..053a5099c37
--- /dev/null
+++ b/spec/frontend/comment_templates/components/form_spec.js
@@ -0,0 +1,145 @@
+import Vue, { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createdSavedReplyResponse from 'test_fixtures/graphql/comment_templates/create_saved_reply.mutation.graphql.json';
+import createdSavedReplyErrorResponse from 'test_fixtures/graphql/comment_templates/create_saved_reply_with_errors.mutation.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import Form from '~/comment_templates/components/form.vue';
+import createSavedReplyMutation from '~/comment_templates/queries/create_saved_reply.mutation.graphql';
+import updateSavedReplyMutation from '~/comment_templates/queries/update_saved_reply.mutation.graphql';
+
+let wrapper;
+let createSavedReplyResponseSpy;
+let updateSavedReplyResponseSpy;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ createSavedReplyResponseSpy = jest.fn().mockResolvedValue(response);
+ updateSavedReplyResponseSpy = jest.fn().mockResolvedValue(response);
+
+ const requestHandlers = [
+ [createSavedReplyMutation, createSavedReplyResponseSpy],
+ [updateSavedReplyMutation, updateSavedReplyResponseSpy],
+ ];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(id = null, response = createdSavedReplyResponse) {
+ const mockApollo = createMockApolloProvider(response);
+
+ return mount(Form, {
+ propsData: {
+ id,
+ },
+ apolloProvider: mockApollo,
+ });
+}
+
+const findSavedReplyNameInput = () => wrapper.find('[data-testid="comment-template-name-input"]');
+const findSavedReplyNameFormGroup = () =>
+ wrapper.find('[data-testid="comment-template-name-form-group"]');
+const findSavedReplyContentInput = () =>
+ wrapper.find('[data-testid="comment-template-content-input"]');
+const findSavedReplyContentFormGroup = () =>
+ wrapper.find('[data-testid="comment-template-content-form-group"]');
+const findSavedReplyFrom = () => wrapper.find('[data-testid="comment-template-form"]');
+const findAlerts = () => wrapper.findAllComponents(GlAlert);
+const findSubmitBtn = () => wrapper.find('[data-testid="comment-template-form-submit-btn"]');
+
+describe('Comment templates form component', () => {
+ describe('creates comment template', () => {
+ it('calls apollo mutation', async () => {
+ wrapper = createComponent();
+
+ findSavedReplyNameInput().setValue('Test');
+ findSavedReplyContentInput().setValue('Test content');
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ expect(createSavedReplyResponseSpy).toHaveBeenCalledWith({
+ id: null,
+ content: 'Test content',
+ name: 'Test',
+ });
+ });
+
+ it('does not submit when form validation fails', async () => {
+ wrapper = createComponent();
+
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ expect(createSavedReplyResponseSpy).not.toHaveBeenCalled();
+ });
+
+ it.each`
+ findFormGroup | findInput | fieldName
+ ${findSavedReplyNameFormGroup} | ${findSavedReplyContentInput} | ${'name'}
+ ${findSavedReplyContentFormGroup} | ${findSavedReplyNameInput} | ${'content'}
+ `('shows errors for empty $fieldName input', async ({ findFormGroup, findInput }) => {
+ wrapper = createComponent(null, createdSavedReplyErrorResponse);
+
+ findInput().setValue('Test');
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ expect(findFormGroup().classes('is-invalid')).toBe(true);
+ });
+
+ it('displays errors when mutation fails', async () => {
+ wrapper = createComponent(null, createdSavedReplyErrorResponse);
+
+ findSavedReplyNameInput().setValue('Test');
+ findSavedReplyContentInput().setValue('Test content');
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ const { errors } = createdSavedReplyErrorResponse;
+ const alertMessages = findAlerts().wrappers.map((x) => x.text());
+
+ expect(alertMessages).toEqual(errors.map((x) => x.message));
+ });
+
+ it('shows loading state when saving', async () => {
+ wrapper = createComponent();
+
+ findSavedReplyNameInput().setValue('Test');
+ findSavedReplyContentInput().setValue('Test content');
+ findSavedReplyFrom().trigger('submit');
+
+ await nextTick();
+
+ expect(findSubmitBtn().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findSubmitBtn().props('loading')).toBe(false);
+ });
+ });
+
+ describe('updates saved reply', () => {
+ it('calls apollo mutation', async () => {
+ wrapper = createComponent('1');
+
+ findSavedReplyNameInput().setValue('Test');
+ findSavedReplyContentInput().setValue('Test content');
+ findSavedReplyFrom().trigger('submit');
+
+ await waitForPromises();
+
+ expect(updateSavedReplyResponseSpy).toHaveBeenCalledWith({
+ id: '1',
+ content: 'Test content',
+ name: 'Test',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/comment_templates/components/list_item_spec.js b/spec/frontend/comment_templates/components/list_item_spec.js
new file mode 100644
index 00000000000..925d78da4ad
--- /dev/null
+++ b/spec/frontend/comment_templates/components/list_item_spec.js
@@ -0,0 +1,154 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mount } from '@vue/test-utils';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
+import { __ } from '~/locale';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createMockDirective } from 'helpers/vue_mock_directive';
+import ListItem from '~/comment_templates/components/list_item.vue';
+import deleteSavedReplyMutation from '~/comment_templates/queries/delete_saved_reply.mutation.graphql';
+
+function createMockApolloProvider(requestHandlers = [deleteSavedReplyMutation]) {
+ Vue.use(VueApollo);
+
+ return createMockApollo([requestHandlers]);
+}
+
+describe('Comment templates list item component', () => {
+ let wrapper;
+ let $router;
+
+ function createComponent(propsData = {}, apolloProvider = createMockApolloProvider) {
+ $router = {
+ push: jest.fn(),
+ };
+
+ return mount(ListItem, {
+ propsData,
+ directives: {
+ GlModal: createMockDirective('gl-modal'),
+ },
+ apolloProvider,
+ mocks: {
+ $router,
+ },
+ });
+ }
+
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ it('renders list item', () => {
+ wrapper = createComponent({ template: { name: 'test', content: '/assign_reviewer' } });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('comment template actions dropdown', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ template: { name: 'test', content: '/assign_reviewer' } });
+ });
+
+ it('exists', () => {
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('has correct toggle text', () => {
+ expect(findDropdown().props('toggleText')).toBe(__('Comment template actions'));
+ });
+
+ it('has correct amount of dropdown items', () => {
+ const items = findDropdownItems();
+
+ expect(items.exists()).toBe(true);
+ expect(items).toHaveLength(2);
+ });
+
+ describe('edit option', () => {
+ it('exists', () => {
+ const items = findDropdownItems();
+
+ const editItem = items.filter((item) => item.text() === __('Edit'));
+
+ expect(editItem.exists()).toBe(true);
+ });
+
+ it('shows as first dropdown item', () => {
+ const items = findDropdownItems();
+
+ expect(items.at(0).text()).toBe(__('Edit'));
+ });
+ });
+
+ describe('delete option', () => {
+ it('exists', () => {
+ const items = findDropdownItems();
+
+ const deleteItem = items.filter((item) => item.text() === __('Delete'));
+
+ expect(deleteItem.exists()).toBe(true);
+ });
+
+ it('shows as first dropdown item', () => {
+ const items = findDropdownItems();
+
+ expect(items.at(1).text()).toBe(__('Delete'));
+ });
+ });
+ });
+
+ describe('Delete modal', () => {
+ let deleteSavedReplyMutationResponse;
+
+ beforeEach(() => {
+ deleteSavedReplyMutationResponse = jest
+ .fn()
+ .mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } });
+
+ const apolloProvider = createMockApolloProvider([
+ deleteSavedReplyMutation,
+ deleteSavedReplyMutationResponse,
+ ]);
+
+ wrapper = createComponent(
+ { template: { name: 'test', content: '/assign_reviewer', id: 1 } },
+ apolloProvider,
+ );
+ });
+
+ it('exists', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('has correct title', () => {
+ expect(findModal().props('title')).toBe(__('Delete comment template'));
+ });
+
+ it('delete button calls Apollo mutate', async () => {
+ await findModal().vm.$emit('primary');
+
+ expect(deleteSavedReplyMutationResponse).toHaveBeenCalledWith({ id: 1 });
+ });
+
+ it('cancel button does not trigger Apollo mutation', async () => {
+ await findModal().vm.$emit('secondary');
+
+ expect(deleteSavedReplyMutationResponse).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Dropdown Edit', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ template: { name: 'test', content: '/assign_reviewer' } });
+ });
+
+ it('click triggers router push', async () => {
+ const editComponent = findDropdownItems().at(0);
+
+ await editComponent.find('button').trigger('click');
+
+ expect($router.push).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/comment_templates/components/list_spec.js b/spec/frontend/comment_templates/components/list_spec.js
new file mode 100644
index 00000000000..8b0daf2fe2f
--- /dev/null
+++ b/spec/frontend/comment_templates/components/list_spec.js
@@ -0,0 +1,46 @@
+import { mount } from '@vue/test-utils';
+import noSavedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies_empty.query.graphql.json';
+import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
+import List from '~/comment_templates/components/list.vue';
+import ListItem from '~/comment_templates/components/list_item.vue';
+
+let wrapper;
+
+function createComponent(res = {}) {
+ const { savedReplies } = res.data.currentUser;
+
+ return mount(List, {
+ propsData: {
+ savedReplies: savedReplies.nodes,
+ pageInfo: savedReplies.pageInfo,
+ count: savedReplies.count,
+ },
+ });
+}
+
+describe('Comment templates list component', () => {
+ it('does not render any list items when response is empty', () => {
+ wrapper = createComponent(noSavedRepliesResponse);
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(0);
+ });
+
+ it('render comment templates count', () => {
+ wrapper = createComponent(savedRepliesResponse);
+
+ expect(wrapper.find('[data-testid="title"]').text()).toEqual('My comment templates (2)');
+ });
+
+ it('renders list of comment templates', () => {
+ const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
+ wrapper = createComponent(savedRepliesResponse);
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(2);
+ expect(wrapper.findAllComponents(ListItem).at(0).props('template')).toEqual(
+ expect.objectContaining(savedReplies[0]),
+ );
+ expect(wrapper.findAllComponents(ListItem).at(1).props('template')).toEqual(
+ expect.objectContaining(savedReplies[1]),
+ );
+ });
+});
diff --git a/spec/frontend/comment_templates/pages/index_spec.js b/spec/frontend/comment_templates/pages/index_spec.js
new file mode 100644
index 00000000000..6dbec3ef4a4
--- /dev/null
+++ b/spec/frontend/comment_templates/pages/index_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import IndexPage from '~/comment_templates/pages/index.vue';
+import ListItem from '~/comment_templates/components/list_item.vue';
+import savedRepliesQuery from '~/comment_templates/queries/saved_replies.query.graphql';
+
+let wrapper;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(options = {}) {
+ const { mockApollo } = options;
+
+ return mount(IndexPage, {
+ apolloProvider: mockApollo,
+ });
+}
+
+describe('Comment templates index page component', () => {
+ it('renders list of comment templates', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(2);
+ expect(wrapper.findAllComponents(ListItem).at(0).props('template')).toEqual(
+ expect.objectContaining(savedReplies[0]),
+ );
+ expect(wrapper.findAllComponents(ListItem).at(1).props('template')).toEqual(
+ expect.objectContaining(savedReplies[1]),
+ );
+ });
+});
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 debd10de118..7be68df61de 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -5,13 +5,13 @@ import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+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 { COMMIT_BOX_POLL_INTERVAL } from '~/projects/commit_box/info/constants';
import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
import getPipelineStagesQuery from '~/projects/commit_box/info/graphql/queries/get_pipeline_stages.query.graphql';
-import * as graphQlUtils from '~/pipelines/components/graph/utils';
+import * as sharedGraphQlUtils from '~/graphql_shared/utils';
import {
mockDownstreamQueryResponse,
mockPipelineStagesQueryResponse,
@@ -20,7 +20,7 @@ import {
mockUpstreamQueryResponse,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -69,10 +69,6 @@ describe('Commit box pipeline mini graph', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('should display loading state when loading', () => {
createComponent();
@@ -87,7 +83,7 @@ describe('Commit box pipeline mini graph', () => {
await createComponent();
});
- it('should not display loading state after the query is resolved', async () => {
+ it('should not display loading state after the query is resolved', () => {
expect(findLoadingIcon().exists()).toBe(false);
expect(findPipelineMiniGraph().exists()).toBe(true);
});
@@ -245,16 +241,16 @@ describe('Commit box pipeline mini graph', () => {
});
it('toggles query polling with visibility check', async () => {
- jest.spyOn(graphQlUtils, 'toggleQueryPollingByVisibility');
+ jest.spyOn(sharedGraphQlUtils, 'toggleQueryPollingByVisibility');
createComponent();
await waitForPromises();
- expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
+ expect(sharedGraphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
wrapper.vm.$apollo.queries.pipelineStages,
);
- expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
+ expect(sharedGraphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
wrapper.vm.$apollo.queries.pipeline,
);
});
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index e75fb697a7b..e474ef9c635 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import Visibility from 'visibilityjs';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Poll from '~/lib/utils/poll';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
jest.mock('~/lib/utils/poll');
jest.mock('visibilityjs');
-jest.mock('~/flash');
+jest.mock('~/alert');
const mockFetchData = jest.fn();
jest.mock('~/projects/tree/services/commit_pipeline_service', () =>
@@ -41,11 +41,6 @@ describe('Commit pipeline status component', () => {
const findLink = () => wrapper.find('a');
const findCiIcon = () => findLink().findComponent(CiIcon);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Visibility management', () => {
describe('when component is hidden', () => {
beforeEach(() => {
@@ -169,7 +164,7 @@ describe('Commit pipeline status component', () => {
});
});
- it('displays flash error message', () => {
+ it('displays alert error message', () => {
expect(createAlert).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
index 8d455f8a3d7..80b75a0a65e 100644
--- a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
+++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CommitBoxPipelineStatus from '~/projects/commit_box/info/components/commit_box_pipeline_status.vue';
import {
@@ -12,7 +12,7 @@ import {
PIPELINE_STATUS_FETCH_ERROR,
} from '~/projects/commit_box/info/constants';
import getLatestPipelineStatusQuery from '~/projects/commit_box/info/graphql/queries/get_latest_pipeline_status.query.graphql';
-import * as graphQlUtils from '~/pipelines/components/graph/utils';
+import * as sharedGraphQlUtils from '~/graphql_shared/utils';
import { mockPipelineStatusResponse } from '../mock_data';
const mockProvide = {
@@ -23,7 +23,7 @@ const mockProvide = {
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Commit box pipeline status', () => {
let wrapper;
@@ -54,10 +54,6 @@ describe('Commit box pipeline status', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('should display loading state when loading', () => {
createComponent();
@@ -74,7 +70,7 @@ describe('Commit box pipeline status', () => {
await waitForPromises();
});
- it('should display pipeline status after the query is resolved successfully', async () => {
+ it('should display pipeline status after the query is resolved successfully', () => {
expect(findStatusIcon().exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
@@ -136,13 +132,13 @@ describe('Commit box pipeline status', () => {
});
it('toggles pipelineStatus polling with visibility check', async () => {
- jest.spyOn(graphQlUtils, 'toggleQueryPollingByVisibility');
+ jest.spyOn(sharedGraphQlUtils, 'toggleQueryPollingByVisibility');
createComponent();
await waitForPromises();
- expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
+ expect(sharedGraphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith(
wrapper.vm.$apollo.queries.pipelineStatus,
);
});
diff --git a/spec/frontend/commit/components/signature_badge_spec.js b/spec/frontend/commit/components/signature_badge_spec.js
new file mode 100644
index 00000000000..d52ad2b43e2
--- /dev/null
+++ b/spec/frontend/commit/components/signature_badge_spec.js
@@ -0,0 +1,134 @@
+import { GlBadge, GlLink, GlPopover } from '@gitlab/ui';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import SignatureBadge from '~/commit/components/signature_badge.vue';
+import X509CertificateDetails from '~/commit/components/x509_certificate_details.vue';
+import { typeConfig, statusConfig, verificationStatuses, signatureTypes } from '~/commit/constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { sshSignatureProp, gpgSignatureProp, x509SignatureProp } from '../mock_data';
+
+describe('Commit signature', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(SignatureBadge, {
+ propsData: {
+ signature: {
+ ...props,
+ },
+ stubs: {
+ GlBadge,
+ GlLink,
+ X509CertificateDetails,
+ GlPopover: stubComponent(GlPopover, { template: RENDER_ALL_SLOTS_TEMPLATE }),
+ },
+ },
+ });
+ };
+
+ const signatureBadge = () => wrapper.findComponent(GlBadge);
+ const signaturePopover = () => wrapper.findComponent(GlPopover);
+ const signatureDescription = () => wrapper.findByTestId('signature-description');
+ const signatureKeyLabel = () => wrapper.findByTestId('signature-key-label');
+ const signatureKey = () => wrapper.findByTestId('signature-key');
+ const helpLink = () => wrapper.findComponent(GlLink);
+ const X509CertificateDetailsComponents = () => wrapper.findAllComponents(X509CertificateDetails);
+
+ describe.each`
+ signatureType | verificationStatus
+ ${signatureTypes.GPG} | ${verificationStatuses.VERIFIED}
+ ${signatureTypes.GPG} | ${verificationStatuses.UNVERIFIED}
+ ${signatureTypes.GPG} | ${verificationStatuses.UNVERIFIED_KEY}
+ ${signatureTypes.GPG} | ${verificationStatuses.UNKNOWN_KEY}
+ ${signatureTypes.GPG} | ${verificationStatuses.OTHER_USER}
+ ${signatureTypes.GPG} | ${verificationStatuses.SAME_USER_DIFFERENT_EMAIL}
+ ${signatureTypes.GPG} | ${verificationStatuses.MULTIPLE_SIGNATURES}
+ ${signatureTypes.X509} | ${verificationStatuses.VERIFIED}
+ ${signatureTypes.SSH} | ${verificationStatuses.VERIFIED}
+ ${signatureTypes.SSH} | ${verificationStatuses.REVOKED_KEY}
+ `(
+ 'For a specified `$signatureType` and `$verificationStatus` it renders component correctly',
+ ({ signatureType, verificationStatus }) => {
+ beforeEach(() => {
+ createComponent({ __typename: signatureType, verificationStatus });
+ });
+ it('renders correct badge class', () => {
+ expect(signatureBadge().props('variant')).toBe(statusConfig[verificationStatus].variant);
+ });
+ it('renders badge text', () => {
+ expect(signatureBadge().text()).toBe(statusConfig[verificationStatus].label);
+ });
+ it('renders popover header text', () => {
+ expect(signaturePopover().text()).toMatch(statusConfig[verificationStatus].title);
+ });
+ it('renders signature description', () => {
+ expect(signatureDescription().text()).toBe(statusConfig[verificationStatus].description);
+ });
+ it('renders help link with correct path', () => {
+ expect(helpLink().text()).toBe(typeConfig[signatureType].helpLink.label);
+ expect(helpLink().attributes('href')).toBe(
+ helpPagePath(typeConfig[signatureType].helpLink.path),
+ );
+ });
+ },
+ );
+
+ describe('SSH signature', () => {
+ beforeEach(() => {
+ createComponent(sshSignatureProp);
+ });
+
+ it('renders key label', () => {
+ expect(signatureKeyLabel().text()).toMatch(typeConfig[signatureTypes.SSH].keyLabel);
+ });
+
+ it('renders key signature', () => {
+ expect(signatureKey().text()).toBe(sshSignatureProp.keyFingerprintSha256);
+ });
+ });
+
+ describe('GPG signature', () => {
+ beforeEach(() => {
+ createComponent(gpgSignatureProp);
+ });
+
+ it('renders key label', () => {
+ expect(signatureKeyLabel().text()).toMatch(typeConfig[signatureTypes.GPG].keyLabel);
+ });
+
+ it('renders key signature for GGP signature', () => {
+ expect(signatureKey().text()).toBe(gpgSignatureProp.gpgKeyPrimaryKeyid);
+ });
+ });
+
+ describe('X509 signature', () => {
+ beforeEach(() => {
+ createComponent(x509SignatureProp);
+ });
+
+ it('does not render key label', () => {
+ expect(signatureKeyLabel().exists()).toBe(false);
+ });
+
+ it('renders X509 certificate details components', () => {
+ expect(X509CertificateDetailsComponents()).toHaveLength(2);
+ });
+
+ it('passes correct props', () => {
+ expect(X509CertificateDetailsComponents().at(0).props()).toStrictEqual({
+ subject: x509SignatureProp.x509Certificate.subject,
+ title: typeConfig[signatureTypes.X509].subjectTitle,
+ subjectKeyIdentifier: wrapper.vm.getSubjectKeyIdentifierToDisplay(
+ x509SignatureProp.x509Certificate.subjectKeyIdentifier,
+ ),
+ });
+ expect(X509CertificateDetailsComponents().at(1).props()).toStrictEqual({
+ subject: x509SignatureProp.x509Certificate.x509Issuer.subject,
+ title: typeConfig[signatureTypes.X509].issuerTitle,
+ subjectKeyIdentifier: wrapper.vm.getSubjectKeyIdentifierToDisplay(
+ x509SignatureProp.x509Certificate.x509Issuer.subjectKeyIdentifier,
+ ),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/commit/components/x509_certificate_details_spec.js b/spec/frontend/commit/components/x509_certificate_details_spec.js
new file mode 100644
index 00000000000..5d9398b572b
--- /dev/null
+++ b/spec/frontend/commit/components/x509_certificate_details_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import X509CertificateDetails from '~/commit/components/x509_certificate_details.vue';
+import { X509_CERTIFICATE_KEY_IDENTIFIER_TITLE } from '~/commit/constants';
+import { x509CertificateDetailsProp } from '../mock_data';
+
+describe('X509 certificate details', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(X509CertificateDetails, {
+ propsData: x509CertificateDetailsProp,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findTitle = () => wrapper.find('strong');
+ const findSubjectValues = () => wrapper.findAll("[data-testid='subject-value']");
+ const findKeyIdentifier = () => wrapper.find("[data-testid='key-identifier']");
+
+ it('renders a title', () => {
+ expect(findTitle().text()).toBe(x509CertificateDetailsProp.title);
+ });
+
+ it('renders subject values', () => {
+ expect(findSubjectValues()).toHaveLength(3);
+ });
+
+ it('renders key identifier', () => {
+ expect(findKeyIdentifier().text()).toBe(
+ `${X509_CERTIFICATE_KEY_IDENTIFIER_TITLE} ${x509CertificateDetailsProp.subjectKeyIdentifier}`,
+ );
+ });
+});
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index a13ef9c563e..3b6971d9607 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -201,3 +201,34 @@ export const mockUpstreamQueryResponse = {
},
},
};
+
+export const sshSignatureProp = {
+ __typename: 'SshSignature',
+ verificationStatus: 'VERIFIED',
+ keyFingerprintSha256: 'xxx',
+};
+
+export const gpgSignatureProp = {
+ __typename: 'GpgSignature',
+ verificationStatus: 'VERIFIED',
+ gpgKeyPrimaryKeyid: 'yyy',
+};
+
+export const x509SignatureProp = {
+ __typename: 'X509Signature',
+ verificationStatus: 'VERIFIED',
+ x509Certificate: {
+ subject: 'CN=gitlab@example.org,OU=Example,O=World',
+ subjectKeyIdentifier: 'BC:BC:BC:BC:BC:BC:BC:BC',
+ x509Issuer: {
+ subject: 'CN=PKI,OU=Example,O=World',
+ subjectKeyIdentifier: 'AB:AB:AB:AB:AB:AB:AB:AB:',
+ },
+ },
+};
+
+export const x509CertificateDetailsProp = {
+ title: 'Title',
+ subject: 'CN=gitlab@example.org,OU=Example,O=World',
+ subjectKeyIdentifier: 'BC BC BC BC BC BC BC BC',
+};
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 4bffb6a0fd3..009ec68ddcf 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
@@ -13,7 +14,7 @@ import {
HTTP_STATUS_OK,
HTTP_STATUS_UNAUTHORIZED,
} from '~/lib/utils/http_status';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { TOAST_MESSAGE } from '~/pipelines/constants';
import axios from '~/lib/utils/axios_utils';
@@ -21,12 +22,13 @@ const $toast = {
show: jest.fn(),
};
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Pipelines table in Commits and Merge requests', () => {
let wrapper;
let pipeline;
let mock;
+ const showMock = jest.fn();
const findRunPipelineBtn = () => wrapper.findByTestId('run_pipeline_button');
const findRunPipelineBtnMobile = () => wrapper.findByTestId('run_pipeline_button_mobile');
@@ -38,7 +40,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link');
- const createComponent = (props = {}) => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
mount(PipelinesTable, {
propsData: {
@@ -50,6 +52,12 @@ describe('Pipelines table in Commits and Merge requests', () => {
mocks: {
$toast,
},
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: '<div />',
+ methods: { show: showMock },
+ }),
+ },
}),
);
};
@@ -62,11 +70,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
pipeline = pipelines.find((p) => p.user !== null && p.commit !== null);
});
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- });
-
describe('successful request', () => {
describe('without pipelines', () => {
beforeEach(async () => {
@@ -95,6 +98,35 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
});
+ describe('with pagination', () => {
+ beforeEach(async () => {
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], {
+ 'X-TOTAL': 10,
+ 'X-PER-PAGE': 2,
+ 'X-PAGE': 1,
+ 'X-TOTAL-PAGES': 5,
+ 'X-NEXT-PAGE': 2,
+ 'X-PREV-PAGE': 2,
+ });
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('should make an API request when using pagination', async () => {
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0].params.page).toBe('1');
+
+ wrapper.find('.next-page-item').trigger('click');
+
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(2);
+ expect(mock.history.get[1].params.page).toBe('2');
+ });
+ });
+
describe('with pipelines', () => {
beforeEach(async () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], { 'x-total': 10 });
@@ -111,32 +143,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(findErrorEmptyState().exists()).toBe(false);
});
- describe('with pagination', () => {
- it('should make an API request when using pagination', async () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({
- store: {
- state: {
- pageInfo: {
- page: 1,
- total: 10,
- perPage: 2,
- nextPage: 2,
- totalPages: 5,
- },
- },
- },
- });
-
- wrapper.find('.next-page-item').trigger('click');
-
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({ page: '2' });
- });
- });
-
describe('pipeline badge counts', () => {
it('should receive update-pipelines-count event', () => {
const element = document.createElement('div');
@@ -203,16 +209,18 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent({
- canRunPipeline: true,
- projectId: '5',
- mergeRequestId: 3,
+ props: {
+ canRunPipeline: true,
+ projectId: '5',
+ mergeRequestId: 3,
+ },
});
await waitForPromises();
});
describe('success', () => {
beforeEach(() => {
- jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockResolvedValue();
});
it('displays a toast message during pipeline creation', async () => {
await findRunPipelineBtn().trigger('click');
@@ -255,9 +263,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
`('displays permissions error message', async ({ status, message }) => {
const response = { response: { status } };
- jest
- .spyOn(Api, 'postMergeRequestPipeline')
- .mockImplementation(() => Promise.reject(response));
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockRejectedValue(response);
await findRunPipelineBtn().trigger('click');
@@ -281,14 +287,16 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent({
- projectId: '5',
- mergeRequestId: 3,
- canCreatePipelineInTargetProject: true,
- sourceProjectFullPath: 'test/parent-project',
- targetProjectFullPath: 'test/fork-project',
+ props: {
+ projectId: '5',
+ mergeRequestId: 3,
+ canCreatePipelineInTargetProject: true,
+ sourceProjectFullPath: 'test/parent-project',
+ targetProjectFullPath: 'test/fork-project',
+ },
});
- jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockResolvedValue();
await waitForPromises();
});
@@ -313,15 +321,15 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []);
createComponent({
- projectId: '5',
- mergeRequestId: 3,
- canCreatePipelineInTargetProject: true,
- sourceProjectFullPath: 'test/parent-project',
- targetProjectFullPath: 'test/fork-project',
+ props: {
+ projectId: '5',
+ mergeRequestId: 3,
+ canCreatePipelineInTargetProject: true,
+ sourceProjectFullPath: 'test/parent-project',
+ targetProjectFullPath: 'test/fork-project',
+ },
});
- jest.spyOn(findModal().vm, 'show').mockReturnValue();
-
await waitForPromises();
});
@@ -331,7 +339,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
findRunPipelineBtn().trigger('click');
- expect(findModal().vm.show).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/commons/nav/user_merge_requests_spec.js b/spec/frontend/commons/nav/user_merge_requests_spec.js
index f660cc8e9de..114cbbf812c 100644
--- a/spec/frontend/commons/nav/user_merge_requests_spec.js
+++ b/spec/frontend/commons/nav/user_merge_requests_spec.js
@@ -16,7 +16,10 @@ describe('User Merge Requests', () => {
let newBroadcastChannelMock;
beforeEach(() => {
+ jest.spyOn(document, 'dispatchEvent').mockReturnValue(false);
+
global.gon.current_user_id = 123;
+ global.gon.use_new_navigation = false;
channelMock = {
postMessage: jest.fn(),
@@ -73,6 +76,10 @@ describe('User Merge Requests', () => {
expect(channelMock.postMessage).not.toHaveBeenCalled();
});
});
+
+ it('does not emit event to refetch counts', () => {
+ expect(document.dispatchEvent).not.toHaveBeenCalled();
+ });
});
describe('openUserCountsBroadcast', () => {
@@ -85,6 +92,7 @@ describe('User Merge Requests', () => {
channelMock.onmessage({ data: TEST_COUNT });
+ expect(newBroadcastChannelMock).toHaveBeenCalled();
expect(findMRCountText()).toEqual(TEST_COUNT.toLocaleString());
});
@@ -93,6 +101,7 @@ describe('User Merge Requests', () => {
openUserCountsBroadcast();
+ expect(newBroadcastChannelMock).toHaveBeenCalled();
expect(channelMock.close).toHaveBeenCalled();
});
});
@@ -118,4 +127,28 @@ describe('User Merge Requests', () => {
});
});
});
+
+ describe('if new navigation is enabled', () => {
+ beforeEach(() => {
+ global.gon.use_new_navigation = true;
+ jest.spyOn(UserApi, 'getUserCounts');
+ });
+
+ it('openUserCountsBroadcast is a noop', () => {
+ openUserCountsBroadcast();
+ expect(newBroadcastChannelMock).not.toHaveBeenCalled();
+ });
+
+ describe('refreshUserMergeRequestCounts', () => {
+ it('does not call api', async () => {
+ await refreshUserMergeRequestCounts();
+ expect(UserApi.getUserCounts).not.toHaveBeenCalled();
+ });
+
+ it('emits event to refetch counts', async () => {
+ await refreshUserMergeRequestCounts();
+ expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('todo:toggle'));
+ });
+ });
+ });
});
diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
index d6f16f1a644..a7ae07a36d9 100644
--- a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
+++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
@@ -46,7 +46,6 @@ function factory(projects = mockData) {
describe('Confidential merge request project form group component', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
it('renders fork dropdown', async () => {
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 a63cca006da..a328f79e4e7 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,7 +1,7 @@
// 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=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" aria-label=\\"Bold\\" title=\\"Bold\\" class=\\"gl-button btn-default-tertiary btn-icon\\">
+"<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>
<!---->
diff --git a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap b/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
deleted file mode 100644
index 331a0a474a3..00000000000
--- a/spec/frontend/content_editor/components/__snapshots__/toolbar_link_button_spec.js.snap
+++ /dev/null
@@ -1,33 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`content_editor/components/toolbar_link_button renders dropdown component 1`] = `
-"<div title=\\"Insert link\\" lazy=\\"\\">
- <li role=\\"presentation\\" class=\\"gl-px-3!\\">
- <form tabindex=\\"-1\\" class=\\"b-dropdown-form gl-p-0\\">
- <div role=\\"group\\" class=\\"input-group\\" placeholder=\\"Link URL\\">
- <!---->
- <!----> <input type=\\"text\\" placeholder=\\"Link URL\\" class=\\"form-control gl-form-input\\">
- <div class=\\"input-group-append\\"><button type=\\"button\\" class=\\"btn btn-confirm btn-md gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Apply</span></button></div>
- <!---->
- </div>
- </form>
- </li>
- <li role=\\"presentation\\" class=\\"gl-dropdown-divider\\">
- <hr role=\\"separator\\" aria-orientation=\\"horizontal\\" class=\\"dropdown-divider\\">
- </li>
- <li role=\\"presentation\\" class=\\"gl-dropdown-item\\"><button role=\\"menuitem\\" type=\\"button\\" class=\\"dropdown-item\\">
- <!---->
- <!---->
- <!---->
- <div class=\\"gl-dropdown-item-text-wrapper\\">
- <p class=\\"gl-dropdown-item-text-primary\\">
- Upload file
- </p>
- <!---->
- </div>
- <!---->
- </button></li>
-</div>"
-`;
diff --git a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
index 0700cf5d529..97716ce848c 100644
--- a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
@@ -51,10 +51,6 @@ describe('content_editor/components/bubble_menus/bubble_menu', () => {
setupMocks();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('initializes BubbleMenuPlugin', async () => {
createWrapper({});
@@ -68,10 +64,12 @@ describe('content_editor/components/bubble_menus/bubble_menu', () => {
tippyOptions: expect.objectContaining({
onHidden: expect.any(Function),
onShow: expect.any(Function),
+ appendTo: expect.any(Function),
...tippyOptions,
}),
});
+ expect(BubbleMenuPlugin.mock.calls[0][0].tippyOptions.appendTo()).toBe(document.body);
expect(tiptapEditor.registerPlugin).toHaveBeenCalledWith(pluginInitializationResult);
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
index 378b11f4ae9..2a6ab75227c 100644
--- a/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js
@@ -59,15 +59,11 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
checked: x.props('isChecked'),
}));
- beforeEach(async () => {
+ beforeEach(() => {
buildEditor();
buildWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders bubble menu component', async () => {
tiptapEditor.commands.insertContent(preTag());
bubbleMenu = wrapper.findComponent(BubbleMenu);
@@ -137,7 +133,7 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
});
describe('preview button', () => {
- it('does not appear for a regular code block', async () => {
+ it('does not appear for a regular code block', () => {
tiptapEditor.commands.insertContent('<pre lang="javascript">var a = 2;</pre>');
expect(wrapper.findByTestId('preview-diagram').exists()).toBe(false);
@@ -273,7 +269,7 @@ describe('content_editor/components/bubble_menus/code_block_bubble_menu', () =>
await emitEditorEvent({ event: 'transaction', tiptapEditor });
});
- it('hides the custom language input form and shows dropdown items', async () => {
+ it('hides the custom language input form and shows dropdown items', () => {
expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true);
expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false);
diff --git a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
deleted file mode 100644
index 98001858851..00000000000
--- a/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import { mockTracking } from 'helpers/tracking_helper';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting_bubble_menu.vue';
-import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
-import { stubComponent } from 'helpers/stub_component';
-
-import {
- BUBBLE_MENU_TRACKING_ACTION,
- CONTENT_EDITOR_TRACKING_LABEL,
-} from '~/content_editor/constants';
-import { createTestEditor } from '../../test_utils';
-
-describe('content_editor/components/bubble_menus/formatting_bubble_menu', () => {
- let wrapper;
- let trackingSpy;
- let tiptapEditor;
-
- const buildEditor = () => {
- tiptapEditor = createTestEditor();
-
- jest.spyOn(tiptapEditor, 'isActive');
- };
-
- const buildWrapper = () => {
- wrapper = shallowMountExtended(FormattingBubbleMenu, {
- provide: {
- tiptapEditor,
- },
- stubs: {
- BubbleMenu: stubComponent(BubbleMenu),
- },
- });
- };
-
- beforeEach(() => {
- trackingSpy = mockTracking(undefined, null, jest.spyOn);
- buildEditor();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders bubble menu component', () => {
- buildWrapper();
- const bubbleMenu = wrapper.findComponent(BubbleMenu);
-
- expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
- });
-
- describe.each`
- testId | controlProps
- ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
- ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
- ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }}
- ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
- ${'superscript'} | ${{ contentType: 'superscript', iconName: 'superscript', label: 'Superscript', editorCommand: 'toggleSuperscript' }}
- ${'subscript'} | ${{ contentType: 'subscript', iconName: 'subscript', label: 'Subscript', editorCommand: 'toggleSubscript' }}
- ${'highlight'} | ${{ contentType: 'highlight', iconName: 'highlight', label: 'Highlight', editorCommand: 'toggleHighlight' }}
- ${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link', editorCommand: 'toggleLink', editorCommandParams: { href: '' } }}
- `('given a $testId toolbar control', ({ testId, controlProps }) => {
- beforeEach(() => {
- buildWrapper();
- });
-
- it('renders the toolbar control with the provided properties', () => {
- expect(wrapper.findByTestId(testId).exists()).toBe(true);
-
- expect(wrapper.findByTestId(testId).props()).toEqual(
- expect.objectContaining({
- ...controlProps,
- size: 'medium',
- category: 'tertiary',
- }),
- );
- });
-
- it('tracks the execution of toolbar controls', () => {
- const eventData = { contentType: 'italic', value: 1 };
- const { contentType, value } = eventData;
-
- wrapper.findByTestId(testId).vm.$emit('execute', eventData);
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, BUBBLE_MENU_TRACKING_ACTION, {
- label: CONTENT_EDITOR_TRACKING_LABEL,
- property: contentType,
- value,
- });
- });
- });
-});
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
index 9aa9c6483f4..c79df9c9ed8 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
@@ -7,7 +7,7 @@ import eventHubFactory from '~/helpers/event_hub_factory';
import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
import { stubComponent } from 'helpers/stub_component';
import Link from '~/content_editor/extensions/link';
-import { createTestEditor } from '../../test_utils';
+import { createTestEditor, emitEditorEvent, createTransactionWithMeta } from '../../test_utils';
const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
@@ -59,22 +59,18 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
expect(wrapper.findByTestId('remove-link').exists()).toBe(exist);
};
- beforeEach(async () => {
+ beforeEach(() => {
buildEditor();
tiptapEditor
.chain()
- .insertContent(
- 'Download <a href="/path/to/project/-/wikis/uploads/my_file.pdf" data-canonical-src="uploads/my_file.pdf" title="Click here to download">PDF File</a>',
+ .setContent(
+ 'Download <a href="/path/to/project/-/wikis/uploads/my_file.pdf" data-canonical-src="uploads/my_file.pdf">PDF File</a>',
)
.setTextSelection(14) // put cursor in the middle of the link
.run();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders bubble menu component', async () => {
await buildWrapperAndDisplayMenu();
@@ -88,13 +84,42 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
expect.objectContaining({
href: '/path/to/project/-/wikis/uploads/my_file.pdf',
'aria-label': 'uploads/my_file.pdf',
- title: 'uploads/my_file.pdf',
target: '_blank',
}),
);
expect(findLink().text()).toBe('uploads/my_file.pdf');
});
+ it('shows a loading percentage for a file being uploaded', async () => {
+ const setUploadProgress = async (progress) => {
+ const transaction = createTransactionWithMeta('uploadProgress', {
+ filename: 'my_file.pdf',
+ progress,
+ });
+ await emitEditorEvent({ event: 'transaction', tiptapEditor, params: { transaction } });
+ };
+
+ tiptapEditor
+ .chain()
+ .extendMarkRange('link')
+ .updateAttributes('link', { uploading: 'my_file.pdf' })
+ .run();
+
+ await buildWrapperAndDisplayMenu();
+
+ expect(findLink().exists()).toBe(false);
+ expect(wrapper.text()).toContain('Uploading: 0%');
+
+ await setUploadProgress(0.4);
+ expect(wrapper.text()).toContain('Uploading: 40%');
+
+ await setUploadProgress(0.7);
+ expect(wrapper.text()).toContain('Uploading: 70%');
+
+ await setUploadProgress(1);
+ expect(wrapper.text()).toContain('Uploading: 100%');
+ });
+
it('updates the bubble menu state when @selectionUpdate event is triggered', async () => {
const linkUrl = 'https://gitlab.com';
@@ -185,52 +210,17 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
});
});
- describe('for a placeholder link', () => {
- beforeEach(async () => {
- tiptapEditor
- .chain()
- .clearContent()
- .insertContent('Dummy link')
- .selectAll()
- .setLink({ href: '' })
- .setTextSelection(4)
- .run();
-
- await buildWrapperAndDisplayMenu();
- });
-
- it('directly opens the edit form for a placeholder link', async () => {
- expectLinkButtonsToExist(false);
-
- expect(wrapper.findComponent(GlForm).exists()).toBe(true);
- });
-
- it('removes the link on clicking apply (if no change)', async () => {
- await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
-
- expect(tiptapEditor.getHTML()).toBe('<p>Dummy link</p>');
- });
-
- it('removes the link on clicking cancel', async () => {
- await wrapper.findByTestId('cancel-link').vm.$emit('click');
-
- expect(tiptapEditor.getHTML()).toBe('<p>Dummy link</p>');
- });
- });
-
describe('edit button', () => {
let linkHrefInput;
- let linkTitleInput;
beforeEach(async () => {
await buildWrapperAndDisplayMenu();
await wrapper.findByTestId('edit-link').vm.$emit('click');
linkHrefInput = wrapper.findByTestId('link-href');
- linkTitleInput = wrapper.findByTestId('link-title');
});
- it('hides the link and copy/edit/remove link buttons', async () => {
+ it('hides the link and copy/edit/remove link buttons', () => {
expectLinkButtonsToExist(false);
});
@@ -238,7 +228,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
expect(wrapper.findComponent(GlForm).exists()).toBe(true);
expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
- expect(linkTitleInput.element.value).toBe('Click here to download');
});
it('extends selection to select the entire link', () => {
@@ -251,26 +240,18 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
describe('after making changes in the form and clicking apply', () => {
beforeEach(async () => {
linkHrefInput.setValue('https://google.com');
- linkTitleInput.setValue('Search Google');
contentEditor.resolveUrl.mockResolvedValue('https://google.com');
await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
});
- it('updates prosemirror doc with new link', async () => {
- expect(tiptapEditor.getHTML()).toBe(
- '<p>Download <a target="_blank" rel="noopener noreferrer nofollow" href="https://google.com" title="Search Google">PDF File</a></p>',
- );
- });
-
it('updates the link in the bubble menu', () => {
const link = wrapper.findComponent(GlLink);
expect(link.attributes()).toEqual(
expect.objectContaining({
href: 'https://google.com',
'aria-label': 'https://google.com',
- title: 'https://google.com',
target: '_blank',
}),
);
@@ -281,7 +262,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
describe('after making changes in the form and clicking cancel', () => {
beforeEach(async () => {
linkHrefInput.setValue('https://google.com');
- linkTitleInput.setValue('Search Google');
await wrapper.findByTestId('cancel-link').vm.$emit('click');
});
@@ -289,17 +269,6 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
it('hides the form and shows the copy/edit/remove link buttons', () => {
expectLinkButtonsToExist();
});
-
- it('resets the form with old values of the link from prosemirror', async () => {
- // click edit once again to show the form back
- await wrapper.findByTestId('edit-link').vm.$emit('click');
-
- linkHrefInput = wrapper.findByTestId('link-href');
- linkTitleInput = wrapper.findByTestId('link-title');
-
- expect(linkHrefInput.element.value).toBe('uploads/my_file.pdf');
- expect(linkTitleInput.element.value).toBe('Click here to download');
- });
});
});
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
index 13c6495ac41..89beb76a6f2 100644
--- a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
@@ -1,49 +1,61 @@
+import { nextTick } from 'vue';
import { GlLink, GlForm } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
import { stubComponent } from 'helpers/stub_component';
import eventHubFactory from '~/helpers/event_hub_factory';
-import Image from '~/content_editor/extensions/image';
import Audio from '~/content_editor/extensions/audio';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
+import Image from '~/content_editor/extensions/image';
import Video from '~/content_editor/extensions/video';
-import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils';
+import {
+ createTestEditor,
+ emitEditorEvent,
+ mockChainedCommands,
+ createTransactionWithMeta,
+} from '../../test_utils';
import {
PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+ PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../../test_constants';
-const TIPTAP_IMAGE_HTML = `<p>
+const TIPTAP_AUDIO_HTML = `<p>
+ <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png" class="with-attachment-icon">gitlab favicon</a></span>
+</p>`;
+
+const TIPTAP_DIAGRAM_HTML = `<p>
<img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
-const TIPTAP_AUDIO_HTML = `<p>
- <span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+const TIPTAP_IMAGE_HTML = `<p>
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
</p>`;
const TIPTAP_VIDEO_HTML = `<p>
- <span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png">gitlab favicon</a></span>
+ <span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png" class="with-attachment-icon">gitlab favicon</a></span>
</p>`;
const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() });
describe.each`
- mediaType | mediaHTML | filePath | mediaOutputHTML
- ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
- ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
- ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
+ mediaType | mediaHTML | filePath | mediaOutputHTML
+ ${'image'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${'test-file.png'} | ${TIPTAP_IMAGE_HTML}
+ ${'drawioDiagram'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${'test-file.drawio.svg'} | ${TIPTAP_DIAGRAM_HTML}
+ ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${'test-file.mp3'} | ${TIPTAP_AUDIO_HTML}
+ ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${'test-file.mp4'} | ${TIPTAP_VIDEO_HTML}
`(
'content_editor/components/bubble_menus/media_bubble_menu ($mediaType)',
({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => {
let wrapper;
let tiptapEditor;
let contentEditor;
- let bubbleMenu;
let eventHub;
const buildEditor = () => {
- tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] });
+ tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video, DrawioDiagram] });
contentEditor = { resolveUrl: jest.fn() };
eventHub = eventHubFactory();
};
@@ -61,6 +73,24 @@ describe.each`
});
};
+ const findBubbleMenu = () => wrapper.findComponent(BubbleMenu);
+
+ const showMenu = async () => {
+ findBubbleMenu().vm.$emit('show');
+ await emitEditorEvent({
+ event: 'transaction',
+ tiptapEditor,
+ params: { transaction: createTransactionWithMeta() },
+ });
+ await nextTick();
+ };
+
+ const buildWrapperAndDisplayMenu = () => {
+ buildWrapper();
+
+ return showMenu();
+ };
+
const selectFile = async (file) => {
const input = wrapper.findComponent({ ref: 'fileSelector' });
@@ -76,9 +106,8 @@ describe.each`
expect(wrapper.findByTestId('delete-media').exists()).toBe(exist);
};
- beforeEach(async () => {
+ beforeEach(() => {
buildEditor();
- buildWrapper();
tiptapEditor
.chain()
@@ -87,21 +116,17 @@ describe.each`
.run();
contentEditor.resolveUrl.mockResolvedValue(`/group1/project1/-/wikis/${filePath}`);
-
- await emitEditorEvent({ event: 'transaction', tiptapEditor });
-
- bubbleMenu = wrapper.findComponent(BubbleMenu);
- });
-
- afterEach(() => {
- wrapper.destroy();
});
it('renders bubble menu component', async () => {
- expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
+ await buildWrapperAndDisplayMenu();
+
+ expect(findBubbleMenu().classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']);
});
it('shows a clickable link to the image', async () => {
+ await buildWrapperAndDisplayMenu();
+
const link = wrapper.findComponent(GlLink);
expect(link.attributes()).toEqual(
expect.objectContaining({
@@ -114,8 +139,61 @@ describe.each`
expect(link.text()).toBe(filePath);
});
+ it('shows a loading percentage for a file being uploaded', async () => {
+ jest.spyOn(tiptapEditor, 'isActive').mockImplementation((name) => name === mediaType);
+
+ await buildWrapperAndDisplayMenu();
+
+ const setUploadProgress = async (progress) => {
+ const transaction = createTransactionWithMeta('uploadProgress', {
+ filename: filePath,
+ progress,
+ });
+ await emitEditorEvent({ event: 'transaction', tiptapEditor, params: { transaction } });
+ };
+
+ tiptapEditor.chain().selectAll().updateAttributes(mediaType, { uploading: filePath }).run();
+
+ await emitEditorEvent({ event: 'selectionUpdate', tiptapEditor });
+
+ expect(wrapper.findComponent(GlLink).exists()).toBe(false);
+ expect(wrapper.text()).toContain('Uploading: 0%');
+
+ await setUploadProgress(0.4);
+
+ expect(wrapper.text()).toContain('Uploading: 40%');
+
+ await setUploadProgress(0.7);
+ expect(wrapper.text()).toContain('Uploading: 70%');
+
+ await setUploadProgress(1);
+ expect(wrapper.text()).toContain('Uploading: 100%');
+ });
+
+ describe('when BubbleMenu emits hidden event', () => {
+ it('resets media bubble menu state', async () => {
+ await buildWrapperAndDisplayMenu();
+
+ // Switch to edit mode to access component state in form fields
+ await wrapper.findByTestId('edit-media').vm.$emit('click');
+
+ const mediaSrcInput = wrapper.findByTestId('media-src').vm.$el;
+ const mediaAltInput = wrapper.findByTestId('media-alt').vm.$el;
+
+ expect(mediaSrcInput.value).not.toBe('');
+ expect(mediaAltInput.value).not.toBe('');
+
+ await wrapper.findComponent(BubbleMenu).vm.$emit('hidden');
+
+ expect(mediaSrcInput.value).toBe('');
+ expect(mediaAltInput.value).toBe('');
+ });
+ });
+
describe('copy button', () => {
it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
+ await buildWrapperAndDisplayMenu();
+
jest.spyOn(navigator.clipboard, 'writeText');
await wrapper.findByTestId('copy-media-src').vm.$emit('click');
@@ -126,6 +204,8 @@ describe.each`
describe(`remove ${mediaType} button`, () => {
it(`removes the ${mediaType}`, async () => {
+ await buildWrapperAndDisplayMenu();
+
await wrapper.findByTestId('delete-media').vm.$emit('click');
expect(tiptapEditor.getHTML()).toBe('<p>\n \n</p>');
@@ -133,23 +213,41 @@ describe.each`
});
describe(`replace ${mediaType} button`, () => {
- it('uploads and replaces the selected image when file input changes', async () => {
- const commands = mockChainedCommands(tiptapEditor, [
- 'focus',
- 'deleteSelection',
- 'uploadAttachment',
- 'run',
- ]);
- const file = new File(['foo'], 'foo.png', { type: 'image/png' });
-
- await wrapper.findByTestId('replace-media').vm.$emit('click');
- await selectFile(file);
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.deleteSelection).toHaveBeenCalled();
- expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
- expect(commands.run).toHaveBeenCalled();
- });
+ beforeEach(buildWrapperAndDisplayMenu);
+
+ if (mediaType !== 'drawioDiagram') {
+ it('uploads and replaces the selected image when file input changes', async () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'deleteSelection',
+ 'uploadAttachment',
+ 'run',
+ ]);
+ const file = new File(['foo'], 'foo.png', { type: 'image/png' });
+
+ await wrapper.findByTestId('replace-media').vm.$emit('click');
+ await selectFile(file);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.deleteSelection).toHaveBeenCalled();
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
+ expect(commands.run).toHaveBeenCalled();
+ });
+ } else {
+ // draw.io diagrams are replaced using the edit diagram button
+ it('invokes editDiagram command', async () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'createOrEditDiagram',
+ 'run',
+ ]);
+ await wrapper.findByTestId('edit-diagram').vm.$emit('click');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.createOrEditDiagram).toHaveBeenCalled();
+ expect(commands.run).toHaveBeenCalled();
+ });
+ }
});
describe('edit button', () => {
@@ -158,6 +256,8 @@ describe.each`
let mediaAltInput;
beforeEach(async () => {
+ await buildWrapperAndDisplayMenu();
+
await wrapper.findByTestId('edit-media').vm.$emit('click');
mediaSrcInput = wrapper.findByTestId('media-src');
@@ -165,7 +265,7 @@ describe.each`
mediaAltInput = wrapper.findByTestId('media-alt');
});
- it('hides the link and copy/edit/remove link buttons', async () => {
+ it('hides the link and copy/edit/remove link buttons', () => {
expectLinkButtonsToExist(false);
});
@@ -188,7 +288,7 @@ describe.each`
await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent());
});
- it(`updates prosemirror doc with new src to the ${mediaType}`, async () => {
+ it(`updates prosemirror doc with new src to the ${mediaType}`, () => {
expect(tiptapEditor.getHTML()).toBe(mediaOutputHTML);
});
diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js
index ee9ead8f8a7..e6873e2cf96 100644
--- a/spec/frontend/content_editor/components/content_editor_alert_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js
@@ -14,7 +14,7 @@ describe('content_editor/components/content_editor_alert', () => {
const findErrorAlert = () => wrapper.findComponent(GlAlert);
- const createWrapper = async () => {
+ const createWrapper = () => {
tiptapEditor = createTestEditor();
eventHub = eventHubFactory();
@@ -29,10 +29,6 @@ describe('content_editor/components/content_editor_alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
variant | message
${'danger'} | ${'An error occurred'}
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 1a3cd36a8bb..852c8a9591a 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { EditorContent, Editor } from '@tiptap/vue-2';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -6,7 +6,6 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
-import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting_bubble_menu.vue';
import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media_bubble_menu.vue';
@@ -27,19 +26,22 @@ describe('ContentEditor', () => {
const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver);
const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator);
const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert);
- const createWrapper = ({ markdown, autofocus, useBottomToolbar } = {}) => {
+ const createWrapper = ({ markdown, autofocus, ...props } = {}) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
renderMarkdown,
uploadsPath,
markdown,
autofocus,
- useBottomToolbar,
+ placeholder: 'Enter some text here...',
+ ...props,
},
stubs: {
EditorStateObserver,
ContentEditorProvider,
ContentEditorAlert,
+ GlLink,
+ GlSprintf,
},
});
};
@@ -48,10 +50,6 @@ describe('ContentEditor', () => {
renderMarkdown = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('triggers initialized event', () => {
createWrapper();
@@ -87,22 +85,23 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true);
});
- it('renders top toolbar component', () => {
+ it('renders toolbar component', () => {
createWrapper();
expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
- expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(false);
- expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(true);
});
- it('renders bottom toolbar component', () => {
- createWrapper({
- useBottomToolbar: true,
- });
+ it('renders footer containing quick actions help text if quick actions docs path is defined', () => {
+ createWrapper({ quickActionsDocsPath: '/foo/bar' });
- expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
- expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-t')).toBe(true);
- expect(wrapper.findComponent(FormattingToolbar).classes('gl-border-b')).toBe(false);
+ expect(findEditorElement().text()).toContain('For quick actions, type /');
+ expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/foo/bar');
+ });
+
+ it('does not render footer containing quick actions help text if quick actions docs path is not defined', () => {
+ createWrapper();
+
+ expect(findEditorElement().text()).not.toContain('For quick actions, type /');
});
describe('when setting initial content', () => {
@@ -124,9 +123,9 @@ describe('ContentEditor', () => {
describe('succeeds', () => {
beforeEach(async () => {
- renderMarkdown.mockResolvedValueOnce('hello world');
+ renderMarkdown.mockResolvedValueOnce('');
- createWrapper({ markddown: 'hello world' });
+ createWrapper({ markddown: '' });
await nextTick();
});
@@ -138,13 +137,17 @@ describe('ContentEditor', () => {
it('emits loadingSuccess event', () => {
expect(wrapper.emitted('loadingSuccess')).toHaveLength(1);
});
+
+ it('shows placeholder text', () => {
+ expect(wrapper.text()).toContain('Enter some text here...');
+ });
});
describe('fails', () => {
beforeEach(async () => {
renderMarkdown.mockRejectedValueOnce(new Error());
- createWrapper({ markddown: 'hello world' });
+ createWrapper({ markdown: 'hello world' });
await nextTick();
});
@@ -209,11 +212,17 @@ describe('ContentEditor', () => {
expect(findEditorElement().classes()).not.toContain('is-focused');
});
+
+ it('hides placeholder text', () => {
+ expect(wrapper.text()).not.toContain('Enter some text here...');
+ });
});
describe('when editorStateObserver emits docUpdate event', () => {
- it('emits change event with the latest markdown', async () => {
- const markdown = 'Loaded content';
+ let markdown;
+
+ beforeEach(async () => {
+ markdown = 'Loaded content';
renderMarkdown.mockResolvedValueOnce(markdown);
@@ -223,7 +232,9 @@ describe('ContentEditor', () => {
await waitForPromises();
findEditorStateObserver().vm.$emit('docUpdate');
+ });
+ it('emits change event with the latest markdown', () => {
expect(wrapper.emitted('change')).toEqual([
[
{
@@ -234,6 +245,10 @@ describe('ContentEditor', () => {
],
]);
});
+
+ it('hides the placeholder text', () => {
+ expect(wrapper.text()).not.toContain('Enter some text here...');
+ });
});
describe('when editorStateObserver emits keydown event', () => {
@@ -248,11 +263,10 @@ describe('ContentEditor', () => {
});
it.each`
- name | component
- ${'formatting'} | ${FormattingBubbleMenu}
- ${'link'} | ${LinkBubbleMenu}
- ${'media'} | ${MediaBubbleMenu}
- ${'codeBlock'} | ${CodeBlockBubbleMenu}
+ name | component
+ ${'link'} | ${LinkBubbleMenu}
+ ${'media'} | ${MediaBubbleMenu}
+ ${'codeBlock'} | ${CodeBlockBubbleMenu}
`('renders formatting bubble menu', ({ component }) => {
createWrapper();
diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js
index 9b42f61c98c..80fb20e5258 100644
--- a/spec/frontend/content_editor/components/editor_state_observer_spec.js
+++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js
@@ -45,10 +45,6 @@ describe('content_editor/components/editor_state_observer', () => {
buildEditor();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when editor content changes', () => {
it('emits update, selectionUpdate, and transaction events', () => {
const content = '<p>My paragraph</p>';
diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index c4bf21ba813..e04c6a00765 100644
--- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -1,3 +1,4 @@
+import { GlTabs, GlTab } from '@gitlab/ui';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
@@ -5,35 +6,39 @@ import {
TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
-describe('content_editor/components/top_toolbar', () => {
+describe('content_editor/components/formatting_toolbar', () => {
let wrapper;
let trackingSpy;
const buildWrapper = () => {
- wrapper = shallowMountExtended(FormattingToolbar);
+ wrapper = shallowMountExtended(FormattingToolbar, {
+ stubs: {
+ GlTabs,
+ GlTab,
+ EditorModeSwitcher,
+ },
+ });
};
beforeEach(() => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
testId | controlProps
${'text-styles'} | ${{}}
${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
+ ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }}
${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
${'link'} | ${{}}
${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }}
- ${'image'} | ${{}}
+ ${'attachment'} | ${{}}
${'table'} | ${{}}
${'more'} | ${{}}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
@@ -62,4 +67,10 @@ describe('content_editor/components/top_toolbar', () => {
});
});
});
+
+ it('renders an editor mode dropdown', () => {
+ buildWrapper();
+
+ expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js
index 0065103d01b..1b0ffaee6c6 100644
--- a/spec/frontend/content_editor/components/loading_indicator_spec.js
+++ b/spec/frontend/content_editor/components/loading_indicator_spec.js
@@ -11,10 +11,6 @@ describe('content_editor/components/loading_indicator', () => {
wrapper = shallowMountExtended(LoadingIndicator);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when loading content', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
index e72eb892e74..9d34d9d0e9e 100644
--- a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlAvatarLabeled, GlDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem, GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue';
@@ -75,6 +75,26 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
unicodeVersion: '6.0',
};
+ it.each`
+ loading | description
+ ${false} | ${'does not show a loading indicator'}
+ ${true} | ${'shows a loading indicator'}
+ `('$description if loading=$loading', ({ loading }) => {
+ buildWrapper({
+ propsData: {
+ loading,
+ char: '@',
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'member',
+ },
+ items: [exampleUser],
+ },
+ });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(loading);
+ });
+
describe('on item select', () => {
it.each`
nodeType | referenceType | char | reference | insertedText | insertedProps
diff --git a/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js b/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js
new file mode 100644
index 00000000000..c6793d5b01b
--- /dev/null
+++ b/spec/frontend/content_editor/components/toolbar_attachment_button_spec.js
@@ -0,0 +1,60 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ToolbarAttachmentButton from '~/content_editor/components/toolbar_attachment_button.vue';
+import Attachment from '~/content_editor/extensions/attachment';
+import Link from '~/content_editor/extensions/link';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
+
+describe('content_editor/components/toolbar_attachment_button', () => {
+ let wrapper;
+ let editor;
+
+ const buildWrapper = () => {
+ wrapper = mountExtended(ToolbarAttachmentButton, {
+ provide: {
+ tiptapEditor: editor,
+ },
+ });
+ };
+
+ const selectFiles = async (...files) => {
+ const input = wrapper.findComponent({ ref: 'fileSelector' });
+
+ // override the property definition because `input.files` isn't directly modifyable
+ Object.defineProperty(input.element, 'files', { value: files, writable: true });
+ await input.trigger('change');
+ };
+
+ beforeEach(() => {
+ editor = createTestEditor({
+ extensions: [
+ Link,
+ Image,
+ Attachment.configure({
+ renderMarkdown: jest.fn(),
+ uploadsPath: '/uploads/',
+ }),
+ ],
+ });
+
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ editor.destroy();
+ });
+
+ it('uploads the selected attachment when file input changes', async () => {
+ const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
+ const file1 = new File(['foo'], 'foo.png', { type: 'image/png' });
+ const file2 = new File(['bar'], 'bar.png', { type: 'image/png' });
+
+ await selectFiles(file1, file2);
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file: file1 });
+ expect(commands.uploadAttachment).toHaveBeenCalledWith({ file: file2 });
+ expect(commands.run).toHaveBeenCalled();
+
+ expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link', value: 'upload' }]);
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js
index 1f1f7b338c6..ffe1ae20ee9 100644
--- a/spec/frontend/content_editor/components/toolbar_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_button_spec.js
@@ -42,10 +42,6 @@ describe('content_editor/components/toolbar_button', () => {
buildEditor();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays tertiary, medium button with a provided label and icon', () => {
buildWrapper();
@@ -85,7 +81,7 @@ describe('content_editor/components/toolbar_button', () => {
await emitEditorEvent({ event: 'transaction', tiptapEditor });
- expect(findButton().classes().includes('active')).toBe(outcome);
+ expect(findButton().classes().includes('gl-bg-gray-100!')).toBe(outcome);
expect(tiptapEditor.isActive).toHaveBeenCalledWith(CONTENT_TYPE);
},
);
diff --git a/spec/frontend/content_editor/components/toolbar_image_button_spec.js b/spec/frontend/content_editor/components/toolbar_image_button_spec.js
deleted file mode 100644
index 5473d43f5a1..00000000000
--- a/spec/frontend/content_editor/components/toolbar_image_button_spec.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import { GlButton, GlFormInputGroup, GlDropdown } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ToolbarImageButton from '~/content_editor/components/toolbar_image_button.vue';
-import Attachment from '~/content_editor/extensions/attachment';
-import Image from '~/content_editor/extensions/image';
-import { stubComponent } from 'helpers/stub_component';
-import { createTestEditor, mockChainedCommands } from '../test_utils';
-
-describe('content_editor/components/toolbar_image_button', () => {
- let wrapper;
- let editor;
-
- const buildWrapper = () => {
- wrapper = mountExtended(ToolbarImageButton, {
- provide: {
- tiptapEditor: editor,
- },
- stubs: {
- GlDropdown: stubComponent(GlDropdown),
- },
- });
- };
-
- const findImageURLInput = () =>
- wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
- const findApplyImageButton = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
-
- const selectFile = async (file) => {
- const input = wrapper.findComponent({ ref: 'fileSelector' });
-
- // override the property definition because `input.files` isn't directly modifyable
- Object.defineProperty(input.element, 'files', { value: [file], writable: true });
- await input.trigger('change');
- };
-
- beforeEach(() => {
- editor = createTestEditor({
- extensions: [
- Image,
- Attachment.configure({
- renderMarkdown: jest.fn(),
- uploadsPath: '/uploads/',
- }),
- ],
- });
-
- buildWrapper();
- });
-
- afterEach(() => {
- editor.destroy();
- wrapper.destroy();
- });
-
- it('sets the image to the value in the URL input when "Insert" button is clicked', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'setImage', 'run']);
-
- await findImageURLInput().setValue('https://example.com/img.jpg');
- await findApplyImageButton().trigger('click');
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.setImage).toHaveBeenCalledWith({
- alt: 'img',
- src: 'https://example.com/img.jpg',
- canonicalSrc: 'https://example.com/img.jpg',
- });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'url' }]);
- });
-
- it('uploads the selected image when file input changes', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
- const file = new File(['foo'], 'foo.png', { type: 'image/png' });
-
- await selectFile(file);
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'image', value: 'upload' }]);
- });
-
- describe('a11y tests', () => {
- it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
- buildWrapper();
-
- expect(findDropdown().props()).toMatchObject({
- text: 'Insert image',
- textSrOnly: true,
- });
- expect(findDropdown().attributes('title')).toBe('Insert image');
- });
- });
-});
diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js
deleted file mode 100644
index 40e859e96af..00000000000
--- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js
+++ /dev/null
@@ -1,224 +0,0 @@
-import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
-import eventHubFactory from '~/helpers/event_hub_factory';
-import Link from '~/content_editor/extensions/link';
-import { hasSelection } from '~/content_editor/services/utils';
-import { stubComponent } from 'helpers/stub_component';
-import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
-
-jest.mock('~/content_editor/services/utils');
-
-describe('content_editor/components/toolbar_link_button', () => {
- let wrapper;
- let editor;
-
- const buildWrapper = () => {
- wrapper = mountExtended(ToolbarLinkButton, {
- provide: {
- tiptapEditor: editor,
- eventHub: eventHubFactory(),
- },
- stubs: {
- GlDropdown: stubComponent(GlDropdown),
- },
- });
- };
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findLinkURLInput = () => wrapper.findComponent(GlFormInputGroup).find('input[type="text"]');
- const findApplyLinkButton = () => wrapper.findComponent(GlButton);
- const findRemoveLinkButton = () => wrapper.findByText('Remove link');
-
- const selectFile = async (file) => {
- const input = wrapper.findComponent({ ref: 'fileSelector' });
-
- // override the property definition because `input.files` isn't directly modifyable
- Object.defineProperty(input.element, 'files', { value: [file], writable: true });
- await input.trigger('change');
- };
-
- beforeEach(() => {
- editor = createTestEditor();
- });
-
- afterEach(() => {
- editor.destroy();
- wrapper.destroy();
- });
-
- it('renders dropdown component', () => {
- buildWrapper();
-
- expect(findDropdown().html()).toMatchSnapshot();
- });
-
- describe('when there is an active link', () => {
- beforeEach(async () => {
- jest.spyOn(editor, 'isActive').mockReturnValueOnce(true);
- buildWrapper();
-
- await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
- });
-
- it('sets dropdown as active when link extension is active', () => {
- expect(findDropdown().props('toggleClass')).toEqual({ active: true });
- });
-
- it('does not display the upload file option', () => {
- expect(wrapper.findByText('Upload file').exists()).toBe(false);
- });
-
- it('displays a remove link dropdown option', () => {
- expect(wrapper.findByText('Remove link').exists()).toBe(true);
- });
-
- it('executes removeLink command when the remove link option is clicked', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'run']);
-
- await findRemoveLinkButton().trigger('click');
-
- expect(commands.unsetLink).toHaveBeenCalled();
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.run).toHaveBeenCalled();
- });
-
- it('updates the link with a new link when "Apply" button is clicked', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'setLink', 'run']);
-
- await findLinkURLInput().setValue('https://example');
- await findApplyLinkButton().trigger('click');
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.unsetLink).toHaveBeenCalled();
- expect(commands.setLink).toHaveBeenCalledWith({
- href: 'https://example',
- canonicalSrc: 'https://example',
- });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
- });
-
- describe('on selection update', () => {
- it('updates link input box with canonical-src if present', async () => {
- jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
- canonicalSrc: 'uploads/my-file.zip',
- href: '/username/my-project/uploads/abcdefgh133535/my-file.zip',
- });
-
- await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
-
- expect(findLinkURLInput().element.value).toEqual('uploads/my-file.zip');
- });
-
- it('updates link input box with link href otherwise', async () => {
- jest.spyOn(editor, 'getAttributes').mockReturnValueOnce({
- href: 'https://gitlab.com',
- });
-
- await emitEditorEvent({ event: 'transaction', tiptapEditor: editor });
-
- expect(findLinkURLInput().element.value).toEqual('https://gitlab.com');
- });
- });
- });
-
- describe('when there is no active link', () => {
- beforeEach(() => {
- jest.spyOn(editor, 'isActive');
- editor.isActive.mockReturnValueOnce(false);
- buildWrapper();
- });
-
- it('does not set dropdown as active', () => {
- expect(findDropdown().props('toggleClass')).toEqual({ active: false });
- });
-
- it('displays the upload file option', () => {
- expect(wrapper.findByText('Upload file').exists()).toBe(true);
- });
-
- it('does not display a remove link dropdown option', () => {
- expect(wrapper.findByText('Remove link').exists()).toBe(false);
- });
-
- it('sets the link to the value in the URL input when "Apply" button is clicked', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'unsetLink', 'setLink', 'run']);
-
- await findLinkURLInput().setValue('https://example');
- await findApplyLinkButton().trigger('click');
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.setLink).toHaveBeenCalledWith({
- href: 'https://example',
- canonicalSrc: 'https://example',
- });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
- });
-
- it('uploads the selected image when file input changes', async () => {
- const commands = mockChainedCommands(editor, ['focus', 'uploadAttachment', 'run']);
- const file = new File(['foo'], 'foo.png', { type: 'image/png' });
-
- await selectFile(file);
-
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.uploadAttachment).toHaveBeenCalledWith({ file });
- expect(commands.run).toHaveBeenCalled();
-
- expect(wrapper.emitted().execute[0]).toEqual([{ contentType: 'link' }]);
- });
- });
-
- describe('when the user displays the dropdown', () => {
- let commands;
-
- beforeEach(() => {
- commands = mockChainedCommands(editor, ['focus', 'extendMarkRange', 'run']);
- });
-
- describe('given the user has not selected text', () => {
- beforeEach(() => {
- hasSelection.mockReturnValueOnce(false);
- });
-
- it('the editor selection is extended to the current mark extent', () => {
- buildWrapper();
-
- findDropdown().vm.$emit('show');
- expect(commands.extendMarkRange).toHaveBeenCalledWith(Link.name);
- expect(commands.focus).toHaveBeenCalled();
- expect(commands.run).toHaveBeenCalled();
- });
- });
-
- describe('given the user has selected text', () => {
- beforeEach(() => {
- hasSelection.mockReturnValueOnce(true);
- });
-
- it('the editor does not modify the current selection', () => {
- buildWrapper();
-
- findDropdown().vm.$emit('show');
- expect(commands.extendMarkRange).not.toHaveBeenCalled();
- expect(commands.focus).not.toHaveBeenCalled();
- expect(commands.run).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('a11y tests', () => {
- it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
- buildWrapper();
-
- expect(findDropdown().props()).toMatchObject({
- text: 'Insert link',
- textSrOnly: true,
- });
- expect(findDropdown().attributes('title')).toBe('Insert link');
- });
- });
-});
diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
index d4fc47601cf..78b02744d51 100644
--- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -9,12 +9,14 @@ import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_
describe('content_editor/components/toolbar_more_dropdown', () => {
let wrapper;
let tiptapEditor;
+ let contentEditor;
let eventHub;
const buildEditor = () => {
tiptapEditor = createTestEditor({
extensions: [Diagram, HorizontalRule],
});
+ contentEditor = { drawioEnabled: true };
eventHub = eventHubFactory();
};
@@ -22,6 +24,7 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
wrapper = mountExtended(ToolbarMoreDropdown, {
provide: {
tiptapEditor,
+ contentEditor,
eventHub,
},
propsData,
@@ -32,29 +35,27 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
beforeEach(() => {
buildEditor();
- buildWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
});
describe.each`
- name | contentType | command | params
- ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
- ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
- ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
- ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
- ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
- ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
- ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
- ${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
- ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
+ name | contentType | command | params
+ ${'Code block'} | ${'codeBlock'} | ${'setNode'} | ${['codeBlock']}
+ ${'Details block'} | ${'details'} | ${'toggleList'} | ${['details', 'detailsContent']}
+ ${'Bullet list'} | ${'bulletList'} | ${'toggleList'} | ${['bulletList', 'listItem']}
+ ${'Ordered list'} | ${'orderedList'} | ${'toggleList'} | ${['orderedList', 'listItem']}
+ ${'Task list'} | ${'taskList'} | ${'toggleList'} | ${['taskList', 'taskItem']}
+ ${'Mermaid diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'mermaid' }]}
+ ${'PlantUML diagram'} | ${'diagram'} | ${'setNode'} | ${['diagram', { language: 'plantuml' }]}
+ ${'Table of contents'} | ${'tableOfContents'} | ${'insertTableOfContents'} | ${[]}
+ ${'Horizontal rule'} | ${'horizontalRule'} | ${'setHorizontalRule'} | ${[]}
+ ${'Create or edit diagram'} | ${'drawioDiagram'} | ${'createOrEditDiagram'} | ${[]}
`('when option $name is clicked', ({ name, command, contentType, params }) => {
let commands;
let btn;
- beforeEach(async () => {
+ beforeEach(() => {
+ buildWrapper();
+
commands = mockChainedCommands(tiptapEditor, [command, 'focus', 'run']);
btn = wrapper.findByRole('button', { name });
});
@@ -71,8 +72,17 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
});
});
+ it('does not show drawio option when drawio is disabled', () => {
+ contentEditor.drawioEnabled = false;
+ buildWrapper();
+
+ expect(wrapper.findByRole('button', { name: 'Create or edit diagram' }).exists()).toBe(false);
+ });
+
describe('a11y tests', () => {
it('sets toggleText and text-sr-only properties to the table button dropdown', () => {
+ buildWrapper();
+
expect(findDropdown().props()).toMatchObject({
textSrOnly: true,
toggleText: 'More options',
diff --git a/spec/frontend/content_editor/components/toolbar_table_button_spec.js b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
index aa4604661e5..35741971488 100644
--- a/spec/frontend/content_editor/components/toolbar_table_button_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_table_button_spec.js
@@ -30,7 +30,6 @@ describe('content_editor/components/toolbar_table_button', () => {
afterEach(() => {
editor.destroy();
- wrapper.destroy();
});
it('renders a grid of 5x5 buttons to create a table', () => {
diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
index 5a725ac1ca4..97f6bdaf778 100644
--- a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
@@ -39,10 +39,6 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
buildEditor();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all text styles as dropdown items', () => {
buildWrapper();
@@ -121,7 +117,6 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
},
],
]);
- wrapper.destroy();
});
});
});
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 fb091419ad9..a9d42769789 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
@@ -8,7 +8,9 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
Table of contents
- <li>
+ <li
+ dir="auto"
+ >
<a
href="#"
>
@@ -17,8 +19,12 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
</a>
- <ul>
- <li>
+ <ul
+ dir="auto"
+ >
+ <li
+ dir="auto"
+ >
<a
href="#"
>
@@ -27,8 +33,12 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
</a>
- <ul>
- <li>
+ <ul
+ dir="auto"
+ >
+ <li
+ dir="auto"
+ >
<a
href="#"
>
@@ -41,7 +51,9 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
</li>
</ul>
</li>
- <li>
+ <li
+ dir="auto"
+ >
<a
href="#"
>
@@ -50,8 +62,12 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
</a>
- <ul>
- <li>
+ <ul
+ dir="auto"
+ >
+ <li
+ dir="auto"
+ >
<a
href="#"
>
@@ -64,7 +80,9 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
</li>
</ul>
</li>
- <li>
+ <li
+ dir="auto"
+ >
<a
href="#"
>
@@ -75,7 +93,9 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
<!---->
</li>
- <li>
+ <li
+ dir="auto"
+ >
<a
href="#"
>
@@ -84,8 +104,12 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
</a>
- <ul>
- <li>
+ <ul
+ dir="auto"
+ >
+ <li
+ dir="auto"
+ >
<a
href="#"
>
@@ -100,7 +124,9 @@ exports[`content/components/wrappers/table_of_contents collects all headings and
</li>
</ul>
</li>
- <li>
+ <li
+ dir="auto"
+ >
<a
href="#"
>
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 a5ef19fb8e8..cbeea90dcb4 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -26,7 +26,7 @@ describe('content/components/wrappers/code_block', () => {
eventHub = eventHubFactory();
};
- const createWrapper = async (nodeAttrs = { language }) => {
+ const createWrapper = (nodeAttrs = { language }) => {
updateAttributesFn = jest.fn();
wrapper = mountExtended(CodeBlockWrapper, {
@@ -55,10 +55,6 @@ describe('content/components/wrappers/code_block', () => {
codeBlockLanguageLoader.findOrCreateLanguageBySyntax.mockReturnValue({ syntax: language });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a node-view-wrapper as a pre element', () => {
createWrapper();
@@ -101,7 +97,7 @@ describe('content/components/wrappers/code_block', () => {
jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(true);
});
- it('does not render a preview if showPreview: false', async () => {
+ it('does not render a preview if showPreview: false', () => {
createWrapper({ language: 'plantuml', isDiagram: true, showPreview: false });
expect(wrapper.findComponent({ ref: 'diagramContainer' }).exists()).toBe(false);
diff --git a/spec/frontend/content_editor/components/wrappers/details_spec.js b/spec/frontend/content_editor/components/wrappers/details_spec.js
index d746b9fa2f1..e35b04636f7 100644
--- a/spec/frontend/content_editor/components/wrappers/details_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/details_spec.js
@@ -5,7 +5,7 @@ import DetailsWrapper from '~/content_editor/components/wrappers/details.vue';
describe('content/components/wrappers/details', () => {
let wrapper;
- const createWrapper = async () => {
+ const createWrapper = () => {
wrapper = shallowMountExtended(DetailsWrapper, {
propsData: {
node: {},
@@ -13,10 +13,6 @@ describe('content/components/wrappers/details', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a node-view-content as a ul element', () => {
createWrapper();
diff --git a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
index 1ff750eb2ac..b5b118a2d9a 100644
--- a/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/footnote_definition_spec.js
@@ -4,7 +4,7 @@ import FootnoteDefinitionWrapper from '~/content_editor/components/wrappers/foot
describe('content/components/wrappers/footnote_definition', () => {
let wrapper;
- const createWrapper = async (node = {}) => {
+ const createWrapper = (node = {}) => {
wrapper = shallowMountExtended(FootnoteDefinitionWrapper, {
propsData: {
node,
@@ -12,10 +12,6 @@ describe('content/components/wrappers/footnote_definition', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders footnote label as a readyonly element', () => {
const label = 'footnote';
diff --git a/spec/frontend/content_editor/components/wrappers/label_spec.js b/spec/frontend/content_editor/components/wrappers/reference_label_spec.js
index 9e58669b0ea..f57caee911b 100644
--- a/spec/frontend/content_editor/components/wrappers/label_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/reference_label_spec.js
@@ -1,20 +1,16 @@
import { GlLabel } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import LabelWrapper from '~/content_editor/components/wrappers/label.vue';
+import ReferenceLabelWrapper from '~/content_editor/components/wrappers/reference_label.vue';
-describe('content/components/wrappers/label', () => {
+describe('content/components/wrappers/reference_label', () => {
let wrapper;
- const createWrapper = async (node = {}) => {
- wrapper = shallowMountExtended(LabelWrapper, {
+ const createWrapper = (node = {}) => {
+ wrapper = shallowMountExtended(ReferenceLabelWrapper, {
propsData: { node },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it("renders a GlLabel with the node's text and color", () => {
createWrapper({ attrs: { color: '#ff0000', text: 'foo bar', originalText: '~"foo bar"' } });
diff --git a/spec/frontend/content_editor/components/wrappers/reference_spec.js b/spec/frontend/content_editor/components/wrappers/reference_spec.js
new file mode 100644
index 00000000000..828b92a6b1e
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/reference_spec.js
@@ -0,0 +1,46 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ReferenceWrapper from '~/content_editor/components/wrappers/reference.vue';
+
+describe('content/components/wrappers/reference', () => {
+ let wrapper;
+
+ const createWrapper = (node = {}) => {
+ wrapper = shallowMountExtended(ReferenceWrapper, {
+ propsData: { node },
+ });
+ };
+
+ it('renders a span for commands', () => {
+ createWrapper({ attrs: { referenceType: 'command', text: '/assign' } });
+
+ const span = wrapper.find('span');
+ expect(span.text()).toBe('/assign');
+ });
+
+ it('renders an anchor for everything else', () => {
+ createWrapper({ attrs: { referenceType: 'issue', text: '#252522' } });
+
+ const link = wrapper.findComponent(GlLink);
+ expect(link.text()).toBe('#252522');
+ });
+
+ it('adds gfm-project_member class for project members', () => {
+ createWrapper({ attrs: { referenceType: 'user', text: '@root' } });
+
+ const link = wrapper.findComponent(GlLink);
+ expect(link.text()).toBe('@root');
+ expect(link.classes('gfm-project_member')).toBe(true);
+ expect(link.classes('current-user')).toBe(false);
+ });
+
+ it('adds a current-user class if the project member is current user', () => {
+ window.gon = { current_username: 'root' };
+
+ createWrapper({ attrs: { referenceType: 'user', text: '@root' } });
+
+ const link = wrapper.findComponent(GlLink);
+ expect(link.text()).toBe('@root');
+ expect(link.classes('current-user')).toBe(true);
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index 1fdddce3962..0d56280d630 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -1,25 +1,33 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { NodeViewWrapper } from '@tiptap/vue-2';
-import { selectedRect as getSelectedRect } from '@_ueberdosis/prosemirror-tables';
+import { selectedRect as getSelectedRect } from '@tiptap/pm/tables';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
-jest.mock('@_ueberdosis/prosemirror-tables');
+jest.mock('@tiptap/pm/tables');
describe('content/components/wrappers/table_cell_base', () => {
let wrapper;
let editor;
let node;
- const createWrapper = async (propsData = { cellType: 'td' }) => {
+ const createWrapper = (propsData = { cellType: 'td' }) => {
wrapper = shallowMountExtended(TableCellBaseWrapper, {
propsData: {
editor,
node,
...propsData,
},
+ stubs: {
+ GlDropdown: stubComponent(GlDropdown, {
+ methods: {
+ hide: jest.fn(),
+ },
+ }),
+ },
});
};
@@ -38,24 +46,12 @@ describe('content/components/wrappers/table_cell_base', () => {
jest.spyOn($cursor, 'node').mockReturnValue(node);
};
- const mockDropdownHide = () => {
- /*
- * TODO: Replace this method with using the scoped hide function
- * provided by BootstrapVue https://bootstrap-vue.org/docs/components/dropdown.
- * GitLab UI is not exposing it in the default scope
- */
- findDropdown().vm.hide = jest.fn();
- };
beforeEach(() => {
node = {};
editor = createTestEditor({});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a td node-view-wrapper with relative position', () => {
createWrapper();
expect(wrapper.findComponent(NodeViewWrapper).classes()).toContain('gl-relative');
@@ -100,8 +96,6 @@ describe('content/components/wrappers/table_cell_base', () => {
createWrapper();
await nextTick();
-
- mockDropdownHide();
});
it.each`
@@ -122,7 +116,7 @@ describe('content/components/wrappers/table_cell_base', () => {
},
);
- it('does not allow deleting rows and columns', async () => {
+ it('does not allow deleting rows and columns', () => {
expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
expect(findDropdownItemWithLabelExists('Delete column')).toBe(false);
});
@@ -177,7 +171,7 @@ describe('content/components/wrappers/table_cell_base', () => {
await nextTick();
});
- it('does not allow adding a row before the header', async () => {
+ it('does not allow adding a row before the header', () => {
expect(findDropdownItemWithLabelExists('Insert row before')).toBe(false);
});
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
index 2aefbc77545..4c91573e0c7 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_body_spec.js
@@ -8,7 +8,7 @@ describe('content/components/wrappers/table_cell_body', () => {
let editor;
let node;
- const createWrapper = async () => {
+ const createWrapper = () => {
wrapper = shallowMount(TableCellBodyWrapper, {
propsData: {
editor,
@@ -22,10 +22,6 @@ describe('content/components/wrappers/table_cell_body', () => {
editor = createTestEditor({});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a TableCellBase component', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
index e48df8734a6..689a8bc32bb 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_header_spec.js
@@ -8,7 +8,7 @@ describe('content/components/wrappers/table_cell_header', () => {
let editor;
let node;
- const createWrapper = async () => {
+ const createWrapper = () => {
wrapper = shallowMount(TableCellHeaderWrapper, {
propsData: {
editor,
@@ -22,10 +22,6 @@ describe('content/components/wrappers/table_cell_header', () => {
editor = createTestEditor({});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a TableCellBase component', () => {
createWrapper();
expect(wrapper.findComponent(TableCellBaseWrapper).props()).toEqual({
diff --git a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
index bfda89a8b09..037da7678bb 100644
--- a/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_of_contents_spec.js
@@ -20,7 +20,7 @@ describe('content/components/wrappers/table_of_contents', () => {
eventHub = eventHubFactory();
};
- const createWrapper = async () => {
+ const createWrapper = () => {
wrapper = mountExtended(TableOfContentsWrapper, {
propsData: {
editor: tiptapEditor,
@@ -70,10 +70,6 @@ describe('content/components/wrappers/table_of_contents', () => {
await nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a node-view-wrapper as a ul element', () => {
expect(wrapper.findComponent(NodeViewWrapper).props().as).toBe('ul');
});
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index 6b804b3b4c6..f037ac520fe 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -1,21 +1,22 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
import Attachment from '~/content_editor/extensions/attachment';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Image from '~/content_editor/extensions/image';
import Audio from '~/content_editor/extensions/audio';
import Video from '~/content_editor/extensions/video';
import Link from '~/content_editor/extensions/link';
-import Loading from '~/content_editor/extensions/loading';
-import { VARIANT_DANGER } from '~/flash';
+import { VARIANT_DANGER } from '~/alert';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
-import { createTestEditor, createDocBuilder } from '../test_utils';
+import { createTestEditor, createDocBuilder, expectDocumentAfterTransaction } from '../test_utils';
import {
PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ PROJECT_WIKI_ATTACHMENT_IMAGE_SVG_HTML,
PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
PROJECT_WIKI_ATTACHMENT_LINK_HTML,
+ PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../test_constants';
describe('content_editor/extensions/attachment', () => {
@@ -24,8 +25,8 @@ describe('content_editor/extensions/attachment', () => {
let p;
let image;
let audio;
+ let drawioDiagram;
let video;
- let loading;
let link;
let renderMarkdown;
let mock;
@@ -33,54 +34,57 @@ describe('content_editor/extensions/attachment', () => {
const uploadsPath = '/uploads/';
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
+ const imageFileSvg = new File(['foo'], 'test-file.svg', { type: 'image/svg+xml' });
const audioFile = new File(['foo'], 'test-file.mp3', { type: 'audio/mpeg' });
const videoFile = new File(['foo'], 'test-file.mp4', { type: 'video/mp4' });
+ const videoFile1 = new File(['foo'], 'test-file1.mp4', { type: 'video/mp4' });
+ const drawioDiagramFile = new File(['foo'], 'test-file.drawio.svg', { type: 'image/svg+xml' });
const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' });
-
- const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => {
- return new Promise((resolve) => {
- let counter = 1;
- const handleTransaction = async () => {
- if (counter === number) {
- expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
- tiptapEditor.off('update', handleTransaction);
- await waitForPromises();
- resolve();
- }
-
- counter += 1;
- };
-
- tiptapEditor.on('update', handleTransaction);
- action();
- });
+ const attachmentFile1 = new File(['foo'], 'test-file1.zip', { type: 'application/zip' });
+ const attachmentFile2 = new File(['foo'], 'test-file2.zip', { type: 'application/zip' });
+
+ const markdownApiResult = {
+ 'test-file.png': PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ 'test-file.svg': PROJECT_WIKI_ATTACHMENT_IMAGE_SVG_HTML,
+ 'test-file.mp3': PROJECT_WIKI_ATTACHMENT_AUDIO_HTML,
+ 'test-file.mp4': PROJECT_WIKI_ATTACHMENT_VIDEO_HTML,
+ 'test-file1.mp4': PROJECT_WIKI_ATTACHMENT_VIDEO_HTML.replace(/test-file/g, 'test-file1'),
+ 'test-file.zip': PROJECT_WIKI_ATTACHMENT_LINK_HTML,
+ 'test-file1.zip': PROJECT_WIKI_ATTACHMENT_LINK_HTML.replace(/test-file/g, 'test-file1'),
+ 'test-file2.zip': PROJECT_WIKI_ATTACHMENT_LINK_HTML.replace(/test-file/g, 'test-file2'),
+ 'test-file.drawio.svg': PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
};
+ const [, group, project] = markdownApiResult[attachmentFile.name].match(
+ /\/(group[0-9]+)\/(project[0-9]+)\//,
+ );
+ const blobUrl = 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b';
+
beforeEach(() => {
renderMarkdown = jest.fn();
eventHub = eventHubFactory();
tiptapEditor = createTestEditor({
extensions: [
- Loading,
Link,
Image,
Audio,
Video,
+ DrawioDiagram,
Attachment.configure({ renderMarkdown, uploadsPath, eventHub }),
],
});
({
- builders: { doc, p, image, audio, video, loading, link },
+ builders: { doc, p, image, audio, video, link, drawioDiagram },
} = createDocBuilder({
tiptapEditor,
names: {
- loading: { markType: Loading.name },
image: { nodeType: Image.name },
link: { nodeType: Link.name },
audio: { nodeType: Audio.name },
video: { nodeType: Video.name },
+ drawioDiagram: { nodeType: DrawioDiagram.name },
},
}));
@@ -97,6 +101,14 @@ describe('content_editor/extensions/attachment', () => {
${'paste'} | ${'handlePaste'} | ${{ clipboardData: { getData: jest.fn(), files: [] } }} | ${undefined}
${'drop'} | ${'handleDrop'} | ${{ dataTransfer: { getData: jest.fn(), files: [attachmentFile] } }} | ${true}
`('handles $eventType properly', ({ eventType, propName, eventData, output }) => {
+ mock.onPost().reply(HTTP_STATUS_OK, {
+ link: {
+ markdown: `![test-file](test-file.png)`,
+ },
+ });
+
+ renderMarkdown.mockResolvedValue(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML);
+
const event = Object.assign(new Event(eventType), eventData);
const handled = tiptapEditor.view.someProp(propName, (eventHandler) => {
return eventHandler(tiptapEditor.view, event);
@@ -113,13 +125,13 @@ describe('content_editor/extensions/attachment', () => {
});
describe.each`
- nodeType | mimeType | html | file | mediaType
- ${'image'} | ${'image/png'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
- ${'audio'} | ${'audio/mpeg'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
- ${'video'} | ${'video/mp4'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
- `('when the file has $nodeType mime type', ({ mimeType, html, file, mediaType }) => {
- const base64EncodedFile = `data:${mimeType};base64,Zm9v`;
-
+ nodeType | html | file | mediaType
+ ${'image (png)'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_HTML} | ${imageFile} | ${(attrs) => image(attrs)}
+ ${'image (svg)'} | ${PROJECT_WIKI_ATTACHMENT_IMAGE_SVG_HTML} | ${imageFileSvg} | ${(attrs) => image(attrs)}
+ ${'audio'} | ${PROJECT_WIKI_ATTACHMENT_AUDIO_HTML} | ${audioFile} | ${(attrs) => audio(attrs)}
+ ${'video'} | ${PROJECT_WIKI_ATTACHMENT_VIDEO_HTML} | ${videoFile} | ${(attrs) => video(attrs)}
+ ${'drawioDiagram'} | ${PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML} | ${drawioDiagramFile} | ${(attrs) => drawioDiagram(attrs)}
+ `('when the file is $nodeType', ({ html, file, mediaType }) => {
beforeEach(() => {
renderMarkdown.mockResolvedValue(html);
});
@@ -135,10 +147,13 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(HTTP_STATUS_OK, successResponse);
});
- it('inserts a media content with src set to the encoded content and uploading true', async () => {
- const expectedDoc = doc(p(mediaType({ uploading: true, src: base64EncodedFile })));
+ it('inserts a media content with src set to the encoded content and uploading=file_name', async () => {
+ const expectedDoc = doc(
+ p(mediaType({ uploading: file.name, src: blobUrl, alt: file.name })),
+ );
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 1,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file }),
@@ -150,14 +165,15 @@ describe('content_editor/extensions/attachment', () => {
p(
mediaType({
canonicalSrc: file.name,
- src: base64EncodedFile,
- alt: 'test-file',
+ src: blobUrl,
+ alt: expect.stringContaining('test-file'),
uploading: false,
}),
),
);
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 2,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file }),
@@ -165,6 +181,25 @@ describe('content_editor/extensions/attachment', () => {
});
});
+ describe('when uploading a large file', () => {
+ beforeEach(() => {
+ // Set max file size to 1 byte, our file is 3 bytes
+ gon.max_file_size = 1 / 1024 / 1024;
+ });
+
+ it('emits an alert event that includes an error message', () => {
+ tiptapEditor.commands.uploadAttachment({ file });
+
+ return new Promise((resolve) => {
+ eventHub.$on('alert', ({ message, variant }) => {
+ expect(variant).toBe(VARIANT_DANGER);
+ expect(message).toContain('File is too big');
+ resolve();
+ });
+ });
+ });
+ });
+
describe('when uploading request fails', () => {
beforeEach(() => {
mock.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
@@ -174,6 +209,7 @@ describe('content_editor/extensions/attachment', () => {
const expectedDoc = doc(p(''));
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 2,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file }),
@@ -195,10 +231,8 @@ describe('content_editor/extensions/attachment', () => {
});
describe('when the file has a zip (or any other attachment) mime type', () => {
- const markdownApiResult = PROJECT_WIKI_ATTACHMENT_LINK_HTML;
-
beforeEach(() => {
- renderMarkdown.mockResolvedValue(markdownApiResult);
+ renderMarkdown.mockResolvedValue(markdownApiResult[attachmentFile.name]);
});
describe('when uploading succeeds', () => {
@@ -212,18 +246,20 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(HTTP_STATUS_OK, successResponse);
});
- it('inserts a loading mark', async () => {
- const expectedDoc = doc(p(loading({ label: 'test-file' })));
+ it('inserts a link with a blob url', async () => {
+ const expectedDoc = doc(
+ p(link({ uploading: attachmentFile.name, href: blobUrl }, 'test-file.zip')),
+ );
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 1,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
});
});
- it('updates the loading mark with a link with canonicalSrc and href attrs', async () => {
- const [, group, project] = markdownApiResult.match(/\/(group[0-9]+)\/(project[0-9]+)\//);
+ it('updates the blob url link with an actual link with canonicalSrc and href attrs', async () => {
const expectedDoc = doc(
p(
link(
@@ -231,12 +267,13 @@ describe('content_editor/extensions/attachment', () => {
canonicalSrc: 'test-file.zip',
href: `/${group}/${project}/-/wikis/test-file.zip`,
},
- 'test-file',
+ 'test-file.zip',
),
),
);
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 2,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
@@ -253,6 +290,7 @@ describe('content_editor/extensions/attachment', () => {
const expectedDoc = doc(p(''));
await expectDocumentAfterTransaction({
+ tiptapEditor,
number: 2,
expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
@@ -269,5 +307,433 @@ describe('content_editor/extensions/attachment', () => {
});
});
});
+
+ describe('uploading multiple files', () => {
+ const uploadMultipleFiles = () => {
+ const files = [
+ attachmentFile,
+ imageFile,
+ videoFile,
+ attachmentFile1,
+ attachmentFile2,
+ videoFile1,
+ audioFile,
+ ];
+
+ for (const file of files) {
+ renderMarkdown.mockImplementation((markdown) =>
+ Promise.resolve(markdownApiResult[markdown.match(/\((.+?)\)$/)[1]]),
+ );
+
+ mock
+ .onPost()
+ .replyOnce(HTTP_STATUS_OK, { link: { markdown: `![test-file](${file.name})` } });
+
+ tiptapEditor.commands.uploadAttachment({ file });
+ }
+ };
+
+ it.each([
+ [1, () => doc(p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')))],
+ [
+ 2,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ ),
+ ],
+ [
+ 3,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ ),
+ ],
+ [
+ 4,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ ),
+ ],
+ [
+ 5,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ ),
+ ],
+ [
+ 6,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ ),
+ ],
+ [
+ 7,
+ () =>
+ doc(
+ p(link({ href: blobUrl, uploading: 'test-file.zip' }, 'test-file.zip')),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 8,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(image({ alt: 'test-file.png', src: blobUrl, uploading: 'test-file.png' })),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 9,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(video({ alt: 'test-file.mp4', src: blobUrl, uploading: 'test-file.mp4' })),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 10,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(link({ href: blobUrl, uploading: 'test-file1.zip' }, 'test-file1.zip')),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 11,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file1.zip`,
+ canonicalSrc: 'test-file1.zip',
+ uploading: false,
+ },
+ 'test-file1.zip',
+ ),
+ ),
+ p(link({ href: blobUrl, uploading: 'test-file2.zip' }, 'test-file2.zip')),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 12,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file1.zip`,
+ canonicalSrc: 'test-file1.zip',
+ uploading: false,
+ },
+ 'test-file1.zip',
+ ),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file2.zip`,
+ canonicalSrc: 'test-file2.zip',
+ uploading: false,
+ },
+ 'test-file2.zip',
+ ),
+ ),
+ p(video({ alt: 'test-file1.mp4', src: blobUrl, uploading: 'test-file1.mp4' })),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 13,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file1.zip`,
+ canonicalSrc: 'test-file1.zip',
+ uploading: false,
+ },
+ 'test-file1.zip',
+ ),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file2.zip`,
+ canonicalSrc: 'test-file2.zip',
+ uploading: false,
+ },
+ 'test-file2.zip',
+ ),
+ ),
+ p(
+ video({
+ alt: 'test-file1.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file1.mp4',
+ uploading: false,
+ }),
+ ),
+ p(audio({ alt: 'test-file.mp3', src: blobUrl, uploading: 'test-file.mp3' })),
+ ),
+ ],
+ [
+ 14,
+ () =>
+ doc(
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file.zip`,
+ canonicalSrc: 'test-file.zip',
+ uploading: false,
+ },
+ 'test-file.zip',
+ ),
+ ),
+ p(
+ image({
+ alt: 'test-file.png',
+ src: blobUrl,
+ canonicalSrc: 'test-file.png',
+ uploading: false,
+ }),
+ ),
+ p(
+ video({
+ alt: 'test-file.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file1.zip`,
+ canonicalSrc: 'test-file1.zip',
+ uploading: false,
+ },
+ 'test-file1.zip',
+ ),
+ ),
+ p(
+ link(
+ {
+ href: `/${group}/${project}/-/wikis/test-file2.zip`,
+ canonicalSrc: 'test-file2.zip',
+ uploading: false,
+ },
+ 'test-file2.zip',
+ ),
+ ),
+ p(
+ video({
+ alt: 'test-file1.mp4',
+ src: blobUrl,
+ canonicalSrc: 'test-file1.mp4',
+ uploading: false,
+ }),
+ ),
+ p(
+ audio({
+ alt: 'test-file.mp3',
+ src: blobUrl,
+ canonicalSrc: 'test-file.mp3',
+ uploading: false,
+ }),
+ ),
+ ),
+ ],
+ ])('uploads all files of mixed types successfully (tx %i)', async (n, document) => {
+ await expectDocumentAfterTransaction({
+ tiptapEditor,
+ number: n,
+ expectedDoc: document(),
+ action: uploadMultipleFiles,
+ });
+ });
+
+ it('cleans up the state if all uploads fail', async () => {
+ await expectDocumentAfterTransaction({
+ tiptapEditor,
+ number: 14,
+ expectedDoc: doc(p(), p(), p(), p(), p(), p(), p()),
+ action: () => {
+ // Set max file size to 1 byte, our file is 3 bytes
+ gon.max_file_size = 1 / 1024 / 1024;
+ uploadMultipleFiles();
+ },
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/content_editor/extensions/drawio_diagram_spec.js b/spec/frontend/content_editor/extensions/drawio_diagram_spec.js
new file mode 100644
index 00000000000..61dc164c99a
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/drawio_diagram_spec.js
@@ -0,0 +1,103 @@
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
+import Image from '~/content_editor/extensions/image';
+import createAssetResolver from '~/content_editor/services/asset_resolver';
+import { create } from '~/drawio/content_editor_facade';
+import { launchDrawioEditor } from '~/drawio/drawio_editor';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+import {
+ PROJECT_WIKI_ATTACHMENT_IMAGE_HTML,
+ PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
+} from '../test_constants';
+
+jest.mock('~/content_editor/services/asset_resolver');
+jest.mock('~/drawio/content_editor_facade');
+jest.mock('~/drawio/drawio_editor');
+
+describe('content_editor/extensions/drawio_diagram', () => {
+ let tiptapEditor;
+ let doc;
+ let paragraph;
+ let image;
+ let drawioDiagram;
+ const uploadsPath = '/uploads';
+ const renderMarkdown = () => {};
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor({
+ extensions: [Image, DrawioDiagram.configure({ uploadsPath, renderMarkdown })],
+ });
+ const { builders } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ image: { nodeType: Image.name },
+ drawioDiagram: { nodeType: DrawioDiagram.name },
+ },
+ });
+
+ doc = builders.doc;
+ paragraph = builders.paragraph;
+ image = builders.image;
+ drawioDiagram = builders.drawioDiagram;
+ });
+
+ describe('parsing', () => {
+ it('distinguishes a drawio diagram from an image', () => {
+ const expectedDocWithDiagram = doc(
+ paragraph(
+ drawioDiagram({
+ alt: 'test-file',
+ canonicalSrc: 'test-file.drawio.svg',
+ src: '/group1/project1/-/wikis/test-file.drawio.svg',
+ }),
+ ),
+ );
+ const expectedDocWithImage = doc(
+ paragraph(
+ image({
+ alt: 'test-file',
+ canonicalSrc: 'test-file.png',
+ src: '/group1/project1/-/wikis/test-file.png',
+ }),
+ ),
+ );
+ tiptapEditor.commands.setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML);
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDocWithDiagram.toJSON());
+
+ tiptapEditor.commands.setContent(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML);
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDocWithImage.toJSON());
+ });
+ });
+
+ describe('createOrEditDiagram command', () => {
+ let editorFacade;
+ let assetResolver;
+
+ beforeEach(() => {
+ editorFacade = {};
+ assetResolver = {};
+ tiptapEditor.commands.createOrEditDiagram();
+
+ create.mockReturnValueOnce(editorFacade);
+ createAssetResolver.mockReturnValueOnce(assetResolver);
+ });
+
+ it('creates a new instance of asset resolver', () => {
+ expect(createAssetResolver).toHaveBeenCalledWith({ renderMarkdown });
+ });
+
+ it('creates a new instance of the content_editor_facade', () => {
+ expect(create).toHaveBeenCalledWith({
+ tiptapEditor,
+ drawioNodeName: DrawioDiagram.name,
+ uploadsPath,
+ assetResolver,
+ });
+ });
+
+ it('calls launchDrawioEditor and provides content_editor_facade', () => {
+ expect(launchDrawioEditor).toHaveBeenCalledWith({ editorFacade });
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
index ead898554d1..3c6f28f0c32 100644
--- a/spec/frontend/content_editor/extensions/link_spec.js
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -31,11 +31,6 @@ describe('content_editor/extensions/link', () => {
${'[link 123](read me.md)'} | ${() => p(link({ href: 'read me.md' }, 'link 123'))}
${'text'} | ${() => p('text')}
${'documentation](readme.md'} | ${() => p('documentation](readme.md')}
- ${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))}
- ${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))}
- ${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))}
- ${'example.com/ab.html '} | ${() => p('example.com/ab.html')}
- ${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
const expectedDoc = doc(insertedNode());
diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
index 30e798e8817..c9997e3c58f 100644
--- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js
+++ b/spec/frontend/content_editor/extensions/paste_markdown_spec.js
@@ -2,8 +2,9 @@ import PasteMarkdown from '~/content_editor/extensions/paste_markdown';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import Diagram from '~/content_editor/extensions/diagram';
import Frontmatter from '~/content_editor/extensions/frontmatter';
+import Heading from '~/content_editor/extensions/heading';
import Bold from '~/content_editor/extensions/bold';
-import { VARIANT_DANGER } from '~/flash';
+import { VARIANT_DANGER } from '~/alert';
import eventHubFactory from '~/helpers/event_hub_factory';
import { ALERT_EVENT } from '~/content_editor/constants';
import waitForPromises from 'helpers/wait_for_promises';
@@ -20,6 +21,7 @@ describe('content_editor/extensions/paste_markdown', () => {
let doc;
let p;
let bold;
+ let heading;
let renderMarkdown;
let eventHub;
const defaultData = { 'text/plain': '**bold text**' };
@@ -36,16 +38,18 @@ describe('content_editor/extensions/paste_markdown', () => {
CodeBlockHighlight,
Diagram,
Frontmatter,
+ Heading,
PasteMarkdown.configure({ renderMarkdown, eventHub }),
],
});
({
- builders: { doc, p, bold },
+ builders: { doc, p, bold, heading },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
+ heading: { nodeType: Heading.name },
},
}));
});
@@ -110,6 +114,52 @@ describe('content_editor/extensions/paste_markdown', () => {
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
+
+ describe('when pasting inline content in an existing paragraph', () => {
+ it('inserts the inline content next to the existing paragraph content', async () => {
+ const expectedDoc = doc(p('Initial text and', bold('bold text')));
+
+ tiptapEditor.commands.setContent('Initial text and ');
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when pasting inline content and there is text selected', () => {
+ it('inserts the block content after the existing paragraph', async () => {
+ const expectedDoc = doc(p('Initial text', bold('bold text')));
+
+ tiptapEditor.commands.setContent('Initial text and ');
+ tiptapEditor.commands.setTextSelection({ from: 13, to: 17 });
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+
+ describe('when pasting block content in an existing paragraph', () => {
+ beforeEach(() => {
+ renderMarkdown.mockReset();
+ renderMarkdown.mockResolvedValueOnce('<h1>Heading</h1><p><strong>bold text</strong></p>');
+ });
+
+ it('inserts the block content after the existing paragraph', async () => {
+ const expectedDoc = doc(
+ p('Initial text and'),
+ heading({ level: 1 }, 'Heading'),
+ p(bold('bold text')),
+ );
+
+ tiptapEditor.commands.setContent('Initial text and ');
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
});
describe('when rendering markdown fails', () => {
diff --git a/spec/frontend/content_editor/markdown_snapshot_spec.js b/spec/frontend/content_editor/markdown_snapshot_spec.js
index fd64003420e..49b466fd7f5 100644
--- a/spec/frontend/content_editor/markdown_snapshot_spec.js
+++ b/spec/frontend/content_editor/markdown_snapshot_spec.js
@@ -42,7 +42,7 @@ describe('markdown example snapshots in ContentEditor', () => {
const expectedProseMirrorJsonExamples = loadExamples(prosemirrorJsonYml);
const exampleNames = Object.keys(markdownExamples);
- beforeAll(async () => {
+ beforeAll(() => {
return renderHtmlAndJsonForAllExamples(markdownExamples).then((examples) => {
actualHtmlAndJsonExamples = examples;
});
@@ -60,7 +60,7 @@ describe('markdown example snapshots in ContentEditor', () => {
if (skipRunningSnapshotWysiwygHtmlTests) {
it.todo(`${exampleNamePrefix} HTML: ${skipRunningSnapshotWysiwygHtmlTests}`);
} else {
- it(`${exampleNamePrefix} HTML`, async () => {
+ it(`${exampleNamePrefix} HTML`, () => {
const expectedHtml = expectedHtmlExamples[name].wysiwyg;
const { html: actualHtml } = actualHtmlAndJsonExamples[name];
@@ -78,7 +78,7 @@ describe('markdown example snapshots in ContentEditor', () => {
if (skipRunningSnapshotProsemirrorJsonTests) {
it.todo(`${exampleNamePrefix} ProseMirror JSON: ${skipRunningSnapshotProsemirrorJsonTests}`);
} else {
- it(`${exampleNamePrefix} ProseMirror JSON`, async () => {
+ it(`${exampleNamePrefix} ProseMirror JSON`, () => {
const expectedJson = expectedProseMirrorJsonExamples[name];
const { json: actualJson } = actualHtmlAndJsonExamples[name];
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index bc43af9bd8b..359e69c083a 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -1349,7 +1349,7 @@ alert("Hello world")
markdown: `
<h1 class="heading-with-class">Header</h1>
`,
- expectedHtml: '<h1>Header</h1>',
+ expectedHtml: '<h1 dir="auto">Header</h1>',
},
{
markdown: `
diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
index 5df901e0f15..bf29d4bdf23 100644
--- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
+++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
@@ -1,4 +1,4 @@
-import { DOMSerializer } from 'prosemirror-model';
+import { DOMSerializer } from '@tiptap/pm/model';
import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
import { createTiptapEditor } from 'jest/content_editor/test_utils';
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index 6175cbdd3d4..5dfe9c06923 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -64,13 +64,13 @@ describe('content_editor/services/content_editor', () => {
});
describe('editable', () => {
- it('returns true when tiptapEditor is editable', async () => {
+ it('returns true when tiptapEditor is editable', () => {
contentEditor.setEditable(true);
expect(contentEditor.editable).toBe(true);
});
- it('returns false when tiptapEditor is readonly', async () => {
+ it('returns false when tiptapEditor is readonly', () => {
contentEditor.setEditable(false);
expect(contentEditor.editable).toBe(false);
diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js
index e1a30819ac8..53cd51b8c5f 100644
--- a/spec/frontend/content_editor/services/create_content_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_content_editor_spec.js
@@ -20,7 +20,7 @@ describe('content_editor/services/create_content_editor', () => {
preserveUnchangedMarkdown: false,
},
};
- editor = createContentEditor({ renderMarkdown, uploadsPath });
+ editor = createContentEditor({ renderMarkdown, uploadsPath, drawioEnabled: true });
});
describe('when preserveUnchangedMarkdown feature is on', () => {
@@ -45,15 +45,15 @@ describe('content_editor/services/create_content_editor', () => {
});
});
- it('sets gl-outline-0! class selector to the tiptapEditor instance', () => {
+ it('sets gl-shadow-none! class selector to the tiptapEditor instance', () => {
expect(editor.tiptapEditor.options.editorProps).toMatchObject({
attributes: {
- class: 'gl-outline-0!',
+ class: 'gl-shadow-none!',
},
});
});
- it('allows providing external content editor extensions', async () => {
+ it('allows providing external content editor extensions', () => {
const labelReference = 'this is a ~group::editor';
const { tiptapExtension, serializer } = createTestContentEditorExtension();
@@ -82,4 +82,14 @@ describe('content_editor/services/create_content_editor', () => {
renderMarkdown,
});
});
+
+ it('provides uploadsPath and renderMarkdown function to DrawioDiagram extension', () => {
+ expect(
+ editor.tiptapEditor.extensionManager.extensions.find((e) => e.name === 'drawioDiagram')
+ .options,
+ ).toMatchObject({
+ uploadsPath,
+ renderMarkdown,
+ });
+ });
});
diff --git a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
index 90d83820c70..a9960918e62 100644
--- a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
+++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
@@ -35,17 +35,15 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
beforeEach(async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- renderMarkdown.mockResolvedValueOnce(
- `<p><strong>${text}</strong></p><pre lang="javascript"></pre><!-- some comment -->`,
- );
+ renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p><!-- some comment -->`);
result = await deserializer.deserialize({
- content: 'content',
+ markdown: '**Bold text**\n<!-- some comment -->',
schema: tiptapEditor.schema,
});
});
- it('transforms HTML returned by render function to a ProseMirror document', async () => {
+ it('transforms HTML returned by render function to a ProseMirror document', () => {
const document = doc(p(bold(text)), comment(' some comment '));
expect(result.document.toJSON()).toEqual(document.toJSON());
@@ -53,12 +51,22 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
});
describe('when the render function returns an empty value', () => {
- it('returns an empty object', async () => {
- const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
+ it('returns an empty prosemirror document', async () => {
+ const deserializer = createMarkdownDeserializer({
+ render: renderMarkdown,
+ schema: tiptapEditor.schema,
+ });
renderMarkdown.mockResolvedValueOnce(null);
- expect(await deserializer.deserialize({ content: 'content' })).toEqual({});
+ const result = await deserializer.deserialize({
+ markdown: '',
+ schema: tiptapEditor.schema,
+ });
+
+ const document = doc(p());
+
+ expect(result.document.toJSON()).toEqual(document.toJSON());
});
});
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 2cd8b8a0d6f..3729b303cc6 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -8,6 +8,7 @@ import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure';
import FigureCaption from '~/content_editor/extensions/figure_caption';
@@ -57,6 +58,7 @@ const {
div,
descriptionItem,
descriptionList,
+ drawioDiagram,
emoji,
footnoteDefinition,
footnoteReference,
@@ -96,6 +98,7 @@ const {
detailsContent: { nodeType: DetailsContent.name },
descriptionItem: { nodeType: DescriptionItem.name },
descriptionList: { nodeType: DescriptionList.name },
+ drawioDiagram: { nodeType: DrawioDiagram.name },
emoji: { markType: Emoji.name },
figure: { nodeType: Figure.name },
figureCaption: { nodeType: FigureCaption.name },
@@ -183,6 +186,19 @@ comment -->
);
});
+ it('correctly renders a comment with markdown in it without adding any slashes', () => {
+ expect(serialize(paragraph('hi'), comment('this is a list\n- a\n- b\n- c'))).toBe(
+ `
+hi
+
+<!--this is a list
+- a
+- b
+- c-->
+ `.trim(),
+ );
+ });
+
it('correctly serializes a line break', () => {
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
});
@@ -265,6 +281,20 @@ comment -->
).toBe('![GitLab][gitlab-url]');
});
+ it.each`
+ src
+ ${'data:image/png;base64,iVBORw0KGgoAAAAN'}
+ ${'blob:https://gitlab.com/1234-5678-9012-3456'}
+ `('omits images with data/blob urls when serializing', ({ src }) => {
+ expect(serialize(paragraph(image({ src, alt: 'image' })))).toBe('');
+ });
+
+ it('does not escape url in an image', () => {
+ expect(
+ serialize(paragraph(image({ src: 'https://example.com/image__1_.png', alt: 'image' }))),
+ ).toBe('![image](https://example.com/image__1_.png)');
+ });
+
it('correctly serializes strikethrough', () => {
expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~');
});
@@ -397,6 +427,12 @@ this is not really json:table but just trying out whether this case works or not
);
});
+ it('correctly serializes a drawio_diagram', () => {
+ expect(
+ serialize(paragraph(drawioDiagram({ src: 'diagram.drawio.svg', alt: 'Draw.io Diagram' }))),
+ ).toBe('![Draw.io Diagram](diagram.drawio.svg)');
+ });
+
it.each`
width | height | outputAttributes
${300} | ${undefined} | ${'width=300'}
@@ -876,6 +912,59 @@ _An elephant at sunset_
);
});
+ it('correctly renders a table with checkboxes', () => {
+ expect(
+ serialize(
+ table(
+ // each table cell must contain at least one paragraph
+ tableRow(
+ tableHeader(paragraph('')),
+ tableHeader(paragraph('Item')),
+ tableHeader(paragraph('Description')),
+ ),
+ tableRow(
+ tableCell(taskList(taskItem(paragraph('')))),
+ tableCell(paragraph('Item 1')),
+ tableCell(paragraph('Description 1')),
+ ),
+ tableRow(
+ tableCell(taskList(taskItem(paragraph('some text')))),
+ tableCell(paragraph('Item 2')),
+ tableCell(paragraph('Description 2')),
+ ),
+ ),
+ ).trim(),
+ ).toBe(
+ `
+<table>
+<tr>
+<th>
+
+</th>
+<th>Item</th>
+<th>Description</th>
+</tr>
+<tr>
+<td>
+
+* [ ] &nbsp;
+</td>
+<td>Item 1</td>
+<td>Description 1</td>
+</tr>
+<tr>
+<td>
+
+* [ ] some text
+</td>
+<td>Item 2</td>
+<td>Description 2</td>
+</tr>
+</table>
+ `.trim(),
+ );
+ });
+
it('correctly serializes a table with line breaks', () => {
expect(
serialize(
@@ -1300,6 +1389,25 @@ paragraph
.run();
};
+ const editNonInclusiveMarkAction = (initialContent) => {
+ tiptapEditor.commands.setContent(initialContent.toJSON());
+ tiptapEditor.commands.selectTextblockEnd();
+
+ let { from } = tiptapEditor.state.selection;
+ tiptapEditor.commands.setTextSelection({
+ from: from - 1,
+ to: from - 1,
+ });
+
+ const sel = tiptapEditor.state.doc.textBetween(from - 1, from, ' ');
+ tiptapEditor.commands.insertContent(`${sel} modified`);
+
+ tiptapEditor.commands.selectTextblockEnd();
+ from = tiptapEditor.state.selection.from;
+
+ tiptapEditor.commands.deleteRange({ from: from - 1, to: from });
+ };
+
it.each`
mark | markdown | modifiedMarkdown | editAction
${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
@@ -1310,8 +1418,8 @@ paragraph
${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
- ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
- ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
+ ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${editNonInclusiveMarkAction}
+ ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${editNonInclusiveMarkAction}
${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
diff --git a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
index 8c1a3831a74..1459988cf8f 100644
--- a/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
+++ b/spec/frontend/content_editor/services/track_input_rules_and_shortcuts_spec.js
@@ -43,7 +43,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
});
describe('when creating a heading using an keyboard shortcut', () => {
- it('sends a tracking event indicating that a heading was created using an input rule', async () => {
+ it('sends a tracking event indicating that a heading was created using an input rule', () => {
const shortcuts = Heading.parent.config.addKeyboardShortcuts.call(Heading);
const [firstShortcut] = Object.keys(shortcuts);
const nodeName = Heading.name;
@@ -68,7 +68,7 @@ describe('content_editor/services/track_input_rules_and_shortcuts', () => {
});
describe('when creating a heading using an input rule', () => {
- it('sends a tracking event indicating that a heading was created using an input rule', async () => {
+ it('sends a tracking event indicating that a heading was created using an input rule', () => {
const nodeName = Heading.name;
triggerNodeInputRule({ tiptapEditor: editor, inputRuleText: '## ' });
expect(trackingSpy).toHaveBeenCalledWith(undefined, INPUT_RULE_TRACKING_ACTION, {
diff --git a/spec/frontend/content_editor/test_constants.js b/spec/frontend/content_editor/test_constants.js
index 45a0e4a8bd1..749f1234de0 100644
--- a/spec/frontend/content_editor/test_constants.js
+++ b/spec/frontend/content_editor/test_constants.js
@@ -4,6 +4,12 @@ export const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27"
</a>
</p>`;
+export const PROJECT_WIKI_ATTACHMENT_IMAGE_SVG_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
+ <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.svg" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.svg">
+ <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.svg" data-canonical-src="test-file.png">
+ </a>
+</p>`;
+
export const PROJECT_WIKI_ATTACHMENT_VIDEO_HTML = `<p data-sourcepos="1:1-1:132" dir="auto">
<span class="media-container video-container">
<video src="/group1/project1/-/wikis/test-file.mp4" controls="true" data-setup="{}" data-title="test-file" width="400" preload="metadata" data-canonical-src="test-file.mp4">
@@ -20,6 +26,12 @@ export const PROJECT_WIKI_ATTACHMENT_AUDIO_HTML = `<p data-sourcepos="3:1-3:74"
</span>
</p>`;
+export const PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
+ <a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.drawio.svg" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.drawio.svg">
+ <img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.drawio.svg" data-canonical-src="test-file.drawio.svg">
+ </a>
+</p>`;
+
export const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
<a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
</p>`;
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 0fa0e65cd26..1f4a367e46c 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -5,6 +5,7 @@ import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { builders, eq } from 'prosemirror-test-builder';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import Audio from '~/content_editor/extensions/audio';
import Blockquote from '~/content_editor/extensions/blockquote';
import Bold from '~/content_editor/extensions/bold';
@@ -17,6 +18,7 @@ import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
import Diagram from '~/content_editor/extensions/diagram';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Emoji from '~/content_editor/extensions/emoji';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
import FootnoteReference from '~/content_editor/extensions/footnote_reference';
@@ -62,6 +64,12 @@ export const emitEditorEvent = ({ tiptapEditor, event, params = {} }) => {
return nextTick();
};
+export const createTransactionWithMeta = (metaKey, metaValue) => {
+ return {
+ getMeta: (key) => (key === metaKey ? metaValue : null),
+ };
+};
+
/**
* Creates an instance of the Tiptap Editor class
* with a minimal configuration for testing purposes.
@@ -204,6 +212,24 @@ export const waitUntilNextDocTransaction = ({ tiptapEditor, action = () => {} })
});
};
+export const expectDocumentAfterTransaction = ({ tiptapEditor, number, expectedDoc, action }) => {
+ return new Promise((resolve) => {
+ let counter = 0;
+ const handleTransaction = async () => {
+ counter += 1;
+ if (counter === number) {
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ tiptapEditor.off('update', handleTransaction);
+ await waitForPromises();
+ resolve();
+ }
+ };
+
+ tiptapEditor.on('update', handleTransaction);
+ action();
+ });
+};
+
export const createTiptapEditor = (extensions = []) =>
createTestEditor({
extensions: [
@@ -218,6 +244,7 @@ export const createTiptapEditor = (extensions = []) =>
DescriptionList,
Details,
DetailsContent,
+ DrawioDiagram,
Diagram,
Emoji,
FootnoteDefinition,
diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
index 2f441f0f747..5cfb4702be7 100644
--- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
+++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
@@ -53,23 +53,23 @@ exports[`Contributors charts should render charts and a RefSelector when loading
Excluding merge commits. Limited to 6,000 commits.
</span>
- <div>
- <glareachart-stub
- annotations=""
- class="gl-mb-5"
- data="[object Object]"
- height="264"
- includelegendavgmax="true"
- legendaveragetext="Avg"
- legendcurrenttext="Current"
- legendlayout="inline"
- legendmaxtext="Max"
- legendmintext="Min"
- option="[object Object]"
- thresholds=""
- width="0"
- />
- </div>
+ <glareachart-stub
+ annotations=""
+ class="gl-mb-5"
+ data="[object Object]"
+ height="264"
+ includelegendavgmax="true"
+ legendaveragetext="Avg"
+ legendcurrenttext="Current"
+ legendlayout="inline"
+ legendmaxtext="Max"
+ legendmintext="Min"
+ legendseriesinfo=""
+ option="[object Object]"
+ responsive=""
+ thresholds=""
+ width="auto"
+ />
<div
class="row"
@@ -91,22 +91,22 @@ exports[`Contributors charts should render charts and a RefSelector when loading
</p>
- <div>
- <glareachart-stub
- annotations=""
- data="[object Object]"
- height="216"
- includelegendavgmax="true"
- legendaveragetext="Avg"
- legendcurrenttext="Current"
- legendlayout="inline"
- legendmaxtext="Max"
- legendmintext="Min"
- option="[object Object]"
- thresholds=""
- width="0"
- />
- </div>
+ <glareachart-stub
+ annotations=""
+ data="[object Object]"
+ height="216"
+ includelegendavgmax="true"
+ legendaveragetext="Avg"
+ legendcurrenttext="Current"
+ legendlayout="inline"
+ legendmaxtext="Max"
+ legendmintext="Min"
+ legendseriesinfo=""
+ option="[object Object]"
+ responsive=""
+ thresholds=""
+ width="auto"
+ />
</div>
</div>
</div>
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index 03b1e977548..f915b834aff 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ContributorsCharts from '~/contributors/components/contributors.vue';
import { createStore } from '~/contributors/stores';
@@ -16,7 +16,6 @@ jest.mock('~/lib/utils/url_utility', () => ({
let wrapper;
let mock;
let store;
-const Component = Vue.extend(ContributorsCharts);
const endpoint = 'contributors/-/graphs';
const branch = 'main';
const chartData = [
@@ -32,7 +31,7 @@ function factory() {
mock.onGet().reply(HTTP_STATUS_OK, chartData);
store = createStore();
- wrapper = mountExtended(Component, {
+ wrapper = mountExtended(ContributorsCharts, {
propsData: {
endpoint,
branch,
@@ -60,7 +59,6 @@ describe('Contributors charts', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
it('should fetch chart data when mounted', () => {
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index b2ebdf2f53c..a15b9ad2978 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Contributors store actions', () => {
describe('fetchChartData', () => {
@@ -38,7 +38,7 @@ describe('Contributors store actions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(
diff --git a/spec/frontend/create_item_dropdown_spec.js b/spec/frontend/create_item_dropdown_spec.js
index aea4bc6017d..df4bfdb4ad0 100644
--- a/spec/frontend/create_item_dropdown_spec.js
+++ b/spec/frontend/create_item_dropdown_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlCreateItemDropdown from 'test_fixtures_static/create_item_dropdown.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import CreateItemDropdown from '~/create_item_dropdown';
const DROPDOWN_ITEM_DATA = [
@@ -42,7 +43,7 @@ describe('CreateItemDropdown', () => {
}
beforeEach(() => {
- loadHTMLFixture('static/create_item_dropdown.html');
+ setHTMLFixture(htmlCreateItemDropdown);
$wrapperEl = $('.js-create-item-dropdown-fixture-root');
});
diff --git a/spec/frontend/crm/contact_form_wrapper_spec.js b/spec/frontend/crm/contact_form_wrapper_spec.js
index 50b432943fb..2fb6940a415 100644
--- a/spec/frontend/crm/contact_form_wrapper_spec.js
+++ b/spec/frontend/crm/contact_form_wrapper_spec.js
@@ -47,7 +47,6 @@ describe('Customer relations contact form wrapper', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js
index ec7172434bf..63b64a6c984 100644
--- a/spec/frontend/crm/contacts_root_spec.js
+++ b/spec/frontend/crm/contacts_root_spec.js
@@ -61,7 +61,6 @@ describe('Customer relations contacts root app', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
router = null;
});
diff --git a/spec/frontend/crm/crm_form_spec.js b/spec/frontend/crm/crm_form_spec.js
index eabcf5b1b1b..fabf43ceb9d 100644
--- a/spec/frontend/crm/crm_form_spec.js
+++ b/spec/frontend/crm/crm_form_spec.js
@@ -188,10 +188,6 @@ describe('Reusable form component', () => {
};
const asTestParams = (...keys) => keys.map((name) => [name, forms[name]]);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each(asTestParams(FORM_CREATE_CONTACT, FORM_UPDATE_CONTACT))(
'%s form save button',
(name, { mountFunction }) => {
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index d795c585622..8408c1920a9 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -40,10 +40,6 @@ describe('Customer relations organization form wrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('in edit mode', () => {
it('should render organization form with correct props', () => {
mountComponent({ isEditMode: true });
diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js
index 1fcf6aa8f50..0b26a49a6b3 100644
--- a/spec/frontend/crm/organizations_root_spec.js
+++ b/spec/frontend/crm/organizations_root_spec.js
@@ -65,7 +65,6 @@ describe('Customer relations organizations root app', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
router = null;
});
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
index 7d9ae548c9a..d3cdd0d16ef 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
@@ -42,7 +42,6 @@ describe('custom metrics form fields component', () => {
});
afterEach(() => {
- wrapper.destroy();
mockAxios.restore();
});
@@ -174,7 +173,7 @@ describe('custom metrics form fields component', () => {
return axios.waitForAll();
});
- it('shows invalid query message', async () => {
+ it('shows invalid query message', () => {
expect(wrapper.text()).toContain(errorMessage);
});
});
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
index af56b94f90b..c633583f2cb 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_spec.js
@@ -26,10 +26,6 @@ describe('CustomMetricsForm', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Computed', () => {
it('Form button and title text indicate the custom metric is being edited', () => {
mountComponent({ metricPersisted: true });
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
index 113e0d8f60d..1cd16e39417 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
@@ -46,14 +46,9 @@ describe('Deploy freeze modal', () => {
wrapper.findComponent(TimezoneDropdown).trigger('input');
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Basic interactions', () => {
it('button is disabled when freeze period is invalid', () => {
- expect(submitDeployFreezeButton().attributes('disabled')).toBe('true');
+ expect(submitDeployFreezeButton().attributes('disabled')).toBeDefined();
});
});
@@ -93,7 +88,7 @@ describe('Deploy freeze modal', () => {
});
it('disables the add deploy freeze button', () => {
- expect(submitDeployFreezeButton().attributes('disabled')).toBe('true');
+ expect(submitDeployFreezeButton().attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
index 27d8fea9d5e..883cc6a344a 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
@@ -24,11 +24,6 @@ describe('Deploy freeze settings', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Deploy freeze table contains components', () => {
it('contains deploy freeze table', () => {
expect(wrapper.findComponent(DeployFreezeTable).exists()).toBe(true);
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
index c2d6eb399bc..6a9e482a184 100644
--- a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
@@ -37,11 +37,6 @@ describe('Deploy freeze table', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('dispatches fetchFreezePeriods when mounted', () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchFreezePeriods');
});
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
index 9b96ce5d252..d39577baa59 100644
--- a/spec/frontend/deploy_freeze/store/actions_spec.js
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -4,14 +4,14 @@ import Api from '~/api';
import * as actions from '~/deploy_freeze/store/actions';
import * as types from '~/deploy_freeze/store/mutation_types';
import getInitialState from '~/deploy_freeze/store/state';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { freezePeriodsFixture } from '../helpers';
import { timezoneDataFixture } from '../../vue_shared/components/timezone_dropdown/helpers';
jest.mock('~/api.js');
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('deploy freeze store actions', () => {
const freezePeriodFixture = freezePeriodsFixture[0];
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index d11ecf95de6..3dfb828b449 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -33,7 +33,6 @@ describe('Deploy keys app component', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index 8599c55c908..3c4fa2a6de6 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -1,9 +1,10 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import data from 'test_fixtures/deploy_keys/keys.json';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import key from '~/deploy_keys/components/key.vue';
import DeployKeysStore from '~/deploy_keys/store';
-import { getTimeago } from '~/lib/utils/datetime_utility';
+import { getTimeago, formatDate } from '~/lib/utils/datetime_utility';
describe('Deploy keys key', () => {
let wrapper;
@@ -18,6 +19,9 @@ describe('Deploy keys key', () => {
endpoint: 'https://test.host/dummy/endpoint',
...propsData,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
@@ -26,11 +30,6 @@ describe('Deploy keys key', () => {
store.keys = data;
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('enabled key', () => {
const deployKey = data.enabled_keys[0];
@@ -48,6 +47,33 @@ describe('Deploy keys key', () => {
);
});
+ it('renders human friendly expiration date', () => {
+ const expiresAt = new Date();
+ createComponent({
+ deployKey: { ...deployKey, expires_at: expiresAt },
+ });
+
+ expect(findTextAndTrim('.key-expires-at')).toBe(`${getTimeago().format(expiresAt)}`);
+ });
+ it('shows tooltip for expiration date', () => {
+ const expiresAt = new Date();
+ createComponent({
+ deployKey: { ...deployKey, expires_at: expiresAt },
+ });
+
+ const expiryComponent = wrapper.find('[data-testid="expires-at-tooltip"]');
+ const tooltip = getBinding(expiryComponent.element, 'gl-tooltip');
+ expect(tooltip).toBeDefined();
+ expect(expiryComponent.attributes('title')).toBe(`${formatDate(expiresAt)}`);
+ });
+ it('renders never when no expiration date', () => {
+ createComponent({
+ deployKey: { ...deployKey, expires_at: null },
+ });
+
+ expect(wrapper.find('[data-testid="expires-never"]').exists()).toBe(true);
+ });
+
it('shows pencil button for editing', () => {
createComponent({ deployKey });
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
index f5f76d5d493..e0f86aadad4 100644
--- a/spec/frontend/deploy_keys/components/keys_panel_spec.js
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -23,11 +23,6 @@ describe('Deploy keys panel', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders list of keys', () => {
mountComponent();
expect(wrapper.findAll('.deploy-key').length).toBe(wrapper.vm.keys.length);
diff --git a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
index 46f7b2f3604..a3fdab88270 100644
--- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -7,20 +7,12 @@ import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/h
import { TEST_HOST } from 'helpers/test_constants';
import NewDeployToken from '~/deploy_tokens/components/new_deploy_token.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
const createNewTokenPath = `${TEST_HOST}/create`;
const deployTokensHelpUrl = `${TEST_HOST}/help`;
-jest.mock('~/flash', () => {
- const original = jest.requireActual('~/flash');
-
- return {
- __esModule: true,
- ...original,
- createAlert: jest.fn(),
- };
-});
+jest.mock('~/alert');
describe('New Deploy Token', () => {
let wrapper;
@@ -43,13 +35,12 @@ describe('New Deploy Token', () => {
createNewTokenPath,
tokenType,
},
+ stubs: {
+ GlFormCheckbox,
+ },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without a container registry', () => {
beforeEach(() => {
wrapper = factory({ containerRegistryEnabled: false });
@@ -69,7 +60,7 @@ describe('New Deploy Token', () => {
it('should show the read registry scope', () => {
const checkbox = wrapper.findAllComponents(GlFormCheckbox).at(1);
- expect(checkbox.text()).toBe('read_registry');
+ expect(checkbox.text()).toContain('read_registry');
});
function submitTokenThenCheck() {
@@ -91,7 +82,7 @@ describe('New Deploy Token', () => {
});
}
- it('should flash error message if token creation fails', async () => {
+ it('should alert error message if token creation fails', async () => {
const mockAxios = new MockAdapter(axios);
const date = new Date();
@@ -222,4 +213,32 @@ describe('New Deploy Token', () => {
return submitTokenThenCheck();
});
});
+
+ describe('help text for write_package_registry scope', () => {
+ const findWriteRegistryScopeCheckbox = () => wrapper.findAllComponents(GlFormCheckbox).at(4);
+
+ describe('with project tokenType', () => {
+ beforeEach(() => {
+ wrapper = factory();
+ });
+
+ it('should show the correct help text', () => {
+ expect(findWriteRegistryScopeCheckbox().text()).toContain(
+ 'Allows read, write and delete access to the package registry.',
+ );
+ });
+ });
+
+ describe('with group tokenType', () => {
+ beforeEach(() => {
+ wrapper = factory({ tokenType: 'group' });
+ });
+
+ it('should show the correct help text', () => {
+ expect(findWriteRegistryScopeCheckbox().text()).toContain(
+ 'Allows read and write access to the package registry.',
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/deploy_tokens/components/revoke_button_spec.js b/spec/frontend/deploy_tokens/components/revoke_button_spec.js
index fa2a7d9b155..6e81205d1c1 100644
--- a/spec/frontend/deploy_tokens/components/revoke_button_spec.js
+++ b/spec/frontend/deploy_tokens/components/revoke_button_spec.js
@@ -52,10 +52,6 @@ describe('RevokeButton', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findRevokeButton = () => wrapper.findByTestId('revoke-button');
const findModal = () => wrapper.findComponent(GlModal);
const findPrimaryModalButton = () => wrapper.findByTestId('primary-revoke-btn');
diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js
index 439c20e0fb5..44279ec7915 100644
--- a/spec/frontend/deprecated_jquery_dropdown_spec.js
+++ b/spec/frontend/deprecated_jquery_dropdown_spec.js
@@ -1,8 +1,9 @@
/* eslint-disable no-param-reassign */
import $ from 'jquery';
+import htmlDeprecatedJqueryDropdown from 'test_fixtures_static/deprecated_jquery_dropdown.html';
import mockProjects from 'test_fixtures_static/projects.json';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
@@ -65,7 +66,7 @@ describe('deprecatedJQueryDropdown', () => {
}
beforeEach(() => {
- loadHTMLFixture('static/deprecated_jquery_dropdown.html');
+ setHTMLFixture(htmlDeprecatedJqueryDropdown);
test.dropdownContainerElement = $('.dropdown.inline');
test.$dropdownMenuElement = $('.dropdown-menu', test.dropdownContainerElement);
test.projectsData = JSON.parse(JSON.stringify(mockProjects));
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
index 426a61f5a47..cacda9a475e 100644
--- a/spec/frontend/design_management/components/delete_button_spec.js
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -21,10 +21,6 @@ describe('Batch delete button component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders non-disabled button by default', () => {
createComponent();
@@ -34,7 +30,7 @@ describe('Batch delete button component', () => {
it('renders disabled button when design is deleting', () => {
createComponent({ isDeleting: true });
- expect(findButton().attributes('disabled')).toBe('true');
+ expect(findButton().attributes('disabled')).toBeDefined();
});
it('emits `delete-selected-designs` event on modal ok click', async () => {
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
index 402e55347af..3b407d11041 100644
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
@@ -58,6 +58,7 @@ exports[`Design note component should match the snapshot 1`] = `
>
<time-ago-tooltip-stub
cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
time="2019-07-26T15:02:20Z"
tooltipplacement="bottom"
/>
@@ -70,6 +71,8 @@ exports[`Design note component should match the snapshot 1`] = `
>
<!---->
+
+ <!---->
</div>
</div>
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 2091e1e08dd..a6ab147884f 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -1,18 +1,22 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignNoteSignedOut from '~/design_management/components/design_notes/design_note_signed_out.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
-import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import destroyNoteMutation from '~/design_management/graphql/mutations/destroy_note.mutation.graphql';
+import { DELETE_NOTE_ERROR_MSG } from '~/design_management/constants';
import mockDiscussion from '../../mock_data/discussion';
import notes from '../../mock_data/notes';
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
const defaultMockDiscussion = {
id: '0',
resolved: false,
@@ -23,7 +27,6 @@ const defaultMockDiscussion = {
const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
- const originalGon = window.gon;
let wrapper;
const findDesignNotes = () => wrapper.findAllComponents(DesignNote);
@@ -34,18 +37,7 @@ describe('Design discussions component', () => {
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
- const findApolloMutation = () => wrapper.findComponent(ApolloMutation);
- const mutationVariables = {
- mutation: createNoteMutation,
- variables: {
- input: {
- noteableId: 'noteable-id',
- body: 'test',
- discussionId: '0',
- },
- },
- };
const registerPath = '/users/sign_up?redirect_to_referer=yes';
const signInPath = '/users/sign_in?redirect_to_referer=yes';
const mutate = jest.fn().mockResolvedValue({ data: { createNote: { errors: [] } } });
@@ -59,7 +51,7 @@ describe('Design discussions component', () => {
provider: { clients: { defaultClient: { readQuery } } },
};
- function createComponent(props = {}, data = {}) {
+ function createComponent({ props = {}, data = {}, apolloConfig = {} } = {}) {
wrapper = mount(DesignDiscussion, {
propsData: {
resolvedDiscussionsExpanded: true,
@@ -82,7 +74,10 @@ describe('Design discussions component', () => {
issueIid: '1',
},
mocks: {
- $apollo,
+ $apollo: {
+ ...$apollo,
+ ...apolloConfig,
+ },
$route: {
hash: '#note_1',
params: {
@@ -101,16 +96,17 @@ describe('Design discussions component', () => {
});
afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
+ confirmAction.mockReset();
});
describe('when discussion is not resolvable', () => {
beforeEach(() => {
createComponent({
- discussion: {
- ...defaultMockDiscussion,
- resolvable: false,
+ props: {
+ discussion: {
+ ...defaultMockDiscussion,
+ resolvable: false,
+ },
},
});
});
@@ -171,11 +167,13 @@ describe('Design discussions component', () => {
innerText: DEFAULT_TODO_COUNT,
});
createComponent({
- discussion: {
- ...defaultMockDiscussion,
- resolved: true,
- resolvedBy: notes[0].author,
- resolvedAt: '2020-05-08T07:10:45Z',
+ props: {
+ discussion: {
+ ...defaultMockDiscussion,
+ resolved: true,
+ resolvedBy: notes[0].author,
+ resolvedAt: '2020-05-08T07:10:45Z',
+ },
},
});
});
@@ -206,10 +204,10 @@ describe('Design discussions component', () => {
});
it('emit todo:toggle when discussion is resolved', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { isFormRendered: true },
+ });
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
@@ -261,32 +259,28 @@ describe('Design discussions component', () => {
expect(findReplyForm().exists()).toBe(true);
});
- it('calls mutation on submitting form and closes the form', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ it('closes the form when note submit mutation is completed', async () => {
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { isFormRendered: true },
+ });
- findReplyForm().vm.$emit('submit-form');
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ findReplyForm().vm.$emit('note-submit-complete', { data: { createNote: {} } });
- await mutate();
await nextTick();
expect(findReplyForm().exists()).toBe(false);
});
it('clears the discussion comment on closing comment form', async () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { isFormRendered: true },
+ });
await nextTick();
findReplyForm().vm.$emit('cancel-form');
- expect(wrapper.vm.discussionComment).toBe('');
-
await nextTick();
expect(findReplyForm().exists()).toBe(false);
});
@@ -295,15 +289,15 @@ describe('Design discussions component', () => {
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
'applies correct class to all notes in the active discussion',
(note) => {
- createComponent(
- { discussion: mockDiscussion },
- {
+ createComponent({
+ props: { discussion: mockDiscussion },
+ data: {
activeDiscussion: {
id: note.id,
source: 'pin',
},
},
- );
+ });
expect(
wrapper
@@ -329,10 +323,10 @@ describe('Design discussions component', () => {
});
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
- createComponent(
- { discussionWithOpenForm: defaultMockDiscussion.id },
- { discussionComment: 'test', isFormRendered: true },
- );
+ createComponent({
+ props: { discussionWithOpenForm: defaultMockDiscussion.id },
+ data: { isFormRendered: true },
+ });
findResolveButton().trigger('click');
findReplyForm().vm.$emit('submitForm');
@@ -359,15 +353,15 @@ describe('Design discussions component', () => {
beforeEach(() => {
window.gon = { current_user_id: null };
- createComponent(
- {
+ createComponent({
+ props: {
discussion: {
...defaultMockDiscussion,
},
discussionWithOpenForm: defaultMockDiscussion.id,
},
- { discussionComment: 'test', isFormRendered: true },
- );
+ data: { isFormRendered: true },
+ });
});
it('does not render resolve discussion button', () => {
@@ -378,10 +372,6 @@ describe('Design discussions component', () => {
expect(findReplyPlaceholder().exists()).toBe(false);
});
- it('does not render apollo-mutation component', () => {
- expect(findApolloMutation().exists()).toBe(false);
- });
-
it('renders design-note-signed-out component', () => {
expect(findDesignNoteSignedOut().exists()).toBe(true);
expect(findDesignNoteSignedOut().props()).toMatchObject({
@@ -390,4 +380,64 @@ describe('Design discussions component', () => {
});
});
});
+
+ it('should open confirmation modal when the note emits `delete-note` event', () => {
+ createComponent();
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: '1' });
+ expect(confirmAction).toHaveBeenCalled();
+ });
+
+ describe('when confirmation modal is opened', () => {
+ const noteId = 'note-test-id';
+
+ it('sends the mutation with correct variables', async () => {
+ confirmAction.mockResolvedValueOnce(true);
+ const destroyNoteMutationSuccess = jest.fn().mockResolvedValue({
+ data: { destroyNote: { note: null, __typename: 'DestroyNote', errors: [] } },
+ });
+ createComponent({ apolloConfig: { mutate: destroyNoteMutationSuccess } });
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: noteId });
+
+ expect(confirmAction).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(destroyNoteMutationSuccess).toHaveBeenCalledWith({
+ update: expect.any(Function),
+ mutation: destroyNoteMutation,
+ variables: {
+ input: {
+ id: noteId,
+ },
+ },
+ optimisticResponse: {
+ destroyNote: {
+ note: null,
+ errors: [],
+ __typename: 'DestroyNotePayload',
+ },
+ },
+ });
+ });
+
+ it('emits `delete-note-error` event if GraphQL mutation fails', async () => {
+ confirmAction.mockResolvedValueOnce(true);
+ const destroyNoteMutationError = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ createComponent({ apolloConfig: { mutate: destroyNoteMutationError } });
+
+ findDesignNotes().at(0).vm.$emit('delete-note', { id: noteId });
+
+ await waitForPromises();
+
+ expect(destroyNoteMutationError).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted()).toEqual({
+ 'delete-note-error': [[DELETE_NOTE_ERROR_MSG]],
+ });
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
index e71bb5ab520..95b08b89809 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_signed_out_spec.js
@@ -18,10 +18,6 @@ function createComponent(isAddDiscussion = false) {
describe('DesignNoteSignedOut', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders message containing register and sign-in links while user wants to reply to a discussion', () => {
wrapper = createComponent();
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index df511586c10..6f5b282fa3b 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -1,6 +1,6 @@
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
-import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { GlAvatar, GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
@@ -38,6 +38,8 @@ describe('Design note component', () => {
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
const findEditButton = () => wrapper.findByTestId('note-edit');
const findNoteContent = () => wrapper.findByTestId('note-text');
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-button"]');
function createComponent(props = {}, data = { isEditing: false }) {
wrapper = shallowMountExtended(DesignNote, {
@@ -63,10 +65,6 @@ describe('Design note component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should match the snapshot', () => {
createComponent({
note,
@@ -112,6 +110,14 @@ describe('Design note component', () => {
expect(findEditButton().exists()).toBe(false);
});
+ it('should not display a dropdown if user does not have a permission to delete note', () => {
+ createComponent({
+ note,
+ });
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
describe('when user has a permission to edit note', () => {
it('should open an edit form on edit button click', async () => {
createComponent({
@@ -158,15 +164,47 @@ describe('Design note component', () => {
expect(findNoteContent().exists()).toBe(true);
});
- it('calls a mutation on submit-form event and hides a form', async () => {
- findReplyForm().vm.$emit('submit-form');
- expect(mutate).toHaveBeenCalled();
+ it('hides a form after update mutation is completed', async () => {
+ findReplyForm().vm.$emit('note-submit-complete', { data: { updateNote: { errors: [] } } });
- await mutate();
await nextTick();
expect(findReplyForm().exists()).toBe(false);
expect(findNoteContent().exists()).toBe(true);
});
});
});
+
+ describe('when user has a permission to delete note', () => {
+ it('should display a dropdown', () => {
+ createComponent({
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+ });
+
+ it('should emit `delete-note` event with proper payload when delete note button is clicked', () => {
+ const payload = {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ };
+
+ createComponent({
+ note: {
+ ...payload,
+ },
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
+ });
});
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index f4d4f9cf896..f08efc0c685 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -1,46 +1,95 @@
+import { GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import Autosave from '~/autosave';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import createNoteMutation from '~/design_management/graphql/mutations/create_note.mutation.graphql';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+import {
+ ADD_DISCUSSION_COMMENT_ERROR,
+ ADD_IMAGE_DIFF_NOTE_ERROR,
+ UPDATE_IMAGE_DIFF_NOTE_ERROR,
+ UPDATE_NOTE_ERROR,
+} from '~/design_management/utils/error_messages';
+import {
+ mockNoteSubmitSuccessMutationResponse,
+ mockNoteSubmitFailureMutationResponse,
+} from '../../mock_data/apollo_mock';
+
+Vue.use(VueApollo);
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/autosave');
describe('Design reply form component', () => {
let wrapper;
- let originalGon;
+ let mockApollo;
const findTextarea = () => wrapper.find('textarea');
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' });
-
- function createComponent(props = {}, mountOptions = {}) {
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const mockNoteableId = 'gid://gitlab/DesignManagement::Design/6';
+ const mockComment = 'New comment';
+ const mockDiscussionId = 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8';
+ const createNoteMutationData = {
+ input: {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ body: mockComment,
+ },
+ };
+
+ const ctrlKey = {
+ ctrlKey: true,
+ };
+ const metaKey = {
+ metaKey: true,
+ };
+ const mockMutationHandler = jest.fn().mockResolvedValue(mockNoteSubmitSuccessMutationResponse);
+
+ function createComponent({
+ props = {},
+ mountOptions = {},
+ data = {},
+ mutationHandler = mockMutationHandler,
+ } = {}) {
+ mockApollo = createMockApollo([[createNoteMutation, mutationHandler]]);
wrapper = mount(DesignReplyForm, {
propsData: {
+ designNoteMutation: createNoteMutation,
+ noteableId: mockNoteableId,
+ markdownDocsPath: 'path/to/markdown/docs',
+ markdownPreviewPath: 'path/to/markdown/preview',
value: '',
- isSaving: false,
- noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props,
},
...mountOptions,
+ apolloProvider: mockApollo,
+ data() {
+ return {
+ ...data,
+ };
+ },
});
}
beforeEach(() => {
- originalGon = window.gon;
window.gon.current_user_id = 1;
});
afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
+ mockApollo = null;
confirmAction.mockReset();
});
it('textarea has focus after component mount', () => {
// We need to attach to document, so that `document.activeElement` is properly set in jsdom
- createComponent({}, { attachTo: document.body });
+ createComponent({ mountOptions: { attachTo: document.body } });
expect(findTextarea().element).toEqual(document.activeElement);
});
@@ -64,7 +113,7 @@ describe('Design reply form component', () => {
});
it('renders button text as "Save comment" when creating a comment', () => {
- createComponent({ isNewComment: false });
+ createComponent({ props: { isNewComment: false } });
expect(findSubmitButton().html()).toMatchSnapshot();
});
@@ -75,9 +124,8 @@ describe('Design reply form component', () => {
${'gid://gitlab/DiffDiscussion/123'} | ${123}
`(
'initializes autosave support on discussion with proper key',
- async ({ discussionId, shortDiscussionId }) => {
- createComponent({ discussionId });
- await nextTick();
+ ({ discussionId, shortDiscussionId }) => {
+ createComponent({ props: { discussionId } });
expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [
'Discussion',
@@ -89,31 +137,21 @@ describe('Design reply form component', () => {
describe('when form has no text', () => {
beforeEach(() => {
- createComponent({
- value: '',
- });
+ createComponent();
});
it('submit button is disabled', () => {
expect(findSubmitButton().attributes().disabled).toBe('disabled');
});
- it('does not emit submitForm event on textarea ctrl+enter keydown', async () => {
- findTextarea().trigger('keydown.enter', {
- ctrlKey: true,
- });
-
- await nextTick();
- expect(wrapper.emitted('submit-form')).toBeUndefined();
- });
-
- it('does not emit submitForm event on textarea meta+enter keydown', async () => {
- findTextarea().trigger('keydown.enter', {
- metaKey: true,
- });
+ it.each`
+ key | keyData
+ ${'ctrl'} | ${ctrlKey}
+ ${'meta'} | ${metaKey}
+ `('does not perform mutation on textarea $key+enter keydown', ({ keyData }) => {
+ findTextarea().trigger('keydown.enter', keyData);
- await nextTick();
- expect(wrapper.emitted('submit-form')).toBeUndefined();
+ expect(mockMutationHandler).not.toHaveBeenCalled();
});
it('emits cancelForm event on pressing escape button on textarea', () => {
@@ -129,118 +167,150 @@ describe('Design reply form component', () => {
});
});
- describe('when form has text', () => {
- beforeEach(() => {
- createComponent({
- value: 'test',
- });
- });
-
+ describe('when the form has text', () => {
it('submit button is enabled', () => {
+ createComponent({ props: { value: mockComment } });
expect(findSubmitButton().attributes().disabled).toBeUndefined();
});
- it('emits submitForm event on Comment button click', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ it('calls a mutation on submit button click event', async () => {
+ const mockMutationVariables = {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ };
- findSubmitButton().vm.$emit('click');
+ createComponent({
+ props: {
+ mutationVariables: mockMutationVariables,
+ value: mockComment,
+ },
+ });
- await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
- });
+ findSubmitButton().vm.$emit('click');
- it('emits submitForm event on textarea ctrl+enter keydown', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ expect(mockMutationHandler).toHaveBeenCalledWith(createNoteMutationData);
- findTextarea().trigger('keydown.enter', {
- ctrlKey: true,
- });
+ await waitForPromises();
- await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
+ expect(wrapper.emitted('note-submit-complete')).toEqual([
+ [mockNoteSubmitSuccessMutationResponse],
+ ]);
});
- it('emits submitForm event on textarea meta+enter keydown', async () => {
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ it.each`
+ key | keyData
+ ${'ctrl'} | ${ctrlKey}
+ ${'meta'} | ${metaKey}
+ `('does perform mutation on textarea $key+enter keydown', async ({ keyData }) => {
+ const mockMutationVariables = {
+ noteableId: mockNoteableId,
+ discussionId: mockDiscussionId,
+ };
- findTextarea().trigger('keydown.enter', {
- metaKey: true,
+ createComponent({
+ props: {
+ mutationVariables: mockMutationVariables,
+ value: mockComment,
+ },
});
- await nextTick();
- expect(wrapper.emitted('submit-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
- });
+ findTextarea().trigger('keydown.enter', keyData);
- it('emits input event on changing textarea content', async () => {
- findTextarea().setValue('test2');
+ expect(mockMutationHandler).toHaveBeenCalledWith(createNoteMutationData);
- await nextTick();
- expect(wrapper.emitted('input')).toEqual([['test2']]);
+ await waitForPromises();
+ expect(wrapper.emitted('note-submit-complete')).toEqual([
+ [mockNoteSubmitSuccessMutationResponse],
+ ]);
});
- it('emits cancelForm event on Escape key if text was not changed', () => {
- findTextarea().trigger('keyup.esc');
+ it('shows error message when mutation fails', async () => {
+ const failedMutation = jest.fn().mockRejectedValue(mockNoteSubmitFailureMutationResponse);
+ createComponent({
+ props: {
+ designNoteMutation: createNoteMutation,
+ value: mockComment,
+ },
+ mutationHandler: failedMutation,
+ data: {
+ errorMessage: 'error',
+ },
+ });
- expect(wrapper.emitted('cancel-form')).toHaveLength(1);
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+ expect(findAlert().exists()).toBe(true);
});
- it('opens confirmation modal on Escape key when text has changed', async () => {
- wrapper.setProps({ value: 'test2' });
+ it.each`
+ isDiscussion | isNewComment | errorMessage
+ ${true} | ${true} | ${ADD_IMAGE_DIFF_NOTE_ERROR}
+ ${true} | ${false} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR}
+ ${false} | ${true} | ${ADD_DISCUSSION_COMMENT_ERROR}
+ ${false} | ${false} | ${UPDATE_NOTE_ERROR}
+ `(
+ 'return proper error message on error in case of isDiscussion is $isDiscussion and isNewComment is $isNewComment',
+ ({ isDiscussion, isNewComment, errorMessage }) => {
+ createComponent({ props: { isDiscussion, isNewComment } });
+
+ expect(wrapper.vm.getErrorMessage()).toBe(errorMessage);
+ },
+ );
- await nextTick();
- findTextarea().trigger('keyup.esc');
- expect(confirmAction).toHaveBeenCalled();
- });
+ it('emits cancelForm event on Escape key if text was not changed', () => {
+ createComponent();
- it('emits cancelForm event on Cancel button click if text was not changed', () => {
- findCancelButton().trigger('click');
+ findTextarea().trigger('keyup.esc');
expect(wrapper.emitted('cancel-form')).toHaveLength(1);
});
- it('opens confirmation modal on Cancel button click when text has changed', async () => {
- wrapper.setProps({ value: 'test2' });
+ it('opens confirmation modal on Escape key when text has changed', () => {
+ createComponent();
+
+ findTextarea().setValue(mockComment);
+
+ findTextarea().trigger('keyup.esc');
- await nextTick();
- findCancelButton().trigger('click');
expect(confirmAction).toHaveBeenCalled();
});
it('emits cancelForm event when confirmed', async () => {
confirmAction.mockResolvedValueOnce(true);
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
- wrapper.setProps({ value: 'test3' });
- await nextTick();
+ createComponent({ props: { value: mockComment } });
+ findTextarea().setValue('Comment changed');
findTextarea().trigger('keyup.esc');
- await nextTick();
expect(confirmAction).toHaveBeenCalled();
- await nextTick();
+ await waitForPromises();
expect(wrapper.emitted('cancel-form')).toHaveLength(1);
- expect(autosaveResetSpy).toHaveBeenCalled();
});
- it("doesn't emit cancelForm event when not confirmed", async () => {
+ it('does not emit cancelForm event when not confirmed', async () => {
confirmAction.mockResolvedValueOnce(false);
- const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
- wrapper.setProps({ value: 'test3' });
- await nextTick();
+ createComponent({ props: { value: mockComment } });
+ findTextarea().setValue('Comment changed');
findTextarea().trigger('keyup.esc');
- await nextTick();
expect(confirmAction).toHaveBeenCalled();
- await nextTick();
+ await waitForPromises();
expect(wrapper.emitted('cancel-form')).toBeUndefined();
- expect(autosaveResetSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when component is destroyed', () => {
+ it('calls autosave.reset', async () => {
+ const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
+ createComponent();
+ await wrapper.destroy();
+ expect(autosaveResetSpy).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
index 41129e2b58d..eaa5a620fa6 100644
--- a/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
+++ b/spec/frontend/design_management/components/design_notes/toggle_replies_widget_spec.js
@@ -23,10 +23,6 @@ describe('Toggle replies widget component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when replies are collapsed', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 2807fe7727f..3eb47fdb97e 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -1,6 +1,6 @@
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
@@ -16,22 +16,20 @@ describe('Design overlay component', () => {
const mockDimensions = { width: 100, height: 100 };
- const findOverlay = () => wrapper.find('[data-testid="design-overlay"]');
- const findAllNotes = () => wrapper.findAll('[data-testid="note-pin"]');
- const findCommentBadge = () => wrapper.find('[data-testid="comment-badge"]');
+ const findOverlay = () => wrapper.findByTestId('design-overlay');
+ const findAllNotes = () => wrapper.findAllByTestId('note-pin');
+ const findCommentBadge = () => wrapper.findByTestId('comment-badge');
const findBadgeAtIndex = (noteIndex) => findAllNotes().at(noteIndex);
const findFirstBadge = () => findBadgeAtIndex(0);
const findSecondBadge = () => findBadgeAtIndex(1);
- const clickAndDragBadge = async (elem, fromPoint, toPoint) => {
+ const clickAndDragBadge = (elem, fromPoint, toPoint) => {
elem.vm.$emit(
'mousedown',
new MouseEvent('click', { clientX: fromPoint.x, clientY: fromPoint.y }),
);
findOverlay().trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
- await nextTick();
elem.vm.$emit('mouseup', new MouseEvent('click', { clientX: toPoint.x, clientY: toPoint.y }));
- await nextTick();
};
function createComponent(props = {}, data = {}) {
@@ -47,7 +45,7 @@ describe('Design overlay component', () => {
},
});
- wrapper = shallowMount(DesignOverlay, {
+ wrapper = shallowMountExtended(DesignOverlay, {
apolloProvider,
propsData: {
dimensions: mockDimensions,
@@ -80,7 +78,7 @@ describe('Design overlay component', () => {
expect(wrapper.attributes().style).toBe('width: 100px; height: 100px; top: 0px; left: 0px;');
});
- it('should emit `openCommentForm` when clicking on overlay', async () => {
+ it('should emit `openCommentForm` when clicking on overlay', () => {
createComponent();
const newCoordinates = {
x: 10,
@@ -90,7 +88,7 @@ describe('Design overlay component', () => {
wrapper
.find('[data-qa-selector="design_image_button"]')
.trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
- await nextTick();
+
expect(wrapper.emitted('openCommentForm')).toEqual([
[{ x: newCoordinates.x, y: newCoordinates.y }],
]);
@@ -175,25 +173,15 @@ describe('Design overlay component', () => {
});
});
- it('should recalculate badges positions on window resize', async () => {
+ it('should calculate badges positions based on dimensions', () => {
createComponent({
notes,
dimensions: {
- width: 400,
- height: 400,
- },
- });
-
- expect(findFirstBadge().props('position')).toEqual({ left: '40px', top: '60px' });
-
- wrapper.setProps({
- dimensions: {
width: 200,
height: 200,
},
});
- await nextTick();
expect(findFirstBadge().props('position')).toEqual({ left: '20px', top: '30px' });
});
@@ -216,7 +204,6 @@ describe('Design overlay component', () => {
new MouseEvent('click', { clientX: position.x, clientY: position.y }),
);
- await nextTick();
findFirstBadge().vm.$emit(
'mouseup',
new MouseEvent('click', { clientX: position.x, clientY: position.y }),
@@ -290,7 +277,7 @@ describe('Design overlay component', () => {
});
describe('when moving the comment badge', () => {
- it('should update badge style when note-moving action ends', async () => {
+ it('should update badge style when note-moving action ends', () => {
const { position } = notes[0];
createComponent({
currentCommentForm: {
@@ -298,19 +285,15 @@ describe('Design overlay component', () => {
},
});
- const commentBadge = findCommentBadge();
+ expect(findCommentBadge().props('position')).toEqual({ left: '10px', top: '15px' });
+
const toPoint = { x: 20, y: 20 };
- await clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint);
- commentBadge.vm.$emit('mouseup', new MouseEvent('click'));
- // simulates the currentCommentForm being updated in index.vue component, and
- // propagated back down to this prop
- wrapper.setProps({
+ createComponent({
currentCommentForm: { height: position.height, width: position.width, ...toPoint },
});
- await nextTick();
- expect(commentBadge.props('position')).toEqual({ left: '20px', top: '20px' });
+ expect(findCommentBadge().props('position')).toEqual({ left: '20px', top: '20px' });
});
it('should emit `openCommentForm` event when mouseleave fired on overlay element', async () => {
@@ -330,8 +313,7 @@ describe('Design overlay component', () => {
newCoordinates,
);
- wrapper.trigger('mouseleave');
- await nextTick();
+ findOverlay().vm.$emit('mouseleave');
expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
});
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index 4a339899473..fdcea6d88c0 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -15,7 +15,6 @@ const mockOverlayData = {
};
describe('Design management design presentation component', () => {
- const originalGon = window.gon;
let wrapper;
function createComponent(
@@ -114,11 +113,6 @@ describe('Design management design presentation component', () => {
window.gon = { current_user_id: 1 };
});
- afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
- });
-
it('renders image and overlay when image provided', async () => {
createComponent(
{
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
index e1a66cea329..b29448b4471 100644
--- a/spec/frontend/design_management/components/design_scaler_spec.js
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -25,11 +25,6 @@ describe('Design management design scaler component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when `scale` value is greater than 1', () => {
beforeEach(async () => {
setScale(1.6);
@@ -41,7 +36,7 @@ describe('Design management design scaler component', () => {
expect(wrapper.emitted('scale')[1]).toEqual([1]);
});
- it('emits @scale event when "decrement" button clicked', async () => {
+ it('emits @scale event when "decrement" button clicked', () => {
getDecreaseScaleButton().vm.$emit('click');
expect(wrapper.emitted('scale')[1]).toEqual([1.4]);
});
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index af995f75ddc..90424175417 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -29,7 +29,6 @@ const $route = {
const mutate = jest.fn().mockResolvedValue();
describe('Design management design sidebar component', () => {
- const originalGon = window.gon;
let wrapper;
const findDiscussions = () => wrapper.findAllComponents(DesignDiscussion);
@@ -67,11 +66,6 @@ describe('Design management design sidebar component', () => {
window.gon = { current_user_id: 1 };
});
- afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
- });
-
it('renders participants', () => {
createComponent();
@@ -143,8 +137,8 @@ describe('Design management design sidebar component', () => {
expect(findResolvedCommentsToggle().props('visible')).toBe(true);
});
- it('sends a mutation to set an active discussion when clicking on a discussion', () => {
- findFirstDiscussion().trigger('click');
+ it('emits correct event to send a mutation to set an active discussion when clicking on a discussion', () => {
+ findFirstDiscussion().vm.$emit('update-active-discussion');
expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables);
});
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index ac26873b692..698535d8937 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -51,8 +51,6 @@ describe('Design management design todo button', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
jest.clearAllMocks();
});
@@ -83,7 +81,7 @@ describe('Design management design todo button', () => {
await nextTick();
});
- it('calls `$apollo.mutate` with the `todoMarkDone` mutation and variables containing `id`', async () => {
+ it('calls `$apollo.mutate` with the `todoMarkDone` mutation and variables containing `id`', () => {
const todoMarkDoneMutationVariables = {
mutation: todoMarkDoneMutation,
update: expect.anything(),
@@ -129,7 +127,7 @@ describe('Design management design todo button', () => {
await nextTick();
});
- it('calls `$apollo.mutate` with the `createDesignTodoMutation` mutation and variables containing `issuable_id`, `issue_id`, & `projectPath`', async () => {
+ it('calls `$apollo.mutate` with the `createDesignTodoMutation` mutation and variables containing `issuable_id`, `issue_id`, & `projectPath`', () => {
const createDesignTodoMutationVariables = {
mutation: createDesignTodoMutation,
update: expect.anything(),
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index 95d2ad504de..53abcc559d8 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -20,10 +20,6 @@ describe('Design management large image component', () => {
stubPerformanceWebAPI();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders loading state', () => {
createComponent({
isLoading: true,
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 3517c0f7a44..9451f35ac5b 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
@@ -38,13 +38,13 @@ exports[`Design management list item component with notes renders item with mult
</div>
<div
- class="card-footer gl-display-flex gl-w-full"
+ class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4"
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
>
<span
- class="gl-font-weight-bold str-truncated-100"
+ class="gl-font-weight-semibold str-truncated-100"
data-qa-selector="design_file_name"
data-testid="design-img-filename-1"
title="test"
@@ -59,6 +59,7 @@ exports[`Design management list item component with notes renders item with mult
Updated
<timeago-stub
cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
time="01-01-2019"
tooltipplacement="bottom"
/>
@@ -117,13 +118,13 @@ exports[`Design management list item component with notes renders item with sing
</div>
<div
- class="card-footer gl-display-flex gl-w-full"
+ class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4"
>
<div
class="gl-display-flex gl-flex-direction-column str-truncated-100"
>
<span
- class="gl-font-weight-bold str-truncated-100"
+ class="gl-font-weight-semibold str-truncated-100"
data-qa-selector="design_file_name"
data-testid="design-img-filename-1"
title="test"
@@ -138,6 +139,7 @@ exports[`Design management list item component with notes renders item with sing
Updated
<timeago-stub
cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
time="01-01-2019"
tooltipplacement="bottom"
/>
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index e907e2e4ac5..4a0ad5a045b 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -54,10 +54,6 @@ describe('Design management list item component', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when item is not in view', () => {
it('image is not rendered', () => {
createComponent();
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
deleted file mode 100644
index b5a69b28a88..00000000000
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
+++ /dev/null
@@ -1,39 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management pagination component hides components when designs are empty 1`] = `<!---->`;
-
-exports[`Design management pagination component renders navigation buttons 1`] = `
-<div
- class="gl-display-flex gl-align-items-center"
->
-
- 0 of 2
-
- <gl-button-group-stub
- class="gl-mx-5"
- >
- <gl-button-stub
- aria-label="Go to previous design"
- buttontextclasses=""
- category="primary"
- class="js-previous-design"
- disabled="true"
- icon="chevron-lg-left"
- size="medium"
- title="Go to previous design"
- variant="default"
- />
-
- <gl-button-stub
- aria-label="Go to next design"
- buttontextclasses=""
- category="primary"
- class="js-next-design"
- icon="chevron-lg-right"
- size="medium"
- title="Go to next design"
- variant="default"
- />
- </gl-button-group-stub>
-</div>
-`;
diff --git a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
index 38a7fadee79..cee05bafeb6 100644
--- a/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
+++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
@@ -1,9 +1,17 @@
-/* global Mousetrap */
-import 'mousetrap';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlButtonGroup } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import DesignNavigation from '~/design_management/components/toolbar/design_navigation.vue';
import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
+import { Mousetrap } from '~/lib/mousetrap';
+import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ getDesignListQueryResponse,
+ designListQueryResponseNodes,
+} from '../../mock_data/apollo_mock';
const push = jest.fn();
const $router = {
@@ -18,11 +26,23 @@ const $route = {
describe('Design management pagination component', () => {
let wrapper;
- function createComponent() {
+ const buildMockHandler = (nodes = designListQueryResponseNodes) => {
+ return jest.fn().mockResolvedValue(getDesignListQueryResponse({ designs: nodes }));
+ };
+
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[getDesignListQuery, handler]]);
+ };
+
+ function createComponent({ propsData = {}, handler = buildMockHandler() } = {}) {
wrapper = shallowMount(DesignNavigation, {
propsData: {
id: '2',
+ ...propsData,
},
+ apolloProvider: createMockApolloProvider(handler),
mocks: {
$router,
$route,
@@ -30,52 +50,43 @@ describe('Design management pagination component', () => {
});
}
- beforeEach(() => {
- createComponent();
- });
+ const findGlButtonGroup = () => wrapper.findComponent(GlButtonGroup);
- afterEach(() => {
- wrapper.destroy();
- });
+ it('hides components when designs are empty', async () => {
+ createComponent({ handler: buildMockHandler([]) });
+ await waitForPromises();
- it('hides components when designs are empty', () => {
- expect(wrapper.element).toMatchSnapshot();
+ expect(findGlButtonGroup().exists()).toBe(false);
});
it('renders navigation buttons', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- designCollection: { designs: [{ id: '1' }, { id: '2' }] },
- });
+ createComponent({ handler: buildMockHandler() });
+ await waitForPromises();
- await nextTick();
- expect(wrapper.element).toMatchSnapshot();
+ expect(findGlButtonGroup().exists()).toBe(true);
});
describe('keyboard buttons navigation', () => {
- beforeEach(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- designCollection: { designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }] },
- });
- });
+ it('routes to previous design on Left button', async () => {
+ createComponent({ propsData: { id: designListQueryResponseNodes[1].filename } });
+ await waitForPromises();
- it('routes to previous design on Left button', () => {
Mousetrap.trigger('left');
expect(push).toHaveBeenCalledWith({
name: DESIGN_ROUTE_NAME,
- params: { id: '1' },
+ params: { id: designListQueryResponseNodes[0].filename },
query: {},
});
});
- it('routes to next design on Right button', () => {
+ it('routes to next design on Right button', async () => {
+ createComponent({ propsData: { id: designListQueryResponseNodes[1].filename } });
+ await waitForPromises();
+
Mousetrap.trigger('right');
expect(push).toHaveBeenCalledWith({
name: DESIGN_ROUTE_NAME,
- params: { id: '3' },
+ params: { id: designListQueryResponseNodes[2].filename },
query: {},
});
});
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index 1776405ece9..764ad73805f 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -1,12 +1,18 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import DeleteButton from '~/design_management/components/delete_button.vue';
import Toolbar from '~/design_management/components/toolbar/index.vue';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
+import { getPermissionsQueryResponse } from '../../mock_data/apollo_mock';
Vue.use(VueRouter);
+Vue.use(VueApollo);
const router = new VueRouter();
const RouterLinkStub = {
@@ -27,7 +33,12 @@ describe('Design management toolbar component', () => {
const updatedAt = new Date();
updatedAt.setHours(updatedAt.getHours() - 1);
+ const mockApollo = createMockApollo([
+ [permissionsQuery, jest.fn().mockResolvedValue(getPermissionsQueryResponse(createDesign))],
+ ]);
+
wrapper = shallowMount(Toolbar, {
+ apolloProvider: mockApollo,
router,
propsData: {
id: '1',
@@ -46,31 +57,20 @@ describe('Design management toolbar component', () => {
'router-link': RouterLinkStub,
},
});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- permissions: {
- createDesign,
- },
- });
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders design and updated data', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
expect(wrapper.element).toMatchSnapshot();
});
it('links back to designs list', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
const link = wrapper.find('a');
expect(link.props('to')).toEqual({
@@ -84,35 +84,41 @@ describe('Design management toolbar component', () => {
it('renders delete button on latest designs version with logged in user', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
expect(wrapper.findComponent(DeleteButton).exists()).toBe(true);
});
it('does not render delete button on non-latest version', async () => {
createComponent(false, true, { isLatestVersion: false });
- await nextTick();
+ await waitForPromises();
+
expect(wrapper.findComponent(DeleteButton).exists()).toBe(false);
});
it('does not render delete button when user is not logged in', async () => {
createComponent(false, false);
- await nextTick();
+ await waitForPromises();
+
expect(wrapper.findComponent(DeleteButton).exists()).toBe(false);
});
it('emits `delete` event on deleteButton `delete-selected-designs` event', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
wrapper.findComponent(DeleteButton).vm.$emit('delete-selected-designs');
expect(wrapper.emitted().delete).toHaveLength(1);
});
- it('renders download button with correct link', () => {
+ it('renders download button with correct link', async () => {
createComponent();
+ await waitForPromises();
+
expect(wrapper.findComponent(GlButton).attributes('href')).toBe(
'/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
);
diff --git a/spec/frontend/design_management/components/upload/button_spec.js b/spec/frontend/design_management/components/upload/button_spec.js
index 59821218ab8..ceae7920e0d 100644
--- a/spec/frontend/design_management/components/upload/button_spec.js
+++ b/spec/frontend/design_management/components/upload/button_spec.js
@@ -14,10 +14,6 @@ describe('Design management upload button component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders upload design button', () => {
createComponent();
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index 6ad10e707ab..3ee68f80538 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -1,9 +1,14 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlAvatar, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import mockAllVersions from './mock_data/all_versions';
+import mockAllVersions from '../../mock_data/all_versions';
+import { getDesignListQueryResponse } from '../../mock_data/apollo_mock';
const LATEST_VERSION_ID = 1;
const PREVIOUS_VERSION_ID = 2;
@@ -20,11 +25,20 @@ const MOCK_ROUTE = {
query: {},
};
+Vue.use(VueApollo);
+
describe('Design management design version dropdown component', () => {
let wrapper;
function createComponent({ maxVersions = -1, $route = MOCK_ROUTE } = {}) {
+ const designVersions =
+ maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions;
+ const designListHandler = jest
+ .fn()
+ .mockResolvedValue(getDesignListQueryResponse({ versions: designVersions }));
+
wrapper = shallowMount(DesignVersionDropdown, {
+ apolloProvider: createMockApollo([[getDesignListQuery, designListHandler]]),
propsData: {
projectPath: '',
issueIid: '',
@@ -34,18 +48,8 @@ describe('Design management design version dropdown component', () => {
},
stubs: { GlAvatar: true, GlCollapsibleListbox },
});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- allVersions: maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions,
- });
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findVersionLink = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
@@ -56,7 +60,7 @@ describe('Design management design version dropdown component', () => {
beforeEach(async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
listItem = findAllListboxItems().at(0);
});
@@ -78,7 +82,8 @@ describe('Design management design version dropdown component', () => {
it('has "latest" on most recent version item', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
expect(findVersionLink(0).text()).toContain('latest');
});
});
@@ -87,7 +92,7 @@ describe('Design management design version dropdown component', () => {
it('displays latest version text by default', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
@@ -95,35 +100,39 @@ describe('Design management design version dropdown component', () => {
it('displays latest version text when only 1 version is present', async () => {
createComponent({ maxVersions: 1 });
- await nextTick();
+ await waitForPromises();
+
expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('displays version text when the current version is not the latest', async () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
- await nextTick();
+ await waitForPromises();
+
expect(findListbox().props('toggleText')).toBe(`Showing version #1`);
});
it('displays latest version text when the current version is the latest', async () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
- await nextTick();
+ await waitForPromises();
+
expect(findListbox().props('toggleText')).toBe('Showing latest version');
});
it('should have the same length as apollo query', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
+
expect(findAllListboxItems()).toHaveLength(wrapper.vm.allVersions.length);
});
it('should render TimeAgo', async () => {
createComponent();
- await nextTick();
+ await waitForPromises();
expect(wrapper.findAllComponents(TimeAgo)).toHaveLength(wrapper.vm.allVersions.length);
});
diff --git a/spec/frontend/design_management/components/upload/mock_data/all_versions.js b/spec/frontend/design_management/components/upload/mock_data/all_versions.js
deleted file mode 100644
index 24c59ce1a75..00000000000
--- a/spec/frontend/design_management/components/upload/mock_data/all_versions.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export default [
- {
- id: 'gid://gitlab/DesignManagement::Version/1',
- sha: 'b389071a06c153509e11da1f582005b316667001',
- createdAt: '2021-08-09T06:05:00Z',
- author: {
- id: 'gid://gitlab/User/1',
- name: 'Adminstrator',
- },
- },
- {
- id: 'gid://gitlab/DesignManagement::Version/2',
- sha: 'b389071a06c153509e11da1f582005b316667021',
- createdAt: '2021-08-09T06:05:00Z',
- author: {
- id: 'gid://gitlab/User/1',
- name: 'Adminstrator',
- },
- },
-];
diff --git a/spec/frontend/design_management/mock_data/all_versions.js b/spec/frontend/design_management/mock_data/all_versions.js
index f4026da7dfd..36f611247a9 100644
--- a/spec/frontend/design_management/mock_data/all_versions.js
+++ b/spec/frontend/design_management/mock_data/all_versions.js
@@ -1,20 +1,26 @@
export default [
{
+ __typename: 'DesignVersion',
id: 'gid://gitlab/DesignManagement::Version/1',
sha: 'b389071a06c153509e11da1f582005b316667001',
createdAt: '2021-08-09T06:05:00Z',
author: {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/1',
name: 'Adminstrator',
+ avatarUrl: 'avatar.png',
},
},
{
- id: 'gid://gitlab/DesignManagement::Version/1',
+ __typename: 'DesignVersion',
+ id: 'gid://gitlab/DesignManagement::Version/2',
sha: 'b389071a06c153509e11da1f582005b316667021',
createdAt: '2021-08-09T06:05:00Z',
author: {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/1',
name: 'Adminstrator',
+ avatarUrl: 'avatar.png',
},
},
];
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
index 2a43b5debee..18e08ecd729 100644
--- a/spec/frontend/design_management/mock_data/apollo_mock.js
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -1,4 +1,49 @@
-export const designListQueryResponse = {
+export const designListQueryResponseNodes = [
+ {
+ __typename: 'Design',
+ id: '1',
+ event: 'NONE',
+ filename: 'fox_1.jpg',
+ notesCount: 3,
+ image: 'image-1',
+ imageV432x230: 'image-1',
+ currentUserTodos: {
+ __typename: 'ToDo',
+ nodes: [],
+ },
+ },
+ {
+ __typename: 'Design',
+ id: '2',
+ event: 'NONE',
+ filename: 'fox_2.jpg',
+ notesCount: 2,
+ image: 'image-2',
+ imageV432x230: 'image-2',
+ currentUserTodos: {
+ __typename: 'ToDo',
+ nodes: [],
+ },
+ },
+ {
+ __typename: 'Design',
+ id: '3',
+ event: 'NONE',
+ filename: 'fox_3.jpg',
+ notesCount: 1,
+ image: 'image-3',
+ imageV432x230: 'image-3',
+ currentUserTodos: {
+ __typename: 'ToDo',
+ nodes: [],
+ },
+ },
+];
+
+export const getDesignListQueryResponse = ({
+ versions = [],
+ designs = designListQueryResponseNodes,
+} = {}) => ({
data: {
project: {
__typename: 'Project',
@@ -11,57 +56,17 @@ export const designListQueryResponse = {
copyState: 'READY',
designs: {
__typename: 'DesignConnection',
- nodes: [
- {
- __typename: 'Design',
- id: '1',
- event: 'NONE',
- filename: 'fox_1.jpg',
- notesCount: 3,
- image: 'image-1',
- imageV432x230: 'image-1',
- currentUserTodos: {
- __typename: 'ToDo',
- nodes: [],
- },
- },
- {
- __typename: 'Design',
- id: '2',
- event: 'NONE',
- filename: 'fox_2.jpg',
- notesCount: 2,
- image: 'image-2',
- imageV432x230: 'image-2',
- currentUserTodos: {
- __typename: 'ToDo',
- nodes: [],
- },
- },
- {
- __typename: 'Design',
- id: '3',
- event: 'NONE',
- filename: 'fox_3.jpg',
- notesCount: 1,
- image: 'image-3',
- imageV432x230: 'image-3',
- currentUserTodos: {
- __typename: 'ToDo',
- nodes: [],
- },
- },
- ],
+ nodes: designs,
},
versions: {
- __typename: 'DesignVersion',
- nodes: [],
+ __typename: 'DesignVersionConnection',
+ nodes: versions,
},
},
},
},
},
-};
+});
export const designUploadMutationCreatedResponse = {
data: {
@@ -91,7 +96,7 @@ export const designUploadMutationUpdatedResponse = {
},
};
-export const permissionsQueryResponse = {
+export const getPermissionsQueryResponse = (createDesign = true) => ({
data: {
project: {
__typename: 'Project',
@@ -99,11 +104,11 @@ export const permissionsQueryResponse = {
issue: {
__typename: 'Issue',
id: 'issue-1',
- userPermissions: { __typename: 'UserPermissions', createDesign: true },
+ userPermissions: { __typename: 'UserPermissions', createDesign },
},
},
},
-};
+});
export const reorderedDesigns = [
{
@@ -211,3 +216,107 @@ export const getDesignQueryResponse = {
},
},
};
+
+export const mockNoteSubmitSuccessMutationResponse = {
+ data: {
+ createNote: {
+ note: {
+ id: 'gid://gitlab/DiffNote/468',
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ body: 'New comment',
+ bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>",
+ createdAt: '2023-02-24T06:49:20Z',
+ resolved: false,
+ position: {
+ diffRefs: {
+ baseSha: 'f63ae53ed82d8765477c191383e1e6a000c10375',
+ startSha: 'f63ae53ed82d8765477c191383e1e6a000c10375',
+ headSha: 'f348c652f1a737151fc79047895e695fbe81464c',
+ __typename: 'DiffRefs',
+ },
+ x: 441,
+ y: 128,
+ height: 152,
+ width: 695,
+ __typename: 'DiffPosition',
+ },
+ userPermissions: {
+ adminNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ discussion: {
+ id: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiffNote/459',
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ errors: [],
+ __typename: 'CreateNotePayload',
+ },
+ },
+};
+
+export const mockNoteSubmitFailureMutationResponse = [
+ {
+ errors: [
+ {
+ message:
+ 'Variable $input of type CreateNoteInput! was provided invalid value for bodyaa (Field is not defined on CreateNoteInput), body (Expected value to not be null)',
+ locations: [
+ {
+ line: 1,
+ column: 21,
+ },
+ ],
+ extensions: {
+ value: {
+ noteableId: 'gid://gitlab/DesignManagement::Design/10',
+ discussionId: 'gid://gitlab/Discussion/6466a72f35b163f3c3e52d7976a09387f2c573e8',
+ bodyaa: 'df',
+ },
+ problems: [
+ {
+ path: ['bodyaa'],
+ explanation: 'Field is not defined on CreateNoteInput',
+ },
+ {
+ path: ['body'],
+ explanation: 'Expected value to not be null',
+ },
+ ],
+ },
+ },
+ ],
+ },
+];
+
+export const mockCreateImageNoteDiffResponse = {
+ data: {
+ createImageDiffNote: {
+ note: {
+ author: {
+ username: '',
+ },
+ discussion: {},
+ },
+ },
+ },
+};
diff --git a/spec/frontend/design_management/mock_data/project.js b/spec/frontend/design_management/mock_data/project.js
new file mode 100644
index 00000000000..e1c2057d8d1
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/project.js
@@ -0,0 +1,17 @@
+import design from './design';
+
+export default {
+ project: {
+ issue: {
+ designCollection: {
+ designs: {
+ nodes: [
+ {
+ ...design,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index ef1ed9bee51..7da0652faba 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -9,7 +9,9 @@ exports[`Design management index page designs renders error 1`] = `
<!---->
- <div>
+ <div
+ class="gl-bg-gray-10 gl-border gl-border-t-0 gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-5"
+ >
<gl-alert-stub
dismisslabel="Dismiss"
primarybuttonlink=""
@@ -41,7 +43,9 @@ exports[`Design management index page designs renders loading icon 1`] = `
<!---->
- <div>
+ <div
+ class="gl-bg-gray-10 gl-border gl-border-t-0 gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-5"
+ >
<gl-loading-icon-stub
color="dark"
label="Loading"
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 d86fbf81d20..18b63082e4a 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,7 +2,7 @@
exports[`Design management design index page renders design index 1`] = `
<div
- class="design-detail js-design-detail fixed-top gl-w-full gl-bottom-0 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
+ 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"
>
<div
class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
@@ -115,7 +115,7 @@ exports[`Design management design index page renders design index 1`] = `
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-bottom-0 gl-display-flex gl-justify-content-center gl-flex-direction-column gl-lg-flex-direction-row"
+ 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"
>
<div
class="gl-display-flex gl-overflow-hidden gl-flex-grow-1 gl-flex-direction-column gl-relative"
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index a11463ab663..fcb03ea3700 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -1,15 +1,14 @@
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-import { ApolloMutation } from 'vue-apollo';
import VueRouter from 'vue-router';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api';
import DesignPresentation from '~/design_management/components/design_presentation.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
-import createImageDiffNoteMutation from '~/design_management/graphql/mutations/create_image_diff_note.mutation.graphql';
import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
+import getDesignQuery from '~/design_management/graphql/queries/get_design.query.graphql';
import DesignIndex from '~/design_management/pages/design/index.vue';
import createRouter from '~/design_management/router';
import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
@@ -23,16 +22,23 @@ import {
DESIGN_SNOWPLOW_EVENT_TYPES,
DESIGN_SERVICE_PING_EVENT_TYPES,
} from '~/design_management/utils/tracking';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
+import * as cacheUpdate from '~/design_management/utils/cache_update';
import mockAllVersions from '../../mock_data/all_versions';
import design from '../../mock_data/design';
+import mockProject from '../../mock_data/project';
import mockResponseWithDesigns from '../../mock_data/designs';
import mockResponseNoDesigns from '../../mock_data/no_designs';
+import { mockCreateImageNoteDiffResponse } from '../../mock_data/apollo_mock';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/api.js');
const focusInput = jest.fn();
+const mockCacheObject = {
+ readQuery: jest.fn().mockReturnValue(mockProject),
+ writeQuery: jest.fn(),
+};
const mutate = jest.fn().mockResolvedValue();
const mockPageLayoutElement = {
classList: {
@@ -52,32 +58,13 @@ const mockDesignNoDiscussions = {
nodes: [],
},
};
-const newComment = 'new comment';
+
const annotationCoordinates = {
x: 10,
y: 10,
width: 100,
height: 100,
};
-const createDiscussionMutationVariables = {
- mutation: createImageDiffNoteMutation,
- update: expect.anything(),
- variables: {
- input: {
- body: newComment,
- noteableId: design.id,
- position: {
- headSha: 'headSha',
- baseSha: 'baseSha',
- startSha: 'startSha',
- paths: {
- newPath: 'full-design-path',
- },
- ...annotationCoordinates,
- },
- },
- },
-};
Vue.use(VueRouter);
@@ -85,7 +72,7 @@ describe('Design management design index page', () => {
let wrapper;
let router;
- const findDiscussionForm = () => wrapper.findComponent(DesignReplyForm);
+ const findDesignReplyForm = () => wrapper.findComponent(DesignReplyForm);
const findSidebar = () => wrapper.findComponent(DesignSidebar);
const findDesignPresentation = () => wrapper.findComponent(DesignPresentation);
@@ -95,7 +82,7 @@ describe('Design management design index page', () => {
data = {},
intialRouteOptions = {},
provide = {},
- stubs = { ApolloMutation, DesignSidebar, DesignReplyForm },
+ stubs = { DesignSidebar, DesignReplyForm },
} = {},
) {
const $apollo = {
@@ -105,6 +92,11 @@ describe('Design management design index page', () => {
},
},
mutate,
+ getClient() {
+ return {
+ cache: mockCacheObject,
+ };
+ },
};
router = createRouter();
@@ -133,10 +125,6 @@ describe('Design management design index page', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when navigating to component', () => {
it('applies fullscreen layout class', () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
@@ -165,7 +153,7 @@ describe('Design management design index page', () => {
});
describe('when navigating away from component', () => {
- it('removes fullscreen layout class', async () => {
+ it('removes fullscreen layout class', () => {
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageLayoutElement);
createComponent({ loading: true });
@@ -216,7 +204,7 @@ describe('Design management design index page', () => {
findDesignPresentation().vm.$emit('openCommentForm', { x: 0, y: 0 });
await nextTick();
- expect(findDiscussionForm().exists()).toBe(true);
+ expect(findDesignReplyForm().exists()).toBe(true);
});
it('keeps new discussion form focused', () => {
@@ -235,24 +223,36 @@ describe('Design management design index page', () => {
expect(focusInput).toHaveBeenCalled();
});
- it('sends a mutation on submitting form and closes form', async () => {
+ it('sends a update and closes the form when mutation is completed', async () => {
createComponent(
{ loading: false },
{
data: {
design,
annotationCoordinates,
- comment: newComment,
},
},
);
- findDiscussionForm().vm.$emit('submit-form');
- expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables);
+ const addImageDiffNoteToStore = jest.spyOn(cacheUpdate, 'updateStoreAfterAddImageDiffNote');
+
+ const mockDesignVariables = {
+ fullPath: 'project-path',
+ iid: '1',
+ filenames: ['gid::/gitlab/Design/1'],
+ atVersion: null,
+ };
+
+ findDesignReplyForm().vm.$emit('note-submit-complete', mockCreateImageNoteDiffResponse);
await nextTick();
- await mutate({ variables: createDiscussionMutationVariables });
- expect(findDiscussionForm().exists()).toBe(false);
+ expect(addImageDiffNoteToStore).toHaveBeenCalledWith(
+ mockCacheObject,
+ mockCreateImageNoteDiffResponse.data.createImageDiffNote,
+ getDesignQuery,
+ mockDesignVariables,
+ );
+ expect(findDesignReplyForm().exists()).toBe(false);
});
it('closes the form and clears the comment on canceling form', async () => {
@@ -262,17 +262,14 @@ describe('Design management design index page', () => {
data: {
design,
annotationCoordinates,
- comment: newComment,
},
},
);
- findDiscussionForm().vm.$emit('cancel-form');
-
- expect(wrapper.vm.comment).toBe('');
+ findDesignReplyForm().vm.$emit('cancel-form');
await nextTick();
- expect(findDiscussionForm().exists()).toBe(false);
+ expect(findDesignReplyForm().exists()).toBe(false);
});
describe('with error', () => {
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 76ece922ded..1a6403d3b87 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -29,19 +29,19 @@ import {
DESIGN_TRACKING_PAGE_NAME,
DESIGN_SNOWPLOW_EVENT_TYPES,
} from '~/design_management/utils/tracking';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
import {
- designListQueryResponse,
+ getDesignListQueryResponse,
designUploadMutationCreatedResponse,
designUploadMutationUpdatedResponse,
- permissionsQueryResponse,
+ getPermissionsQueryResponse,
moveDesignMutationResponse,
reorderedDesigns,
moveDesignMutationResponseWithErrors,
} from '../mock_data/apollo_mock';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
const mockPageEl = {
classList: {
remove: jest.fn(),
@@ -100,6 +100,7 @@ describe('Design management index page', () => {
let wrapper;
let fakeApollo;
let moveDesignHandler;
+ let permissionsQueryHandler;
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
const findSelectAllButton = () => wrapper.findByTestId('select-all-designs-button');
@@ -174,14 +175,16 @@ describe('Design management index page', () => {
}
function createComponentWithApollo({
+ permissionsHandler = jest.fn().mockResolvedValue(getPermissionsQueryResponse()),
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
}) {
Vue.use(VueApollo);
+ permissionsQueryHandler = permissionsHandler;
moveDesignHandler = moveHandler;
const requestHandlers = [
- [getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
- [permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
+ [getDesignListQuery, jest.fn().mockResolvedValue(getDesignListQueryResponse())],
+ [permissionsQuery, permissionsQueryHandler],
[moveDesignMutation, moveDesignHandler],
];
@@ -197,11 +200,6 @@ describe('Design management index page', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('designs', () => {
it('renders loading icon', () => {
createComponent({ loading: true });
@@ -235,13 +233,6 @@ describe('Design management index page', () => {
expect(findDesignUploadButton().exists()).toBe(true);
});
- it('does not render toolbar when there is no permission', () => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
-
- expect(findDesignToolbarWrapper().exists()).toBe(false);
- expect(findDesignUploadButton().exists()).toBe(false);
- });
-
it('has correct classes applied to design dropzone', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
expect(dropzoneClasses()).toContain('design-list-item');
@@ -728,7 +719,7 @@ describe('Design management index page', () => {
expect(mockMutate).not.toHaveBeenCalled();
});
- it('removes onPaste listener after mouseleave event', async () => {
+ it('removes onPaste listener after mouseleave event', () => {
findDesignsWrapper().trigger('mouseleave');
document.dispatchEvent(event);
@@ -749,6 +740,17 @@ describe('Design management index page', () => {
});
});
+ describe('when there is no permission to create a design', () => {
+ beforeEach(() => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
+ });
+
+ it("doesn't render the design toolbar and dropzone", () => {
+ expect(findToolbar().exists()).toBe(false);
+ expect(findDropzoneWrapper().exists()).toBe(false);
+ });
+ });
+
describe('with mocked Apollo client', () => {
it('has a design with id 1 as a first one', async () => {
createComponentWithApollo({});
@@ -800,7 +802,7 @@ describe('Design management index page', () => {
expect(draggableAttributes().disabled).toBe(false);
});
- it('displays flash if mutation had a recoverable error', async () => {
+ it('displays alert if mutation had a recoverable error', async () => {
createComponentWithApollo({
moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
});
@@ -824,5 +826,17 @@ describe('Design management index page', () => {
'Something went wrong when reordering designs. Please try again',
);
});
+
+ it("doesn't render the design toolbar and dropzone if the user can't edit", async () => {
+ createComponentWithApollo({
+ permissionsHandler: jest.fn().mockResolvedValue(getPermissionsQueryResponse(false)),
+ });
+
+ await waitForPromises();
+
+ expect(permissionsQueryHandler).toHaveBeenCalled();
+ expect(findToolbar().exists()).toBe(false);
+ expect(findDropzoneWrapper().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
index b9edde559c8..3503725f741 100644
--- a/spec/frontend/design_management/router_spec.js
+++ b/spec/frontend/design_management/router_spec.js
@@ -11,8 +11,6 @@ import '~/commons/bootstrap';
function factory(routeArg) {
Vue.use(VueRouter);
- window.gon = { sprite_icons: '' };
-
const router = createRouter('/');
if (routeArg !== undefined) {
router.push(routeArg);
@@ -36,10 +34,6 @@ function factory(routeArg) {
}
describe('Design management router', () => {
- afterEach(() => {
- window.location.hash = '';
- });
-
describe.each([['/'], [{ name: DESIGNS_ROUTE_NAME }]])('root route', (routeArg) => {
it('pushes home component', () => {
const wrapper = factory(routeArg);
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index 42777adfd58..e89dfe9f860 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -10,10 +10,10 @@ import {
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
} from '~/design_management/utils/error_messages';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import design from '../mock_data/design';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Design Management cache update', () => {
const mockErrors = ['code red!'];
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 513e67ea247..42eec0af961 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -1,7 +1,6 @@
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import Mousetrap from 'mousetrap';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -11,6 +10,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 TreeList from '~/diffs/components/tree_list.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
@@ -18,6 +18,7 @@ import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { Mousetrap } from '~/lib/mousetrap';
import * as urlUtils from '~/lib/utils/url_utility';
import { stubPerformanceWebAPI } from 'helpers/performance';
import createDiffsStore from '../create_diffs_store';
@@ -30,6 +31,8 @@ const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`;
Vue.use(Vuex);
+Vue.config.ignoredElements = ['copy-code'];
+
function getCollapsedFilesWarning(wrapper) {
return wrapper.findComponent(CollapsedFilesWarning);
}
@@ -59,6 +62,7 @@ describe('diffs/components/app', () => {
endpoint: TEST_ENDPOINT,
endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`,
endpointBatch: `${TEST_HOST}/diff/endpointBatch`,
+ endpointDiffForPath: TEST_ENDPOINT,
endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`,
endpointCodequality: '',
projectPath: 'namespace/project',
@@ -71,12 +75,6 @@ describe('diffs/components/app', () => {
},
provide,
store,
- stubs: {
- DynamicScroller: {
- template: `<div><slot :item="$store.state.diffs.diffFiles[0]"></slot></div>`,
- },
- DynamicScrollerItem: true,
- },
});
}
@@ -95,12 +93,6 @@ describe('diffs/components/app', () => {
// reset globals
window.mrTabs = oldMrTabs;
- // reset component
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
-
mock.restore();
});
@@ -179,7 +171,7 @@ describe('diffs/components/app', () => {
});
describe('codequality diff', () => {
- it('does not fetch code quality data on FOSS', async () => {
+ it('does not fetch code quality data on FOSS', () => {
createComponent();
jest.spyOn(wrapper.vm, 'fetchCodequality');
wrapper.vm.fetchData(false);
@@ -265,7 +257,7 @@ describe('diffs/components/app', () => {
it('sets width of tree list', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
+ state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
});
expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px');
@@ -294,13 +286,14 @@ describe('diffs/components/app', () => {
it('does not render empty state when diff files exist', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({
- id: 1,
- });
+ state.diffs.diffFiles = ['anything'];
+ state.diffs.treeEntries['1'] = { type: 'blob', id: 1 };
});
expect(wrapper.findComponent(NoChanges).exists()).toBe(false);
- expect(wrapper.findAllComponents(DiffFile).length).toBe(1);
+ expect(wrapper.findComponent({ name: 'DynamicScroller' }).props('items')).toBe(
+ store.state.diffs.diffFiles,
+ );
});
});
@@ -388,19 +381,15 @@ describe('diffs/components/app', () => {
beforeEach(() => {
createComponent({}, () => {
- store.state.diffs.diffFiles = [
- { file_hash: '111', file_path: '111.js' },
- { file_hash: '222', file_path: '222.js' },
- { file_hash: '333', file_path: '333.js' },
+ store.state.diffs.treeEntries = [
+ { type: 'blob', fileHash: '111', path: '111.js' },
+ { type: 'blob', fileHash: '222', path: '222.js' },
+ { type: 'blob', fileHash: '333', path: '333.js' },
];
});
spy = jest.spyOn(store, 'dispatch');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('jumps to next and previous files in the list', async () => {
await nextTick();
@@ -507,7 +496,6 @@ describe('diffs/components/app', () => {
describe('diffs', () => {
it('should render compare versions component', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
state.diffs.mergeRequestDiffs = diffsMockData;
state.diffs.targetBranchName = 'target-branch';
state.diffs.mergeRequestDiff = mergeRequestDiff;
@@ -578,10 +566,18 @@ describe('diffs/components/app', () => {
it('should display diff file if there are diff files', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({ sha: '123' });
+ state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
+ state.diffs.treeEntries = {
+ 111: { type: 'blob', fileHash: '111', path: '111.js' },
+ 123: { type: 'blob', fileHash: '123', path: '123.js' },
+ 312: { type: 'blob', fileHash: '312', path: '312.js' },
+ };
});
- expect(wrapper.findComponent(DiffFile).exists()).toBe(true);
+ expect(wrapper.findComponent({ name: 'DynamicScroller' }).exists()).toBe(true);
+ expect(wrapper.findComponent({ name: 'DynamicScroller' }).props('items')).toBe(
+ store.state.diffs.diffFiles,
+ );
});
it("doesn't render tree list when no changes exist", () => {
@@ -592,7 +588,7 @@ describe('diffs/components/app', () => {
it('should render tree list', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }];
+ state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
});
expect(wrapper.findComponent(TreeList).exists()).toBe(true);
@@ -606,7 +602,7 @@ describe('diffs/components/app', () => {
it('calls setShowTreeList when only 1 file', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({ sha: '123' });
+ state.diffs.treeEntries = { 123: { type: 'blob', fileHash: '123' } };
});
jest.spyOn(store, 'dispatch');
wrapper.vm.setTreeDisplay();
@@ -617,10 +613,12 @@ describe('diffs/components/app', () => {
});
});
- it('calls setShowTreeList with true when more than 1 file is in diffs array', () => {
+ it('calls setShowTreeList with true when more than 1 file is in tree entries map', () => {
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({ sha: '123' });
- state.diffs.diffFiles.push({ sha: '124' });
+ state.diffs.treeEntries = {
+ 111: { type: 'blob', fileHash: '111', path: '111.js' },
+ 123: { type: 'blob', fileHash: '123', path: '123.js' },
+ };
});
jest.spyOn(store, 'dispatch');
@@ -640,7 +638,7 @@ describe('diffs/components/app', () => {
localStorage.setItem('mr_tree_show', showTreeList);
createComponent({}, ({ state }) => {
- state.diffs.diffFiles.push({ sha: '123' });
+ state.diffs.treeEntries['123'] = { sha: '123' };
});
jest.spyOn(store, 'dispatch');
@@ -656,7 +654,10 @@ describe('diffs/components/app', () => {
describe('file-by-file', () => {
it('renders a single diff', async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' });
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123' },
+ 312: { type: 'blob', fileHash: '312' },
+ };
state.diffs.diffFiles.push({ file_hash: '312' });
});
@@ -671,7 +672,10 @@ describe('diffs/components/app', () => {
it('sets previous button as disabled', async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123' },
+ 312: { type: 'blob', fileHash: '312' },
+ };
});
await nextTick();
@@ -682,7 +686,10 @@ describe('diffs/components/app', () => {
it('sets next button as disabled', async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123' },
+ 312: { type: 'blob', fileHash: '312' },
+ };
state.diffs.currentDiffFileId = '312';
});
@@ -694,7 +701,7 @@ describe('diffs/components/app', () => {
it("doesn't display when there's fewer than 2 files", async () => {
createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' });
+ state.diffs.treeEntries = { 123: { type: 'blob', fileHash: '123' } };
state.diffs.currentDiffFileId = '123';
});
@@ -704,16 +711,27 @@ describe('diffs/components/app', () => {
});
it.each`
- currentDiffFileId | targetFile
- ${'123'} | ${2}
- ${'312'} | ${1}
+ currentDiffFileId | targetFile | newFileByFile
+ ${'123'} | ${2} | ${false}
+ ${'312'} | ${1} | ${true}
`(
'calls navigateToDiffFileIndex with $index when $link is clicked',
- async ({ currentDiffFileId, targetFile }) => {
- createComponent({ fileByFileUserPreference: true }, ({ state }) => {
- state.diffs.diffFiles.push({ file_hash: '123' }, { file_hash: '312' });
- state.diffs.currentDiffFileId = currentDiffFileId;
- });
+ async ({ currentDiffFileId, targetFile, newFileByFile }) => {
+ createComponent(
+ { fileByFileUserPreference: true },
+ ({ state }) => {
+ state.diffs.treeEntries = {
+ 123: { type: 'blob', fileHash: '123', filePaths: { old: '1234', new: '123' } },
+ 312: { type: 'blob', fileHash: '312', filePaths: { old: '3124', new: '312' } },
+ };
+ state.diffs.currentDiffFileId = currentDiffFileId;
+ },
+ {
+ glFeatures: {
+ singleFileFileByFile: newFileByFile,
+ },
+ },
+ );
await nextTick();
@@ -723,9 +741,28 @@ describe('diffs/components/app', () => {
await nextTick();
- expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith(targetFile - 1);
+ expect(wrapper.vm.navigateToDiffFileIndex).toHaveBeenCalledWith({
+ index: targetFile - 1,
+ singleFile: newFileByFile,
+ });
},
);
});
});
+
+ 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/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
index eca5b536a35..ae40f6c898d 100644
--- a/spec/frontend/diffs/components/collapsed_files_warning_spec.js
+++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js
@@ -45,10 +45,6 @@ describe('CollapsedFilesWarning', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there is more than one file', () => {
it.each`
present | dismissed
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 08be3fa2745..3c092296130 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlFormCheckbox } from '@gitlab/ui';
import getDiffWithCommit from 'test_fixtures/merge_request_diffs/with_commit.json';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
@@ -28,6 +29,7 @@ describe('diffs/components/commit_item', () => {
const getCommitterElement = () => wrapper.find('.committer');
const getCommitActionsElement = () => wrapper.find('.commit-actions');
const getCommitPipelineStatus = () => wrapper.findComponent(CommitPipelineStatus);
+ const getCommitCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const mountComponent = (propsData) => {
wrapper = mount(Component, {
@@ -41,11 +43,6 @@ describe('diffs/components/commit_item', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default state', () => {
beforeEach(() => {
mountComponent();
@@ -173,4 +170,24 @@ describe('diffs/components/commit_item', () => {
expect(getCommitPipelineStatus().exists()).toBe(true);
});
});
+
+ describe('when commit is selectable', () => {
+ beforeEach(() => {
+ mountComponent({
+ commit: { ...commit },
+ isSelectable: true,
+ });
+ });
+
+ it('renders checkbox', () => {
+ expect(getCommitCheckbox().exists()).toBe(true);
+ });
+
+ it('emits "handleCheckboxChange" event on change', () => {
+ expect(wrapper.emitted('handleCheckboxChange')).toBeUndefined();
+ getCommitCheckbox().vm.$emit('change');
+
+ expect(wrapper.emitted('handleCheckboxChange')[0]).toEqual([true]);
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
index 09128b04caa..785ff537777 100644
--- a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
+++ b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js
@@ -38,11 +38,6 @@ describe('CompareDropdownLayout', () => {
isActive: listItem.classes().includes('is-active'),
}));
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with versions', () => {
beforeEach(() => {
const versions = [
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 21f3ee26bf8..47a266c2e36 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -21,6 +21,7 @@ beforeEach(() => {
describe('CompareVersions', () => {
let wrapper;
let store;
+ let dispatchMock;
const targetBranchName = 'tmp-wine-dev';
const { commit } = getDiffWithCommit;
@@ -29,6 +30,8 @@ describe('CompareVersions', () => {
store.state.diffs.commit = { ...store.state.diffs.commit, ...commitArgs };
}
+ dispatchMock = jest.spyOn(store, 'dispatch');
+
wrapper = mount(CompareVersionsComponent, {
store,
propsData: {
@@ -58,11 +61,6 @@ describe('CompareVersions', () => {
store.state.diffs.mergeRequestDiffs = diffsMockData;
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
beforeEach(() => {
createWrapper({}, {}, false);
@@ -151,7 +149,7 @@ describe('CompareVersions', () => {
it('renders short commit ID', () => {
expect(wrapper.text()).toContain('Viewing commit');
- expect(wrapper.text()).toContain(wrapper.vm.commit.short_id);
+ expect(wrapper.text()).toContain(commit.short_id);
});
});
@@ -209,10 +207,6 @@ describe('CompareVersions', () => {
setWindowLocation(`?commit_id=${mrCommit.id}`);
});
- beforeEach(() => {
- jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
- });
-
it('uses the correct href', () => {
const link = getPrevCommitNavElement();
@@ -224,7 +218,7 @@ describe('CompareVersions', () => {
link.trigger('click');
await nextTick();
- expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({
+ expect(dispatchMock).toHaveBeenCalledWith('diffs/moveToNeighboringCommit', {
direction: 'previous',
});
});
@@ -243,10 +237,6 @@ describe('CompareVersions', () => {
setWindowLocation(`?commit_id=${mrCommit.id}`);
});
- beforeEach(() => {
- jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
- });
-
it('uses the correct href', () => {
const link = getNextCommitNavElement();
@@ -258,7 +248,9 @@ describe('CompareVersions', () => {
link.trigger('click');
await nextTick();
- expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({ direction: 'next' });
+ expect(dispatchMock).toHaveBeenCalledWith('diffs/moveToNeighboringCommit', {
+ direction: 'next',
+ });
});
it('renders a disabled button when there is no next commit', () => {
diff --git a/spec/frontend/diffs/components/diff_code_quality_item_spec.js b/spec/frontend/diffs/components/diff_code_quality_item_spec.js
new file mode 100644
index 00000000000..be9fb61a77d
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_code_quality_item_spec.js
@@ -0,0 +1,66 @@
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
+import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { multipleFindingsArr } from '../mock_data/diff_code_quality';
+
+let wrapper;
+
+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 = {} } = {}) => {
+ return shallowMountExtended(DiffCodeQualityItem, {
+ propsData: {
+ finding: multipleFindingsArr[0],
+ },
+ provide: {
+ glFeatures,
+ },
+ });
+ };
+
+ it('shows icon for given degradation', () => {
+ wrapper = createWrapper();
+ expect(findIcon().exists()).toBe(true);
+
+ expect(findIcon().attributes()).toMatchObject({
+ class: `codequality-severity-icon ${SEVERITY_CLASSES[multipleFindingsArr[0].severity]}`,
+ name: SEVERITY_ICONS[multipleFindingsArr[0].severity],
+ size: '12',
+ });
+ });
+
+ describe('with codeQualityInlineDrawer flag false', () => {
+ it('should render severity + description in plain text', () => {
+ wrapper = createWrapper({
+ glFeatures: {
+ codeQualityInlineDrawer: false,
+ },
+ });
+ expect(findDescriptionPlainText().text()).toContain(multipleFindingsArr[0].severity);
+ expect(findDescriptionPlainText().text()).toContain(multipleFindingsArr[0].description);
+ });
+ });
+
+ describe('with codeQualityInlineDrawer flag true', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ glFeatures: {
+ codeQualityInlineDrawer: true,
+ },
+ });
+ });
+
+ it('should render severity as plain text', () => {
+ expect(findDescriptionLinkSection().text()).toContain(multipleFindingsArr[0].severity);
+ });
+
+ it('should render button with description text', () => {
+ expect(findButton().text()).toContain(multipleFindingsArr[0].description);
+ });
+ });
+});
diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js
index 7bd9afab648..9ecfb62e1c5 100644
--- a/spec/frontend/diffs/components/diff_code_quality_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_spec.js
@@ -1,20 +1,15 @@
-import { GlIcon } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
import { multipleFindingsArr } from '../mock_data/diff_code_quality';
let wrapper;
-const findIcon = () => wrapper.findComponent(GlIcon);
+const diffItems = () => wrapper.findAllComponents(DiffCodeQualityItem);
const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`);
describe('DiffCodeQuality', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
const createWrapper = (codeQuality, mountFunction = mountExtended) => {
return mountFunction(DiffCodeQuality, {
propsData: {
@@ -32,37 +27,12 @@ describe('DiffCodeQuality', () => {
expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1);
});
- it('renders heading and correct amount of list items for codequality array and their description', async () => {
- wrapper = createWrapper(multipleFindingsArr);
- expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS);
-
- const listItems = wrapper.findAll('li');
- expect(wrapper.findAll('li').length).toBe(5);
+ it('renders heading and correct amount of list items for codequality array and their description', () => {
+ wrapper = createWrapper(multipleFindingsArr, shallowMountExtended);
- listItems.wrappers.map((e, i) => {
- return expect(e.text()).toContain(
- `${multipleFindingsArr[i].severity} - ${multipleFindingsArr[i].description}`,
- );
- });
- });
-
- it.each`
- severity
- ${'info'}
- ${'minor'}
- ${'major'}
- ${'critical'}
- ${'blocker'}
- ${'unknown'}
- `('shows icon for $severity degradation', ({ severity }) => {
- wrapper = createWrapper([{ severity }], shallowMountExtended);
-
- expect(findIcon().exists()).toBe(true);
+ expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS);
- expect(findIcon().attributes()).toMatchObject({
- class: `codequality-severity-icon ${SEVERITY_CLASSES[severity]}`,
- name: SEVERITY_ICONS[severity],
- size: '12',
- });
+ expect(diffItems()).toHaveLength(multipleFindingsArr.length);
+ expect(diffItems().at(0).props().finding).toEqual(multipleFindingsArr[0]);
});
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 0bce6451ce4..3524973278c 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -93,11 +93,6 @@ describe('DiffContent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with text based files', () => {
afterEach(() => {
[isParallelViewGetterMock, isInlineViewGetterMock].forEach((m) => m.mockRestore());
diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
index bf4a1a1c1f7..348439d6006 100644
--- a/spec/frontend/diffs/components/diff_discussion_reply_spec.js
+++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js
@@ -26,10 +26,6 @@ describe('DiffDiscussionReply', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('if user can reply', () => {
beforeEach(() => {
getters = {
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index 5092ae6ab6e..73d9f2d6d45 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -25,10 +25,6 @@ describe('DiffDiscussions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('should have notes list', () => {
createComponent();
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index c23eb2f3d24..900aa8d1469 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -72,8 +72,6 @@ describe('DiffFileHeader component', () => {
diffHasExpandedDiscussionsResultMock,
...Object.values(mockStoreConfig.modules.diffs.actions),
].forEach((mock) => mock.mockReset());
-
- wrapper.destroy();
});
const findHeader = () => wrapper.findComponent({ ref: 'header' });
@@ -87,7 +85,7 @@ describe('DiffFileHeader component', () => {
const findExternalLink = () => wrapper.findComponent({ ref: 'externalLink' });
const findReplacedFileButton = () => wrapper.findComponent({ ref: 'replacedFileButton' });
const findViewFileButton = () => wrapper.findComponent({ ref: 'viewButton' });
- const findCollapseIcon = () => wrapper.findComponent({ ref: 'collapseIcon' });
+ const findCollapseButton = () => wrapper.findComponent({ ref: 'collapseButton' });
const findEditButton = () => wrapper.findComponent({ ref: 'editButton' });
const findReviewFileCheckbox = () => wrapper.find("[data-testid='fileReviewCheckbox']");
@@ -113,7 +111,7 @@ describe('DiffFileHeader component', () => {
${'hidden'} | ${false}
`('collapse toggle is $visibility if collapsible is $collapsible', ({ collapsible }) => {
createComponent({ props: { collapsible } });
- expect(findCollapseIcon().exists()).toBe(collapsible);
+ expect(findCollapseButton().exists()).toBe(collapsible);
});
it.each`
@@ -122,7 +120,7 @@ describe('DiffFileHeader component', () => {
${false} | ${'chevron-right'}
`('collapse icon is $icon if expanded is $expanded', ({ icon, expanded }) => {
createComponent({ props: { expanded, collapsible: true } });
- expect(findCollapseIcon().props('name')).toBe(icon);
+ expect(findCollapseButton().props('icon')).toBe(icon);
});
it('when header is clicked emits toggleFile', async () => {
@@ -135,7 +133,7 @@ describe('DiffFileHeader component', () => {
it('when collapseIcon is clicked emits toggleFile', async () => {
createComponent({ props: { collapsible: true } });
- findCollapseIcon().vm.$emit('click', new Event('click'));
+ findCollapseButton().vm.$emit('click', new Event('click'));
await nextTick();
expect(wrapper.emitted().toggleFile).toBeDefined();
});
diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js
index c5b76551fcc..66ee4e955b8 100644
--- a/spec/frontend/diffs/components/diff_file_row_spec.js
+++ b/spec/frontend/diffs/components/diff_file_row_spec.js
@@ -13,10 +13,6 @@ describe('Diff File Row component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders file row component', () => {
const sharedProps = {
level: 4,
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index ccfc36f8f16..389b192a515 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -129,8 +129,6 @@ describe('DiffFile', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
axiosMock.restore();
});
@@ -168,6 +166,23 @@ describe('DiffFile', () => {
});
},
);
+
+ it('emits the "first file shown" and "files end" events when in File-by-File mode', async () => {
+ ({ wrapper, store } = createComponent({
+ file: getReadableFile(),
+ first: false,
+ last: false,
+ props: {
+ viewDiffsFileByFile: true,
+ },
+ }));
+
+ await nextTick();
+
+ expect(eventHub.$emit).toHaveBeenCalledTimes(2);
+ expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN);
+ expect(eventHub.$emit).toHaveBeenCalledWith(EVT_PERF_MARK_DIFF_FILES_END);
+ });
});
describe('after loading the diff', () => {
@@ -222,21 +237,10 @@ describe('DiffFile', () => {
describe('computed', () => {
describe('showLocalFileReviews', () => {
- let gon;
-
function setLoggedIn(bool) {
window.gon.current_user_id = bool;
}
- beforeAll(() => {
- gon = window.gon;
- window.gon = {};
- });
-
- afterEach(() => {
- window.gon = gon;
- });
-
it.each`
loggedIn | bool
${true} | ${true}
@@ -319,7 +323,7 @@ describe('DiffFile', () => {
markFileToBeRendered(store);
});
- it('should have the file content', async () => {
+ it('should have the file content', () => {
expect(wrapper.findComponent(DiffContentComponent).exists()).toBe(true);
});
@@ -329,7 +333,7 @@ describe('DiffFile', () => {
});
describe('toggle', () => {
- it('should update store state', async () => {
+ it('should update store state', () => {
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(() => {});
toggleFile(wrapper);
@@ -507,8 +511,6 @@ describe('DiffFile', () => {
});
it('loads collapsed file on mounted when single file mode is enabled', async () => {
- wrapper.destroy();
-
const file = {
...getReadableFile(),
load_collapsed_diff_url: '/diff_for_path',
@@ -527,10 +529,6 @@ describe('DiffFile', () => {
});
describe('merge conflicts', () => {
- beforeEach(() => {
- wrapper.destroy();
- });
-
it('does not render conflict alert', () => {
const file = {
...getReadableFile(),
diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
index f13988fc11f..5f2b1a81b91 100644
--- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
+++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
@@ -21,10 +21,6 @@ describe('DiffGutterAvatars', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when expanded', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index bd0e3455872..eb895bd9057 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
-import Autosave from '~/autosave';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import { createModules } from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
@@ -11,7 +10,6 @@ import { noteableDataMock } from 'jest/notes/mock_data';
import { getDiffFileMock } from '../mock_data/diff_file';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
-jest.mock('~/autosave');
describe('DiffLineNoteForm', () => {
let wrapper;
@@ -77,7 +75,6 @@ describe('DiffLineNoteForm', () => {
const findCommentForm = () => wrapper.findComponent(MultilineCommentForm);
beforeEach(() => {
- Autosave.mockClear();
createComponent();
});
@@ -100,19 +97,6 @@ describe('DiffLineNoteForm', () => {
});
});
- it('should init autosave', () => {
- // we're using shallow mount here so there's no element to pass to Autosave
- expect(Autosave).toHaveBeenCalledWith(undefined, [
- 'Note',
- 'Issue',
- 98,
- undefined,
- 'DiffNote',
- undefined,
- '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_2',
- ]);
- });
-
describe('when cancelling form', () => {
afterEach(() => {
confirmAction.mockReset();
@@ -146,7 +130,6 @@ describe('DiffLineNoteForm', () => {
await nextTick();
expect(getSelectedLine().hasForm).toBe(false);
- expect(Autosave.mock.instances[0].reset).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index a7a95ed2f35..356c7ef925a 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -89,10 +89,6 @@ describe('DiffRow', () => {
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
- window.gon = {};
showCommentForm.mockReset();
enterdragging.mockReset();
stopdragging.mockReset();
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 9bff6bd14f1..cfc80e61b30 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -14,7 +14,7 @@ describe('DiffView', () => {
const setSelectedCommentPosition = jest.fn();
const getDiffRow = (wrapper) => wrapper.findComponent(DiffRow).vm;
- const createWrapper = (props, provide = {}) => {
+ const createWrapper = (props) => {
Vue.use(Vuex);
const batchComments = {
@@ -48,7 +48,7 @@ describe('DiffView', () => {
...props,
};
const stubs = { DiffExpansionCell, DiffRow, DiffCommentCell, DraftNote };
- return shallowMount(DiffView, { propsData, store, stubs, provide });
+ return shallowMount(DiffView, { propsData, store, stubs });
};
it('does not render a diff-line component when there is no finding', () => {
@@ -56,24 +56,13 @@ describe('DiffView', () => {
expect(wrapper.findComponent(DiffLine).exists()).toBe(false);
});
- it('does render a diff-line component with the correct props when there is a finding & refactorCodeQualityInlineFindings flag is true', async () => {
- const wrapper = createWrapper(diffCodeQuality, {
- glFeatures: { refactorCodeQualityInlineFindings: true },
- });
+ it('does render a diff-line component with the correct props when there is a finding', async () => {
+ const wrapper = createWrapper(diffCodeQuality);
wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
await nextTick();
expect(wrapper.findComponent(DiffLine).props('line')).toBe(diffCodeQuality.diffLines[2]);
});
- it('does not render a diff-line component when there is a finding & refactorCodeQualityInlineFindings flag is false', async () => {
- const wrapper = createWrapper(diffCodeQuality, {
- glFeatures: { refactorCodeQualityInlineFindings: false },
- });
- wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2);
- await nextTick();
- expect(wrapper.findComponent(DiffLine).exists()).toBe(false);
- });
-
it.each`
type | side | container | sides | total
${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true }, right: { lineDrafts: [], renderDiscussion: true } }} | ${2}
diff --git a/spec/frontend/diffs/components/hidden_files_warning_spec.js b/spec/frontend/diffs/components/hidden_files_warning_spec.js
index bbd4f5faeec..9b748a3ed6f 100644
--- a/spec/frontend/diffs/components/hidden_files_warning_spec.js
+++ b/spec/frontend/diffs/components/hidden_files_warning_spec.js
@@ -23,10 +23,6 @@ describe('HiddenFilesWarning', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a correct plain diff URL', () => {
const plainDiffLink = wrapper.findAllComponents(GlButton).at(0);
@@ -41,7 +37,9 @@ describe('HiddenFilesWarning', () => {
it('has a correct visible/total files text', () => {
expect(wrapper.text()).toContain(
- __('To preserve performance only 5 of 10 files are displayed.'),
+ __(
+ 'For a faster browsing experience, only 5 of 10 files are shown. Download one of the files below to see all changes',
+ ),
);
});
});
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index ccf942bdcef..18901781587 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -36,10 +36,6 @@ describe('Diffs image diff overlay component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders comment badges', () => {
createComponent();
diff --git a/spec/frontend/diffs/components/merge_conflict_warning_spec.js b/spec/frontend/diffs/components/merge_conflict_warning_spec.js
index 4e47249f5b4..715912b361f 100644
--- a/spec/frontend/diffs/components/merge_conflict_warning_spec.js
+++ b/spec/frontend/diffs/components/merge_conflict_warning_spec.js
@@ -25,10 +25,6 @@ describe('MergeConflictWarning', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
present | resolutionPath
${false} | ${''}
diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js
index dbfe9770e07..e637b1dd43d 100644
--- a/spec/frontend/diffs/components/no_changes_spec.js
+++ b/spec/frontend/diffs/components/no_changes_spec.js
@@ -34,11 +34,6 @@ describe('Diff no changes empty state', () => {
store.state.diffs.mergeRequestDiffs = diffsMockData;
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findMessage = () => wrapper.find('[data-testid="no-changes-message"]');
it('prevents XSS', () => {
diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js
index 2ec11ba86fd..3d2bbe43746 100644
--- a/spec/frontend/diffs/components/settings_dropdown_spec.js
+++ b/spec/frontend/diffs/components/settings_dropdown_spec.js
@@ -39,7 +39,6 @@ describe('Diff settings dropdown component', () => {
afterEach(() => {
store.dispatch.mockRestore();
- wrapper.destroy();
});
describe('tree view buttons', () => {
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
new file mode 100644
index 00000000000..e82687aa146
--- /dev/null
+++ b/spec/frontend/diffs/components/shared/__snapshots__/findings_drawer_spec.js.snap
@@ -0,0 +1,126 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FindingsDrawer matches the snapshot 1`] = `
+<gl-drawer-stub
+ class="findings-drawer"
+ headerheight=""
+ open="true"
+ variant="default"
+ zindex="252"
+>
+ <h2
+ class="gl-font-size-h2 gl-mt-0 gl-mb-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.
+
+ </h2>
+ <ul
+ class="gl-list-style-none gl-border-b-initial gl-mb-0 gl-pb-0!"
+ >
+ <li
+ class="gl-mb-4"
+ data-testid="findings-drawer-severity"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Severity:
+ </span>
+
+ <gl-icon-stub
+ class="codequality-severity-icon gl-text-orange-300"
+ data-testid="findings-drawer-severity-icon"
+ name="severity-low"
+ size="12"
+ />
+
+
+ minor
+
+ </li>
+
+ <li
+ class="gl-mb-4"
+ data-testid="findings-drawer-engine"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Engine:
+ </span>
+
+ testengine name
+
+ </li>
+
+ <li
+ class="gl-mb-4"
+ data-testid="findings-drawer-category"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+ Category:
+ </span>
+
+ 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"
+ >
+ Other locations:
+ </span>
+
+ <ul
+ class="gl-pl-6"
+ >
+ <li
+ class="gl-mb-1"
+ >
+ <gl-link-stub
+ data-testid="findings-drawer-other-locations-link"
+ href="http://testlink.com"
+ >
+ testpath
+ </gl-link-stub>
+ </li>
+ <li
+ class="gl-mb-1"
+ >
+ <gl-link-stub
+ data-testid="findings-drawer-other-locations-link"
+ href="http://testlink.com"
+ >
+ testpath 1
+ </gl-link-stub>
+ </li>
+ <li
+ class="gl-mb-1"
+ >
+ <gl-link-stub
+ data-testid="findings-drawer-other-locations-link"
+ href="http://testlink.com"
+ >
+ testpath2
+ </gl-link-stub>
+ </li>
+ </ul>
+ </li>
+ </ul>
+
+ <span
+ class="drawer-body gl-display-block gl-px-3 gl-py-0!"
+ data-testid="findings-drawer-body"
+ >
+ Duplicated Code Duplicated code
+ </span>
+</gl-drawer-stub>
+`;
diff --git a/spec/frontend/diffs/components/shared/findings_drawer_spec.js b/spec/frontend/diffs/components/shared/findings_drawer_spec.js
new file mode 100644
index 00000000000..0af6e0f0e96
--- /dev/null
+++ b/spec/frontend/diffs/components/shared/findings_drawer_spec.js
@@ -0,0 +1,19 @@
+import FindingsDrawer from '~/diffs/components/shared/findings_drawer.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import mockFinding from '../../mock_data/findings_drawer';
+
+let wrapper;
+describe('FindingsDrawer', () => {
+ const createWrapper = () => {
+ return shallowMountExtended(FindingsDrawer, {
+ propsData: {
+ drawer: mockFinding,
+ },
+ });
+ };
+
+ it('matches the snapshot', () => {
+ wrapper = createWrapper();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 1656eaf8ba0..87c638d065a 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -1,20 +1,36 @@
-import { shallowMount, mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TreeList from '~/diffs/components/tree_list.vue';
import createStore from '~/diffs/store/modules';
-import FileTree from '~/vue_shared/components/file_tree.vue';
+import DiffFileRow from '~/diffs/components//diff_file_row.vue';
+import { stubComponent } from 'helpers/stub_component';
describe('Diffs tree list component', () => {
let wrapper;
let store;
- const getFileRows = () => wrapper.findAll('.file-row');
+ const getScroller = () => wrapper.findComponent({ name: 'RecycleScroller' });
+ const getFileRow = () => wrapper.findComponent(DiffFileRow);
Vue.use(Vuex);
- const createComponent = (mountFn = mount) => {
- wrapper = mountFn(TreeList, {
+ const createComponent = () => {
+ wrapper = shallowMount(TreeList, {
store,
propsData: { hideFileStats: false },
+ stubs: {
+ // eslint will fail if we import the real component
+ RecycleScroller: stubComponent(
+ {
+ name: 'RecycleScroller',
+ props: {
+ items: null,
+ },
+ },
+ {
+ template: '<div><slot :item="{ tree: [] }"></slot></div>',
+ },
+ ),
+ },
});
};
@@ -80,10 +96,6 @@ describe('Diffs tree list component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -101,26 +113,32 @@ describe('Diffs tree list component', () => {
});
describe('search by file extension', () => {
+ it('hides scroller for no matches', async () => {
+ wrapper.find('[data-testid="diff-tree-search"]').setValue('*.md');
+
+ await nextTick();
+
+ expect(getScroller().exists()).toBe(false);
+ expect(wrapper.text()).toContain('No files found');
+ });
+
it.each`
extension | itemSize
- ${'*.md'} | ${0}
- ${'*.js'} | ${1}
- ${'index.js'} | ${1}
- ${'app/*.js'} | ${1}
- ${'*.js, *.rb'} | ${2}
+ ${'*.js'} | ${2}
+ ${'index.js'} | ${2}
+ ${'app/*.js'} | ${2}
+ ${'*.js, *.rb'} | ${3}
`('returns $itemSize item for $extension', async ({ extension, itemSize }) => {
wrapper.find('[data-testid="diff-tree-search"]').setValue(extension);
await nextTick();
- expect(getFileRows()).toHaveLength(itemSize);
+ expect(getScroller().props('items')).toHaveLength(itemSize);
});
});
it('renders tree', () => {
- expect(getFileRows()).toHaveLength(2);
- expect(getFileRows().at(0).html()).toContain('index.js');
- expect(getFileRows().at(1).html()).toContain('app');
+ expect(getScroller().props('items')).toHaveLength(2);
});
it('hides file stats', async () => {
@@ -133,33 +151,16 @@ describe('Diffs tree list component', () => {
it('calls toggleTreeOpen when clicking folder', () => {
jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
- getFileRows().at(1).trigger('click');
+ getFileRow().vm.$emit('toggleTreeOpen', 'app');
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app');
});
- it('calls scrollToFile when clicking blob', () => {
- jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
-
- wrapper.find('.file-row').trigger('click');
-
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', {
- path: 'app/index.js',
- });
- });
-
- it('renders as file list when renderTreeList is false', async () => {
- wrapper.vm.$store.state.diffs.renderTreeList = false;
-
- await nextTick();
- expect(getFileRows()).toHaveLength(2);
- });
-
- it('renders file paths when renderTreeList is false', async () => {
+ it('renders when renderTreeList is false', async () => {
wrapper.vm.$store.state.diffs.renderTreeList = false;
await nextTick();
- expect(wrapper.find('.file-row').html()).toContain('index.js');
+ expect(getScroller().props('items')).toHaveLength(3);
});
});
@@ -172,12 +173,10 @@ describe('Diffs tree list component', () => {
});
it('passes the viewedDiffFileIds to the FileTree', async () => {
- createComponent(shallowMount);
+ createComponent();
await nextTick();
- // Have to use $attrs['viewed-files'] because we are passing down an object
- // and attributes('') stringifies values (e.g. [object])...
- expect(wrapper.findComponent(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds);
+ expect(wrapper.findComponent(DiffFileRow).props('viewedFiles')).toBe(viewedDiffFileIds);
});
});
});
diff --git a/spec/frontend/diffs/create_diffs_store.js b/spec/frontend/diffs/create_diffs_store.js
index 307ebdaa4ac..92f38858ca5 100644
--- a/spec/frontend/diffs/create_diffs_store.js
+++ b/spec/frontend/diffs/create_diffs_store.js
@@ -3,6 +3,7 @@ import Vuex from 'vuex';
import batchCommentsModule from '~/batch_comments/stores/modules/batch_comments';
import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
+import findingsDrawer from '~/mr_notes/stores/drawer';
Vue.use(Vuex);
@@ -18,6 +19,7 @@ export default function createDiffsStore() {
diffs: diffsModule(),
notes: notesModule(),
batchComments: batchCommentsModule(),
+ findingsDrawer: findingsDrawer(),
},
});
}
diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js
index 7558592f6a4..29f16da8d89 100644
--- a/spec/frontend/diffs/mock_data/diff_code_quality.js
+++ b/spec/frontend/diffs/mock_data/diff_code_quality.js
@@ -24,6 +24,11 @@ export const multipleFindingsArr = [
description: 'mocked blocker Issue',
line: 3,
},
+ {
+ severity: 'unknown',
+ description: 'mocked unknown Issue',
+ line: 3,
+ },
];
export const fiveFindings = {
diff --git a/spec/frontend/diffs/mock_data/findings_drawer.js b/spec/frontend/diffs/mock_data/findings_drawer.js
new file mode 100644
index 00000000000..d7e7e957c83
--- /dev/null
+++ b/spec/frontend/diffs/mock_data/findings_drawer.js
@@ -0,0 +1,21 @@
+export default {
+ line: 7,
+ description:
+ "Unused method argument - `c`. If it's necessary, use `_` or `_c` as an argument name to indicate that it won't be used.",
+ severity: 'minor',
+ engineName: 'testengine name',
+ categories: ['testcategory 1', 'testcategory 2'],
+ content: {
+ body: 'Duplicated Code Duplicated code',
+ },
+ location: {
+ path: 'workhorse/config_test.go',
+ lines: { begin: 221, end: 284 },
+ },
+ otherLocations: [
+ { path: 'testpath', href: 'http://testlink.com' },
+ { path: 'testpath 1', href: 'http://testlink.com' },
+ { path: 'testpath2', href: 'http://testlink.com' },
+ ],
+ type: 'issue',
+};
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 78765204322..f883aea764f 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import Cookies from '~/lib/utils/cookies';
+import waitForPromises from 'helpers/wait_for_promises';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
@@ -8,12 +9,14 @@ import {
DIFF_VIEW_COOKIE_NAME,
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
+ EVT_MR_PREPARED,
} from '~/diffs/constants';
+import { LOAD_SINGLE_DIFF_FAILED } from '~/diffs/i18n';
import * as diffActions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
import * as treeWorkerUtils from '~/diffs/utils/tree_worker_utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import {
@@ -24,9 +27,15 @@ import {
} from '~/lib/utils/http_status';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
+import diffsEventHub from '~/diffs/event_hub';
import { diffMetadata } from '../mock_data/diff_metadata';
-jest.mock('~/flash');
+jest.mock('~/alert');
+
+jest.mock('~/lib/utils/secret_detection', () => ({
+ confirmSensitiveAction: jest.fn(() => Promise.resolve(false)),
+ containsSensitiveToken: jest.requireActual('~/lib/utils/secret_detection').containsSensitiveToken,
+}));
describe('DiffsStoreActions', () => {
let mock;
@@ -69,6 +78,7 @@ describe('DiffsStoreActions', () => {
const endpoint = '/diffs/set/endpoint';
const endpointMetadata = '/diffs/set/endpoint/metadata';
const endpointBatch = '/diffs/set/endpoint/batch';
+ const endpointDiffForPath = '/diffs/set/endpoint/path';
const endpointCoverage = '/diffs/set/coverage_reports';
const projectPath = '/root/project';
const dismissEndpoint = '/-/user_callouts';
@@ -83,6 +93,7 @@ describe('DiffsStoreActions', () => {
{
endpoint,
endpointBatch,
+ endpointDiffForPath,
endpointMetadata,
endpointCoverage,
projectPath,
@@ -93,6 +104,7 @@ describe('DiffsStoreActions', () => {
{
endpoint: '',
endpointBatch: '',
+ endpointDiffForPath: '',
endpointMetadata: '',
endpointCoverage: '',
projectPath: '',
@@ -106,6 +118,7 @@ describe('DiffsStoreActions', () => {
endpoint,
endpointMetadata,
endpointBatch,
+ endpointDiffForPath,
endpointCoverage,
projectPath,
dismissEndpoint,
@@ -131,6 +144,177 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('fetchFileByFile', () => {
+ beforeEach(() => {
+ window.location.hash = 'e334a2a10f036c00151a04cea7938a5d4213a818';
+ });
+
+ it('should do nothing if there is no tree entry for the file ID', () => {
+ return testAction(diffActions.fetchFileByFile, {}, { flatBlobsList: [] }, [], []);
+ });
+
+ it('should do nothing if the tree entry for the file ID has already been marked as loaded', () => {
+ return testAction(
+ diffActions.fetchFileByFile,
+ {},
+ {
+ flatBlobsList: [
+ { fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818', diffLoaded: true },
+ ],
+ },
+ [],
+ [],
+ );
+ });
+
+ describe('when a tree entry exists for the file, but it has not been marked as loaded', () => {
+ let state;
+ let getters;
+ let commit;
+ let hubSpy;
+ const defaultParams = {
+ old_path: 'old/123',
+ new_path: 'new/123',
+ w: '1',
+ view: 'inline',
+ };
+ const endpointDiffForPath = '/diffs/set/endpoint/path';
+ const diffForPath = mergeUrlParams(defaultParams, endpointDiffForPath);
+ const treeEntry = {
+ fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818',
+ filePaths: { old: 'old/123', new: 'new/123' },
+ };
+ const fileResult = {
+ diff_files: [{ file_hash: 'e334a2a10f036c00151a04cea7938a5d4213a818' }],
+ };
+
+ beforeEach(() => {
+ commit = jest.fn();
+ state = {
+ endpointDiffForPath,
+ diffFiles: [],
+ };
+ getters = {
+ flatBlobsList: [treeEntry],
+ getDiffFileByHash(hash) {
+ return state.diffFiles?.find((entry) => entry.file_hash === hash);
+ },
+ };
+ hubSpy = jest.spyOn(diffsEventHub, '$emit');
+ });
+
+ it('does nothing if the file already exists in the loaded diff files', () => {
+ state.diffFiles = fileResult.diff_files;
+
+ return testAction(diffActions.fetchFileByFile, state, getters, [], []);
+ });
+
+ it('does some standard work every time', async () => {
+ mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
+
+ await diffActions.fetchFileByFile({ state, getters, commit });
+
+ expect(commit).toHaveBeenCalledWith(types.SET_BATCH_LOADING_STATE, 'loading');
+ expect(commit).toHaveBeenCalledWith(types.SET_RETRIEVING_BATCHES, true);
+
+ // wait for the mocked network request to return and start processing the .then
+ await waitForPromises();
+
+ expect(commit).toHaveBeenCalledWith(types.SET_DIFF_DATA_BATCH, fileResult);
+ expect(commit).toHaveBeenCalledWith(types.SET_BATCH_LOADING_STATE, 'loaded');
+
+ expect(hubSpy).toHaveBeenCalledWith('diffFilesModified');
+ });
+
+ it.each`
+ urlHash | diffFiles | expected
+ ${treeEntry.fileHash} | ${[]} | ${''}
+ ${'abcdef1234567890'} | ${fileResult.diff_files} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ `(
+ "sets the current file to the first diff file ('$id') if it's not a note hash and there isn't a current ID set",
+ async ({ urlHash, diffFiles, expected }) => {
+ window.location.hash = urlHash;
+ mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
+ state.diffFiles = diffFiles;
+
+ await diffActions.fetchFileByFile({ state, getters, commit });
+
+ // wait for the mocked network request to return and start processing the .then
+ await waitForPromises();
+
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, expected);
+ },
+ );
+
+ it('should fetch data without commit ID', async () => {
+ getters.commitId = null;
+ mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
+
+ await diffActions.fetchFileByFile({ state, getters, commit });
+
+ // wait for the mocked network request to return and start processing the .then
+ await waitForPromises();
+
+ // This tests that commit_id is NOT added, if there isn't one in the store
+ expect(mock.history.get[0].url).toEqual(diffForPath);
+ });
+
+ it('should fetch data with commit ID', async () => {
+ const finalPath = mergeUrlParams(
+ { ...defaultParams, commit_id: '123' },
+ endpointDiffForPath,
+ );
+
+ getters.commitId = '123';
+ mock.onGet(finalPath).reply(HTTP_STATUS_OK, fileResult);
+
+ await diffActions.fetchFileByFile({ state, getters, commit });
+
+ // wait for the mocked network request to return and start processing the .then
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toEqual(finalPath);
+ });
+
+ describe('version parameters', () => {
+ const diffId = '4';
+ const startSha = 'abc';
+ const pathRoot = 'a/a/-/merge_requests/1';
+
+ it('fetches the data when there is no mergeRequestDiff', async () => {
+ diffActions.fetchFileByFile({ state, getters, commit });
+
+ // wait for the mocked network request to return and start processing the .then
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toEqual(diffForPath);
+ });
+
+ it.each`
+ desc | versionPath | start_sha | diff_id
+ ${'no additional version information'} | ${`${pathRoot}?search=terms`} | ${undefined} | ${undefined}
+ ${'the diff_id'} | ${`${pathRoot}?diff_id=${diffId}`} | ${undefined} | ${diffId}
+ ${'the start_sha'} | ${`${pathRoot}?start_sha=${startSha}`} | ${startSha} | ${undefined}
+ ${'all available version information'} | ${`${pathRoot}?diff_id=${diffId}&start_sha=${startSha}`} | ${startSha} | ${diffId}
+ `('fetches the data and includes $desc', async ({ versionPath, start_sha, diff_id }) => {
+ const finalPath = mergeUrlParams(
+ { ...defaultParams, diff_id, start_sha },
+ endpointDiffForPath,
+ );
+ state.mergeRequestDiff = { version_path: versionPath };
+ mock.onGet(finalPath).reply(HTTP_STATUS_OK, fileResult);
+
+ diffActions.fetchFileByFile({ state, getters, commit });
+
+ // wait for the mocked network request to return and start processing the .then
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toEqual(finalPath);
+ });
+ });
+ });
+ });
+
describe('fetchDiffFilesBatch', () => {
it('should fetch batch diff files', () => {
const endpointBatch = '/fetch/diffs_batch';
@@ -213,36 +397,63 @@ describe('DiffsStoreActions', () => {
);
});
- it('should show a warning on 404 reponse', async () => {
- mock.onGet(endpointMetadata).reply(HTTP_STATUS_NOT_FOUND);
+ describe('on a 404 response', () => {
+ let dismissAlert;
- await testAction(
- diffActions.fetchDiffFilesMeta,
- {},
- { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
- [{ type: types.SET_LOADING, payload: true }],
- [],
- );
+ beforeAll(() => {
+ dismissAlert = jest.fn();
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringMatching(
- 'Building your merge request. Wait a few moments, then refresh this page.',
- ),
- variant: 'warning',
+ mock.onGet(endpointMetadata).reply(HTTP_STATUS_NOT_FOUND);
+ createAlert.mockImplementation(() => ({ dismiss: dismissAlert }));
+ });
+
+ it('should show a warning', async () => {
+ await testAction(
+ diffActions.fetchDiffFilesMeta,
+ {},
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
+ [{ type: types.SET_LOADING, payload: true }],
+ [],
+ );
+
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.stringMatching(
+ 'Building your merge request… This page will update when the build is complete.',
+ ),
+ variant: 'warning',
+ });
+ });
+
+ it("should attempt to close the alert if the MR reports that it's been prepared", async () => {
+ await testAction(
+ diffActions.fetchDiffFilesMeta,
+ {},
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
+ [{ type: types.SET_LOADING, payload: true }],
+ [],
+ );
+
+ diffsEventHub.$emit(EVT_MR_PREPARED);
+
+ expect(dismissAlert).toHaveBeenCalled();
});
});
it('should show no warning on any other status code', async () => {
mock.onGet(endpointMetadata).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- await testAction(
- diffActions.fetchDiffFilesMeta,
- {},
- { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
- [{ type: types.SET_LOADING, payload: true }],
- [],
- );
+ try {
+ await testAction(
+ diffActions.fetchDiffFilesMeta,
+ {},
+ { endpointMetadata, diffViewType: 'inline', showWhitespace: true },
+ [{ type: types.SET_LOADING, payload: true }],
+ [],
+ );
+ } catch (error) {
+ expect(error.response.status).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ }
expect(createAlert).not.toHaveBeenCalled();
});
@@ -265,7 +476,7 @@ describe('DiffsStoreActions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mock.onGet(endpointCoverage).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
@@ -389,7 +600,7 @@ describe('DiffsStoreActions', () => {
return testAction(
diffActions.assignDiscussionsToDiff,
[],
- { diffFiles: [] },
+ { diffFiles: [], flatBlobsList: [] },
[],
[{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
);
@@ -810,31 +1021,32 @@ describe('DiffsStoreActions', () => {
});
describe('saveDiffDiscussion', () => {
- it('dispatches actions', () => {
- const commitId = 'something';
- const formData = {
- diffFile: getDiffFileMock(),
- noteableData: {},
- };
- const note = {};
- const state = {
- commit: {
- id: commitId,
- },
- };
- const dispatch = jest.fn((name) => {
- switch (name) {
- case 'saveNote':
- return Promise.resolve({
- discussion: 'test',
- });
- case 'updateDiscussion':
- return Promise.resolve('discussion');
- default:
- return Promise.resolve({});
- }
- });
+ const dispatch = jest.fn((name) => {
+ switch (name) {
+ case 'saveNote':
+ return Promise.resolve({
+ discussion: 'test',
+ });
+ case 'updateDiscussion':
+ return Promise.resolve('discussion');
+ default:
+ return Promise.resolve({});
+ }
+ });
+
+ const commitId = 'something';
+ const formData = {
+ diffFile: getDiffFileMock(),
+ noteableData: {},
+ };
+ const note = {};
+ const state = {
+ commit: {
+ id: commitId,
+ },
+ };
+ it('dispatches actions', () => {
return diffActions.saveDiffDiscussion({ state, dispatch }, { note, formData }).then(() => {
expect(dispatch).toHaveBeenCalledTimes(5);
expect(dispatch).toHaveBeenNthCalledWith(1, 'saveNote', expect.any(Object), {
@@ -848,6 +1060,16 @@ describe('DiffsStoreActions', () => {
expect(dispatch).toHaveBeenNthCalledWith(3, 'assignDiscussionsToDiff', ['discussion']);
});
});
+
+ it('should not add note with sensitive token', async () => {
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+
+ await diffActions.saveDiffDiscussion(
+ { state, dispatch },
+ { note: sensitiveMessage, formData },
+ );
+ expect(dispatch).not.toHaveBeenCalled();
+ });
});
describe('toggleTreeOpen', () => {
@@ -862,6 +1084,104 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('goToFile', () => {
+ const getters = {};
+ const file = { path: 'path' };
+ const fileHash = 'test';
+ let state;
+ let dispatch;
+ let commit;
+
+ beforeEach(() => {
+ getters.isTreePathLoaded = () => false;
+ state = {
+ viewDiffsFileByFile: true,
+ treeEntries: {
+ path: {
+ fileHash,
+ },
+ },
+ };
+ commit = jest.fn();
+ dispatch = jest.fn().mockResolvedValue();
+ });
+
+ it('immediately defers to scrollToFile if the app is not in file-by-file mode', () => {
+ state.viewDiffsFileByFile = false;
+
+ diffActions.goToFile({ state, dispatch }, file);
+
+ expect(dispatch).toHaveBeenCalledWith('scrollToFile', file);
+ });
+
+ describe('when the app is in fileByFile mode', () => {
+ describe('when the singleFileFileByFile feature flag is enabled', () => {
+ it('commits SET_CURRENT_DIFF_FILE', () => {
+ diffActions.goToFile(
+ { state, commit, dispatch, getters },
+ { path: file.path, singleFile: true },
+ );
+
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
+ });
+
+ it('does nothing more if the path has already been loaded', () => {
+ getters.isTreePathLoaded = () => true;
+
+ diffActions.goToFile(
+ { state, dispatch, getters, commit },
+ { path: file.path, singleFile: true },
+ );
+
+ expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, fileHash);
+ expect(dispatch).toHaveBeenCalledTimes(0);
+ });
+
+ describe('when the tree entry has not been loaded', () => {
+ it('updates location hash', () => {
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
+
+ expect(document.location.hash).toBe('#test');
+ });
+
+ it('loads the file and then scrolls to it', async () => {
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
+
+ // Wait for the fetchFileByFile dispatch to return, to trigger scrollToFile
+ await waitForPromises();
+
+ expect(dispatch).toHaveBeenCalledWith('fetchFileByFile');
+ expect(dispatch).toHaveBeenCalledWith('scrollToFile', file);
+ expect(dispatch).toHaveBeenCalledTimes(2);
+ });
+
+ it('shows an alert when there was an error fetching the file', async () => {
+ dispatch = jest.fn().mockRejectedValue();
+
+ diffActions.goToFile(
+ { state, commit, getters, dispatch },
+ { path: file.path, singleFile: true },
+ );
+
+ // Wait for the fetchFileByFile dispatch to return, to trigger the catch
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.stringMatching(LOAD_SINGLE_DIFF_FAILED),
+ });
+ });
+ });
+ });
+ });
+ });
+
describe('scrollToFile', () => {
let commit;
const getters = { isVirtualScrollingEnabled: false };
@@ -1007,20 +1327,14 @@ describe('DiffsStoreActions', () => {
describe('setShowWhitespace', () => {
const endpointUpdateUser = 'user/prefs';
let putSpy;
- let gon;
beforeEach(() => {
putSpy = jest.spyOn(axios, 'put');
- gon = window.gon;
mock.onPut(endpointUpdateUser).reply(HTTP_STATUS_OK, {});
jest.spyOn(eventHub, '$emit').mockImplementation();
});
- afterEach(() => {
- window.gon = gon;
- });
-
it('commits SET_SHOW_WHITESPACE', () => {
return testAction(
diffActions.setShowWhitespace,
@@ -1390,42 +1704,89 @@ describe('DiffsStoreActions', () => {
);
});
+ describe('rereadNoteHash', () => {
+ beforeEach(() => {
+ window.location.hash = 'note_123';
+ });
+
+ it('dispatches setCurrentDiffFileIdFromNote if the hash is a note URL', () => {
+ window.location.hash = 'note_123';
+
+ return testAction(
+ diffActions.rereadNoteHash,
+ {},
+ {},
+ [],
+ [{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
+ );
+ });
+
+ it('dispatches fetchFileByFile if the app is in fileByFile mode', () => {
+ window.location.hash = 'note_123';
+
+ return testAction(
+ diffActions.rereadNoteHash,
+ {},
+ { viewDiffsFileByFile: true },
+ [],
+ [{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }, { type: 'fetchFileByFile' }],
+ );
+ });
+
+ it('does not try to fetch the diff file if the app is not in fileByFile mode', () => {
+ window.location.hash = 'note_123';
+
+ return testAction(
+ diffActions.rereadNoteHash,
+ {},
+ { viewDiffsFileByFile: false },
+ [],
+ [{ type: 'setCurrentDiffFileIdFromNote', payload: '123' }],
+ );
+ });
+
+ it('does nothing if the hash is not a note URL', () => {
+ window.location.hash = 'abcdef1234567890';
+
+ return testAction(diffActions.rereadNoteHash, {}, {}, [], []);
+ });
+ });
+
describe('setCurrentDiffFileIdFromNote', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
const commit = jest.fn();
- const state = { diffFiles: [{ file_hash: '123' }] };
+ const getters = { flatBlobsList: [{ fileHash: '123' }] };
const rootGetters = {
getDiscussion: () => ({ diff_file: { file_hash: '123' } }),
notesById: { 1: { discussion_id: '2' } },
};
- diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, getters, rootGetters }, '1');
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, '123');
});
it('does not commit SET_CURRENT_DIFF_FILE when discussion has no diff_file', () => {
const commit = jest.fn();
- const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
getDiscussion: () => ({ id: '1' }),
notesById: { 1: { discussion_id: '2' } },
};
- diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, rootGetters }, '1');
expect(commit).not.toHaveBeenCalled();
});
it('does not commit SET_CURRENT_DIFF_FILE when diff file does not exist', () => {
const commit = jest.fn();
- const state = { diffFiles: [{ file_hash: '123' }] };
+ const getters = { flatBlobsList: [{ fileHash: '123' }] };
const rootGetters = {
getDiscussion: () => ({ diff_file: { file_hash: '124' } }),
notesById: { 1: { discussion_id: '2' } },
};
- diffActions.setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+ diffActions.setCurrentDiffFileIdFromNote({ commit, getters, rootGetters }, '1');
expect(commit).not.toHaveBeenCalled();
});
@@ -1435,12 +1796,22 @@ describe('DiffsStoreActions', () => {
it('commits SET_CURRENT_DIFF_FILE', () => {
return testAction(
diffActions.navigateToDiffFileIndex,
- 0,
- { diffFiles: [{ file_hash: '123' }] },
+ { index: 0, singleFile: false },
+ { flatBlobsList: [{ fileHash: '123' }] },
[{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
[],
);
});
+
+ it('dispatches the fetchFileByFile action when the state value viewDiffsFileByFile is true and the single-file file-by-file feature flag is enabled', () => {
+ return testAction(
+ diffActions.navigateToDiffFileIndex,
+ { index: 0, singleFile: true },
+ { viewDiffsFileByFile: true, flatBlobsList: [{ fileHash: '123' }] },
+ [{ type: types.SET_CURRENT_DIFF_FILE, payload: '123' }],
+ [{ type: 'fetchFileByFile' }],
+ );
+ });
});
describe('setFileByFile', () => {
diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js
index 2e3a66d5b01..ed7b6699e2c 100644
--- a/spec/frontend/diffs/store/getters_spec.js
+++ b/spec/frontend/diffs/store/getters_spec.js
@@ -288,6 +288,19 @@ describe('Diffs Module Getters', () => {
});
});
+ describe('isTreePathLoaded', () => {
+ it.each`
+ desc | loaded | path | bool
+ ${'the file exists and has been loaded'} | ${true} | ${'path/tofile'} | ${true}
+ ${'the file exists and has not been loaded'} | ${false} | ${'path/tofile'} | ${false}
+ ${'the file does not exist'} | ${false} | ${'tofile/path'} | ${false}
+ `('returns $bool when $desc', ({ loaded, path, bool }) => {
+ localState.treeEntries['path/tofile'] = { diffLoaded: loaded };
+
+ expect(getters.isTreePathLoaded(localState)(path)).toBe(bool);
+ });
+ });
+
describe('allBlobs', () => {
it('returns an array of blobs', () => {
localState.treeEntries = {
@@ -328,7 +341,11 @@ describe('Diffs Module Getters', () => {
describe('currentDiffIndex', () => {
it('returns index of currently selected diff in diffList', () => {
- localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
+ localState.treeEntries = [
+ { type: 'blob', fileHash: '111' },
+ { type: 'blob', fileHash: '222' },
+ { type: 'blob', fileHash: '333' },
+ ];
localState.currentDiffFileId = '222';
expect(getters.currentDiffIndex(localState)).toEqual(1);
@@ -339,7 +356,11 @@ describe('Diffs Module Getters', () => {
});
it('returns 0 if no diff is selected yet or diff is not found', () => {
- localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
+ localState.treeEntries = [
+ { type: 'blob', fileHash: '111' },
+ { type: 'blob', fileHash: '222' },
+ { type: 'blob', fileHash: '333' },
+ ];
localState.currentDiffFileId = '';
expect(getters.currentDiffIndex(localState)).toEqual(0);
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index 031e4fe2be2..ed8d7397bbc 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -93,15 +93,20 @@ describe('DiffsStoreMutations', () => {
describe('SET_DIFF_DATA_BATCH_DATA', () => {
it('should set diff data batch type properly', () => {
- const state = { diffFiles: [] };
+ const mockFile = getDiffFileMock();
+ const state = {
+ diffFiles: [],
+ treeEntries: { [mockFile.file_path]: { fileHash: mockFile.file_hash } },
+ };
const diffMock = {
- diff_files: [getDiffFileMock()],
+ diff_files: [mockFile],
};
mutations[types.SET_DIFF_DATA_BATCH](state, diffMock);
expect(state.diffFiles[0].renderIt).toEqual(true);
expect(state.diffFiles[0].collapsed).toEqual(false);
+ expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true);
});
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index b5c44b084d8..4760a8b7166 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -892,4 +892,61 @@ describe('DiffsStoreUtils', () => {
expect(files[6].right).toBeNull();
});
});
+
+ describe('isUrlHashNoteLink', () => {
+ it.each`
+ input | bool
+ ${'#note_12345'} | ${true}
+ ${'#12345'} | ${false}
+ ${'note_12345'} | ${true}
+ ${'12345'} | ${false}
+ `('returns $bool for $input', ({ bool, input }) => {
+ expect(utils.isUrlHashNoteLink(input)).toBe(bool);
+ });
+ });
+
+ describe('isUrlHashFileHeader', () => {
+ it.each`
+ input | bool
+ ${'#diff-content-12345'} | ${true}
+ ${'#12345'} | ${false}
+ ${'diff-content-12345'} | ${true}
+ ${'12345'} | ${false}
+ `('returns $bool for $input', ({ bool, input }) => {
+ expect(utils.isUrlHashFileHeader(input)).toBe(bool);
+ });
+ });
+
+ describe('parseUrlHashAsFileHash', () => {
+ it.each`
+ input | currentDiffId | resultId
+ ${'#note_12345'} | ${'1A2B3C'} | ${'1A2B3C'}
+ ${'note_12345'} | ${'1A2B3C'} | ${'1A2B3C'}
+ ${'#note_12345'} | ${undefined} | ${null}
+ ${'note_12345'} | ${undefined} | ${null}
+ ${'#diff-content-12345'} | ${undefined} | ${'12345'}
+ ${'diff-content-12345'} | ${undefined} | ${'12345'}
+ ${'#diff-content-12345'} | ${'98765'} | ${'12345'}
+ ${'diff-content-12345'} | ${'98765'} | ${'12345'}
+ ${'#e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ ${'e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ ${'#Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null}
+ ${'Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null}
+ `('returns $resultId for $input and $currentDiffId', ({ input, currentDiffId, resultId }) => {
+ expect(utils.parseUrlHashAsFileHash(input, currentDiffId)).toBe(resultId);
+ });
+ });
+
+ describe('markTreeEntriesLoaded', () => {
+ it.each`
+ desc | entries | loaded | outcome
+ ${'marks an existing entry as loaded'} | ${{ abc: {} }} | ${[{ new_path: 'abc' }]} | ${{ abc: { diffLoaded: true } }}
+ ${'does nothing if the new file is not found in the tree entries'} | ${{ abc: {} }} | ${[{ new_path: 'def' }]} | ${{ abc: {} }}
+ ${'leaves entries unmodified if they are not in the loaded files'} | ${{ abc: {}, def: { diffLoaded: true }, ghi: {} }} | ${[{ new_path: 'ghi' }]} | ${{ abc: {}, def: { diffLoaded: true }, ghi: { diffLoaded: true } }}
+ `('$desc', ({ entries, loaded, outcome }) => {
+ expect(utils.markTreeEntriesLoaded({ priorEntries: entries, loadedFiles: loaded })).toEqual(
+ outcome,
+ );
+ });
+ });
});
diff --git a/spec/frontend/diffs/utils/merge_request_spec.js b/spec/frontend/diffs/utils/merge_request_spec.js
index c070e8c004d..11c0efb9a9c 100644
--- a/spec/frontend/diffs/utils/merge_request_spec.js
+++ b/spec/frontend/diffs/utils/merge_request_spec.js
@@ -1,10 +1,14 @@
-import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
+import {
+ updateChangesTabCount,
+ getDerivedMergeRequestInformation,
+} from '~/diffs/utils/merge_request';
+import { ZERO_CHANGES_ALT_DISPLAY } from '~/diffs/constants';
import { diffMetadata } from '../mock_data/diff_metadata';
describe('Merge Request utilities', () => {
const derivedBaseInfo = {
mrPath: '/gitlab-org/gitlab-test/-/merge_requests/4',
- userOrGroup: 'gitlab-org',
+ namespace: 'gitlab-org',
project: 'gitlab-test',
id: '4',
};
@@ -18,36 +22,98 @@ describe('Merge Request utilities', () => {
};
const unparseableEndpoint = {
mrPath: undefined,
- userOrGroup: undefined,
+ namespace: undefined,
project: undefined,
id: undefined,
...noVersion,
};
+ describe('updateChangesTabCount', () => {
+ let dummyTab;
+ let badge;
+
+ beforeEach(() => {
+ dummyTab = document.createElement('div');
+ dummyTab.classList.add('js-diffs-tab');
+ dummyTab.insertAdjacentHTML('afterbegin', '<span class="gl-badge">ERROR</span>');
+ badge = dummyTab.querySelector('.gl-badge');
+ });
+
+ afterEach(() => {
+ dummyTab.remove();
+ dummyTab = null;
+ badge = null;
+ });
+
+ it('uses the alt hyphen display when the new changes are falsey', () => {
+ updateChangesTabCount({ count: 0, badge });
+
+ expect(dummyTab.textContent).toBe(ZERO_CHANGES_ALT_DISPLAY);
+
+ updateChangesTabCount({ badge });
+
+ expect(dummyTab.textContent).toBe(ZERO_CHANGES_ALT_DISPLAY);
+
+ updateChangesTabCount({ count: false, badge });
+
+ expect(dummyTab.textContent).toBe(ZERO_CHANGES_ALT_DISPLAY);
+ });
+
+ it('uses the actual value for display when the value is truthy', () => {
+ updateChangesTabCount({ count: 42, badge });
+
+ expect(dummyTab.textContent).toBe('42');
+
+ updateChangesTabCount({ count: '999+', badge });
+
+ expect(dummyTab.textContent).toBe('999+');
+ });
+
+ it('selects the proper element to modify by default', () => {
+ document.body.insertAdjacentElement('afterbegin', dummyTab);
+
+ updateChangesTabCount({ count: 42 });
+
+ expect(dummyTab.textContent).toBe('42');
+ });
+ });
+
describe('getDerivedMergeRequestInformation', () => {
- let endpoint = `${diffMetadata.latest_version_path}.json?searchParam=irrelevant`;
+ const bare = diffMetadata.latest_version_path;
it.each`
- argument | response
- ${{ endpoint }} | ${{ ...derivedBaseInfo, ...noVersion }}
- ${{}} | ${unparseableEndpoint}
- ${{ endpoint: undefined }} | ${unparseableEndpoint}
- ${{ endpoint: null }} | ${unparseableEndpoint}
+ argument | response
+ ${{ endpoint: `${bare}.json?searchParam=irrelevant` }} | ${{ ...derivedBaseInfo, ...noVersion }}
+ ${{}} | ${unparseableEndpoint}
+ ${{ endpoint: undefined }} | ${unparseableEndpoint}
+ ${{ endpoint: null }} | ${unparseableEndpoint}
`('generates the correct derived results based on $argument', ({ argument, response }) => {
expect(getDerivedMergeRequestInformation(argument)).toStrictEqual(response);
});
- describe('version information', () => {
- const bare = diffMetadata.latest_version_path;
- endpoint = diffMetadata.merge_request_diffs[0].compare_path;
+ describe('sub-group namespace', () => {
+ it('extracts the entire namespace plus the project name', () => {
+ const { namespace, project } = getDerivedMergeRequestInformation({
+ endpoint: `/some/deep/path/of/groups${bare}`,
+ });
+
+ expect(namespace).toBe('some/deep/path/of/groups/gitlab-org');
+ expect(project).toBe('gitlab-test');
+ });
+ });
+ describe('version information', () => {
it('still gets the correct derived information', () => {
- expect(getDerivedMergeRequestInformation({ endpoint })).toMatchObject(derivedBaseInfo);
+ expect(
+ getDerivedMergeRequestInformation({
+ endpoint: diffMetadata.merge_request_diffs[0].compare_path,
+ }),
+ ).toMatchObject(derivedBaseInfo);
});
it.each`
url | versionPart
- ${endpoint} | ${derivedVersionInfo}
+ ${diffMetadata.merge_request_diffs[0].compare_path} | ${derivedVersionInfo}
${`${bare}?diff_id=${derivedVersionInfo.diffId}`} | ${{ ...derivedVersionInfo, startSha: undefined }}
${`${bare}?start_sha=${derivedVersionInfo.startSha}`} | ${{ ...derivedVersionInfo, diffId: undefined }}
`(
diff --git a/spec/frontend/diffs/utils/tree_worker_utils_spec.js b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
index 4df5fe75004..b8bd4fcd081 100644
--- a/spec/frontend/diffs/utils/tree_worker_utils_spec.js
+++ b/spec/frontend/diffs/utils/tree_worker_utils_spec.js
@@ -75,8 +75,13 @@ describe('~/diffs/utils/tree_worker_utils', () => {
{
addedLines: 0,
changed: true,
+ diffLoaded: false,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'app/index.js',
+ old: undefined,
+ },
key: 'app/index.js',
name: 'index.js',
parentPath: 'app/',
@@ -97,8 +102,13 @@ describe('~/diffs/utils/tree_worker_utils', () => {
{
addedLines: 0,
changed: true,
+ diffLoaded: false,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'app/test/index.js',
+ old: undefined,
+ },
key: 'app/test/index.js',
name: 'index.js',
parentPath: 'app/test/',
@@ -112,8 +122,13 @@ describe('~/diffs/utils/tree_worker_utils', () => {
{
addedLines: 0,
changed: true,
+ diffLoaded: false,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'app/test/filepathneedstruncating.js',
+ old: undefined,
+ },
key: 'app/test/filepathneedstruncating.js',
name: 'filepathneedstruncating.js',
parentPath: 'app/test/',
@@ -138,8 +153,13 @@ describe('~/diffs/utils/tree_worker_utils', () => {
{
addedLines: 42,
changed: true,
+ diffLoaded: false,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'constructor/test/aFile.js',
+ old: undefined,
+ },
key: 'constructor/test/aFile.js',
name: 'aFile.js',
parentPath: 'constructor/test/',
@@ -160,10 +180,15 @@ describe('~/diffs/utils/tree_worker_utils', () => {
name: 'submodule @ abcdef123',
type: 'blob',
changed: true,
+ diffLoaded: false,
tempFile: true,
submodule: true,
deleted: false,
fileHash: 'test',
+ filePaths: {
+ new: 'submodule @ abcdef123',
+ old: undefined,
+ },
addedLines: 1,
removedLines: 0,
tree: [],
@@ -175,10 +200,15 @@ describe('~/diffs/utils/tree_worker_utils', () => {
name: 'package.json',
type: 'blob',
changed: true,
+ diffLoaded: false,
tempFile: false,
submodule: undefined,
deleted: true,
fileHash: 'test',
+ filePaths: {
+ new: 'package.json',
+ old: undefined,
+ },
addedLines: 0,
removedLines: 0,
tree: [],
diff --git a/spec/frontend/drawio/content_editor_facade_spec.js b/spec/frontend/drawio/content_editor_facade_spec.js
new file mode 100644
index 00000000000..673968bac9f
--- /dev/null
+++ b/spec/frontend/drawio/content_editor_facade_spec.js
@@ -0,0 +1,138 @@
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { create } from '~/drawio/content_editor_facade';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
+import axios from '~/lib/utils/axios_utils';
+import { PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML } from '../content_editor/test_constants';
+import { createTestEditor } from '../content_editor/test_utils';
+
+describe('drawio/contentEditorFacade', () => {
+ let tiptapEditor;
+ let axiosMock;
+ let contentEditorFacade;
+ let assetResolver;
+ const imageURL = '/group1/project1/-/wikis/test-file.drawio.svg';
+ const diagramSvg = '<svg></svg>';
+ const contentType = 'image/svg+xml';
+ const filename = 'test-file.drawio.svg';
+ const uploadsPath = '/uploads';
+ const canonicalSrc = '/new-diagram.drawio.svg';
+ const src = `/uploads${canonicalSrc}`;
+
+ beforeEach(() => {
+ assetResolver = {
+ resolveUrl: jest.fn(),
+ };
+ tiptapEditor = createTestEditor({ extensions: [DrawioDiagram] });
+ contentEditorFacade = create({
+ tiptapEditor,
+ drawioNodeName: DrawioDiagram.name,
+ uploadsPath,
+ assetResolver,
+ });
+ });
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ tiptapEditor.destroy();
+ });
+
+ describe('getDiagram', () => {
+ describe('when there is a selected diagram', () => {
+ beforeEach(() => {
+ tiptapEditor
+ .chain()
+ .setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML)
+ .setNodeSelection(1)
+ .run();
+ axiosMock
+ .onGet(imageURL)
+ .reply(HTTP_STATUS_OK, diagramSvg, { 'content-type': contentType });
+ });
+
+ it('returns diagram information', async () => {
+ const diagram = await contentEditorFacade.getDiagram();
+
+ expect(diagram).toEqual({
+ diagramURL: imageURL,
+ filename,
+ diagramSvg,
+ contentType,
+ });
+ });
+ });
+
+ describe('when there is not a selected diagram', () => {
+ beforeEach(() => {
+ tiptapEditor.chain().setContent('<p>text</p>').setNodeSelection(1).run();
+ });
+
+ it('returns null', async () => {
+ const diagram = await contentEditorFacade.getDiagram();
+
+ expect(diagram).toBe(null);
+ });
+ });
+ });
+
+ describe('updateDiagram', () => {
+ beforeEach(() => {
+ tiptapEditor
+ .chain()
+ .setContent(PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML)
+ .setNodeSelection(1)
+ .run();
+
+ assetResolver.resolveUrl.mockReturnValueOnce(src);
+ contentEditorFacade.updateDiagram({ uploadResults: { file_path: canonicalSrc } });
+ });
+
+ it('updates selected diagram diagram node src and canonicalSrc', () => {
+ tiptapEditor.commands.setNodeSelection(1);
+ expect(tiptapEditor.state.selection.node.attrs).toMatchObject({
+ src,
+ canonicalSrc,
+ });
+ });
+ });
+
+ describe('insertDiagram', () => {
+ beforeEach(() => {
+ tiptapEditor.chain().setContent('<p></p>').run();
+
+ assetResolver.resolveUrl.mockReturnValueOnce(src);
+ contentEditorFacade.insertDiagram({ uploadResults: { file_path: canonicalSrc } });
+ });
+
+ it('inserts a new draw.io diagram in the document', () => {
+ tiptapEditor.commands.setNodeSelection(1);
+ expect(tiptapEditor.state.selection.node.attrs).toMatchObject({
+ src,
+ canonicalSrc,
+ });
+ });
+ });
+
+ describe('uploadDiagram', () => {
+ it('sends a post request to the uploadsPath containing the diagram svg', async () => {
+ const link = { markdown: '![](diagram.drawio.svg)' };
+ const blob = new Blob([diagramSvg], { type: 'image/svg+xml' });
+ const formData = new FormData();
+
+ formData.append('file', blob, filename);
+
+ axiosMock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, {
+ data: {
+ link,
+ },
+ });
+
+ const response = await contentEditorFacade.uploadDiagram({ diagramSvg, filename });
+
+ expect(response).not.toBe(link);
+ });
+ });
+});
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js
new file mode 100644
index 00000000000..d7d75922e1e
--- /dev/null
+++ b/spec/frontend/drawio/drawio_editor_spec.js
@@ -0,0 +1,479 @@
+import { launchDrawioEditor } from '~/drawio/drawio_editor';
+import {
+ DRAWIO_EDITOR_URL,
+ DRAWIO_FRAME_ID,
+ DIAGRAM_BACKGROUND_COLOR,
+ DRAWIO_IFRAME_TIMEOUT,
+ DIAGRAM_MAX_SIZE,
+} from '~/drawio/constants';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+
+jest.mock('~/alert');
+
+jest.useFakeTimers();
+
+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 filename = 'diagram.drawio.svg';
+
+ const findDrawioIframe = () => document.getElementById(DRAWIO_FRAME_ID);
+ const waitForDrawioIFrameMessage = ({ messageNumber = 1 } = {}) =>
+ new Promise((resolve) => {
+ let messageCounter = 0;
+ const iframe = findDrawioIframe();
+
+ iframe?.contentWindow.addEventListener('message', (event) => {
+ drawioIFrameReceivedMessages.push(event);
+
+ messageCounter += 1;
+
+ if (messageCounter === messageNumber) {
+ resolve();
+ }
+ });
+ });
+ const expectDrawioIframeMessage = ({ expectation, messageNumber = 1 }) => {
+ expect(drawioIFrameReceivedMessages).toHaveLength(messageNumber);
+ expect(JSON.parse(drawioIFrameReceivedMessages[messageNumber - 1].data)).toEqual(expectation);
+ };
+ const postMessageToParentWindow = (data) => {
+ const event = new Event('message');
+
+ Object.setPrototypeOf(event, {
+ source: findDrawioIframe().contentWindow,
+ data: JSON.stringify(data),
+ });
+
+ window.dispatchEvent(event);
+ };
+
+ beforeEach(() => {
+ editorFacade = {
+ getDiagram: jest.fn(),
+ uploadDiagram: jest.fn(),
+ insertDiagram: jest.fn(),
+ updateDiagram: jest.fn(),
+ };
+ drawioIFrameReceivedMessages = [];
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ findDrawioIframe()?.remove();
+ });
+
+ describe('initializing', () => {
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade });
+ });
+
+ it('creates the drawio editor iframe and attaches it to the body', () => {
+ expect(findDrawioIframe().getAttribute('src')).toBe(DRAWIO_EDITOR_URL);
+ });
+
+ it('sets drawio-editor classname to the iframe', () => {
+ expect(findDrawioIframe().classList).toContain('drawio-editor');
+ });
+ });
+
+ describe(`when parent window does not receive configure event after ${DRAWIO_IFRAME_TIMEOUT} ms`, () => {
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade });
+ });
+
+ it('disposes draw.io iframe', () => {
+ expect(findDrawioIframe()).not.toBe(null);
+ jest.runAllTimers();
+ expect(findDrawioIframe()).toBe(null);
+ });
+
+ it('displays an alert indicating that the draw.io editor could not be loaded', () => {
+ jest.runAllTimers();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'The diagrams.net editor could not be loaded.',
+ });
+ });
+ });
+
+ describe('when parent window receives configure event', () => {
+ beforeEach(async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'configure' });
+
+ await waitForDrawioIFrameMessage();
+ });
+
+ it('sends configure action to the draw.io iframe', () => {
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'configure',
+ config: {
+ darkColor: '#202020',
+ settingsName: 'gitlab',
+ },
+ colorSchemeMeta: false,
+ },
+ });
+ });
+
+ it('does not remove the iframe after the load error timeouts run', () => {
+ jest.runAllTimers();
+
+ expect(findDrawioIframe()).not.toBe(null);
+ });
+ });
+
+ describe('when parent window receives init event', () => {
+ describe('when there isn’t a diagram selected', () => {
+ beforeEach(() => {
+ editorFacade.getDiagram.mockResolvedValueOnce(null);
+
+ launchDrawioEditor({ editorFacade });
+
+ postMessageToParentWindow({ event: 'init' });
+ });
+
+ it('sends load action to the draw.io iframe with empty svg and title', async () => {
+ await waitForDrawioIFrameMessage();
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'load',
+ xml: null,
+ border: 8,
+ background: DIAGRAM_BACKGROUND_COLOR,
+ dark: false,
+ title: null,
+ },
+ });
+ });
+ });
+
+ describe('when there is a diagram selected', () => {
+ const diagramSvg = '<svg></svg>';
+
+ beforeEach(() => {
+ editorFacade.getDiagram.mockResolvedValueOnce({
+ diagramURL,
+ diagramSvg,
+ filename,
+ contentType: 'image/svg+xml',
+ });
+
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'init' });
+ });
+
+ it('sends load action to the draw.io iframe with the selected diagram svg and filename', async () => {
+ await waitForDrawioIFrameMessage();
+
+ // Step 5: The draw.io editor will send the downloaded diagram to the iframe
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'load',
+ xml: diagramSvg,
+ border: 8,
+ background: DIAGRAM_BACKGROUND_COLOR,
+ dark: false,
+ title: filename,
+ },
+ });
+ });
+
+ it('sets the drawio iframe as visible and resets cursor', async () => {
+ await waitForDrawioIFrameMessage();
+
+ expect(findDrawioIframe().style.visibility).toBe('visible');
+ expect(findDrawioIframe().style.cursor).toBe('');
+ });
+
+ it('scrolls window to the top', async () => {
+ await waitForDrawioIFrameMessage();
+
+ expect(window.scrollX).toBe(0);
+ });
+ });
+
+ describe.each`
+ description | errorMessage | diagram
+ ${'when there is an image selected that is not an svg file'} | ${'The selected image is not a valid SVG diagram'} | ${{
+ diagramURL,
+ contentType: 'image/png',
+ filename: 'image.png',
+}}
+ ${'when the selected image is not an asset upload'} | ${'The selected image is not an asset uploaded in the application'} | ${{
+ diagramSvg: '<svg></svg>',
+ filename,
+ contentType: 'image/svg+xml',
+ diagramURL: 'https://example.com/image.drawio.svg',
+}}
+ ${'when the selected image is too large'} | ${'The selected image is too large.'} | ${{
+ diagramSvg: 'x'.repeat(DIAGRAM_MAX_SIZE + 1),
+ filename,
+ contentType: 'image/svg+xml',
+ diagramURL,
+}}
+ `('$description', ({ errorMessage, diagram }) => {
+ beforeEach(() => {
+ editorFacade.getDiagram.mockResolvedValueOnce(diagram);
+
+ launchDrawioEditor({ editorFacade });
+
+ postMessageToParentWindow({ event: 'init' });
+ });
+
+ it('displays an error alert indicating that the image is not a diagram', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: errorMessage,
+ error: expect.any(Error),
+ });
+ });
+
+ it('disposes the draw.io diagram iframe', () => {
+ expect(findDrawioIframe()).toBe(null);
+ });
+ });
+
+ describe('when loading a diagram fails', () => {
+ beforeEach(() => {
+ editorFacade.getDiagram.mockRejectedValueOnce(new Error());
+
+ launchDrawioEditor({ editorFacade });
+
+ postMessageToParentWindow({ event: 'init' });
+ });
+
+ it('displays an error alert indicating the failure', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Cannot load the diagram into the diagrams.net editor',
+ error: expect.any(Error),
+ });
+ });
+
+ it('disposes the draw.io diagram iframe', () => {
+ expect(findDrawioIframe()).toBe(null);
+ });
+ });
+ });
+
+ describe('when parent window receives prompt event', () => {
+ describe('when the filename is empty', () => {
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade });
+
+ postMessageToParentWindow({ event: 'prompt', value: '' });
+ });
+
+ it('sends prompt action to the draw.io iframe requesting a filename', async () => {
+ await waitForDrawioIFrameMessage({ messageNumber: 1 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'prompt',
+ titleKey: 'filename',
+ okKey: 'save',
+ defaultValue: 'diagram.drawio.svg',
+ },
+ messageNumber: 1,
+ });
+ });
+
+ it('sends dialog action to the draw.io iframe indicating that the filename cannot be empty', async () => {
+ await waitForDrawioIFrameMessage({ messageNumber: 2 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'dialog',
+ titleKey: 'error',
+ messageKey: 'filenameShort',
+ buttonKey: 'ok',
+ },
+ messageNumber: 2,
+ });
+ });
+ });
+
+ describe('when the event data is not empty', () => {
+ beforeEach(async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'prompt', value: 'diagram.drawio.svg' });
+
+ await waitForDrawioIFrameMessage();
+ });
+
+ it('starts the saving file process', () => {
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'spinner',
+ show: true,
+ messageKey: 'saving',
+ },
+ });
+ });
+ });
+ });
+
+ describe('when parent receives export event', () => {
+ beforeEach(() => {
+ editorFacade.uploadDiagram.mockResolvedValueOnce({});
+ });
+
+ it('reloads diagram in the draw.io editor', async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage();
+
+ expectDrawioIframeMessage({
+ expectation: expect.objectContaining({
+ action: 'load',
+ xml: expect.stringContaining(testSvg),
+ }),
+ });
+ });
+
+ it('marks the diagram as modified in the draw.io editor', async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 2 });
+
+ expectDrawioIframeMessage({
+ expectation: expect.objectContaining({
+ action: 'status',
+ modified: true,
+ }),
+ messageNumber: 2,
+ });
+ });
+
+ describe('when the diagram filename is set', () => {
+ const TEST_FILENAME = 'diagram.drawio.svg';
+
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade, filename: TEST_FILENAME });
+ });
+
+ it('displays loading spinner in the draw.io editor', async () => {
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 3 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'spinner',
+ show: true,
+ messageKey: 'saving',
+ },
+ messageNumber: 3,
+ });
+ });
+
+ it('uploads exported diagram', async () => {
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 3 });
+
+ expect(editorFacade.uploadDiagram).toHaveBeenCalledWith({
+ filename: TEST_FILENAME,
+ diagramSvg: expect.stringContaining(testSvg),
+ });
+ });
+
+ describe('when uploading the exported diagram succeeds', () => {
+ it('displays an alert indicating that the diagram was uploaded successfully', async () => {
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 3 });
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: expect.any(String),
+ variant: VARIANT_SUCCESS,
+ fadeTransition: true,
+ });
+ });
+
+ it('disposes iframe', () => {
+ jest.runAllTimers();
+
+ expect(findDrawioIframe()).toBe(null);
+ });
+ });
+
+ describe('when uploading the exported diagram fails', () => {
+ const uploadError = new Error();
+
+ beforeEach(() => {
+ editorFacade.uploadDiagram.mockReset();
+ editorFacade.uploadDiagram.mockRejectedValue(uploadError);
+
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+ });
+
+ it('hides loading indicator in the draw.io editor', async () => {
+ await waitForDrawioIFrameMessage({ messageNumber: 4 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'spinner',
+ show: false,
+ },
+ messageNumber: 4,
+ });
+ });
+
+ it('displays an error dialog in the draw.io editor', async () => {
+ await waitForDrawioIFrameMessage({ messageNumber: 5 });
+
+ expectDrawioIframeMessage({
+ expectation: {
+ action: 'dialog',
+ titleKey: 'error',
+ modified: true,
+ buttonKey: 'close',
+ messageKey: 'errorSavingFile',
+ },
+ messageNumber: 5,
+ });
+ });
+ });
+ });
+
+ describe('when diagram filename is not set', () => {
+ it('sends prompt action to the draw.io iframe', async () => {
+ launchDrawioEditor({ editorFacade });
+ postMessageToParentWindow({ event: 'export', data: testEncodedSvg });
+
+ await waitForDrawioIFrameMessage({ messageNumber: 3 });
+
+ expect(drawioIFrameReceivedMessages[2].data).toEqual(
+ JSON.stringify({
+ action: 'prompt',
+ titleKey: 'filename',
+ okKey: 'save',
+ defaultValue: 'diagram.drawio.svg',
+ }),
+ );
+ });
+ });
+ });
+
+ describe('when parent window receives exit event', () => {
+ beforeEach(() => {
+ launchDrawioEditor({ editorFacade });
+ });
+
+ it('disposes the the draw.io iframe', () => {
+ expect(findDrawioIframe()).not.toBe(null);
+
+ postMessageToParentWindow({ event: 'exit' });
+
+ expect(findDrawioIframe()).toBe(null);
+ });
+ });
+});
diff --git a/spec/frontend/drawio/markdown_field_editor_facade_spec.js b/spec/frontend/drawio/markdown_field_editor_facade_spec.js
new file mode 100644
index 00000000000..e3eafc63839
--- /dev/null
+++ b/spec/frontend/drawio/markdown_field_editor_facade_spec.js
@@ -0,0 +1,148 @@
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { create } from '~/drawio/markdown_field_editor_facade';
+import * as textMarkdown from '~/lib/utils/text_markdown';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
+
+jest.mock('~/lib/utils/text_markdown');
+
+describe('drawio/textareaMarkdownEditor', () => {
+ let textArea;
+ let textareaMarkdownEditor;
+ let axiosMock;
+
+ const markdownPreviewPath = '/markdown/preview';
+ const imageURL = '/assets/image.png';
+ const diagramMarkdown = '![](image.png)';
+ const diagramSvg = '<svg></svg>';
+ const contentType = 'image/svg+xml';
+ const filename = 'image.png';
+ const newDiagramMarkdown = '![](newdiagram.svg)';
+ const uploadsPath = '/uploads';
+
+ beforeEach(() => {
+ textArea = document.createElement('textarea');
+ textareaMarkdownEditor = create({ textArea, markdownPreviewPath, uploadsPath });
+
+ document.body.appendChild(textArea);
+ });
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ textArea.remove();
+ });
+
+ describe('getDiagram', () => {
+ describe('when there is a selected diagram', () => {
+ beforeEach(() => {
+ textMarkdown.resolveSelectedImage.mockReturnValueOnce({
+ imageURL,
+ imageMarkdown: diagramMarkdown,
+ filename,
+ });
+ axiosMock
+ .onGet(imageURL)
+ .reply(HTTP_STATUS_OK, diagramSvg, { 'content-type': contentType });
+ });
+
+ it('returns diagram information', async () => {
+ const diagram = await textareaMarkdownEditor.getDiagram();
+
+ expect(textMarkdown.resolveSelectedImage).toHaveBeenCalledWith(
+ textArea,
+ markdownPreviewPath,
+ );
+
+ expect(diagram).toEqual({
+ diagramURL: imageURL,
+ diagramMarkdown,
+ filename,
+ diagramSvg,
+ contentType,
+ });
+ });
+ });
+
+ describe('when there is not a selected diagram', () => {
+ beforeEach(() => {
+ textMarkdown.resolveSelectedImage.mockReturnValueOnce(null);
+ });
+
+ it('returns null', async () => {
+ const diagram = await textareaMarkdownEditor.getDiagram();
+
+ expect(textMarkdown.resolveSelectedImage).toHaveBeenCalledWith(
+ textArea,
+ markdownPreviewPath,
+ );
+
+ expect(diagram).toBe(null);
+ });
+ });
+ });
+
+ describe('updateDiagram', () => {
+ beforeEach(() => {
+ jest.spyOn(textArea, 'focus');
+ jest.spyOn(textArea, 'dispatchEvent');
+
+ textArea.value = `diagram ${diagramMarkdown}`;
+
+ textareaMarkdownEditor.updateDiagram({
+ diagramMarkdown,
+ uploadResults: { link: { markdown: newDiagramMarkdown } },
+ });
+ });
+
+ it('focuses the textarea', () => {
+ expect(textArea.focus).toHaveBeenCalled();
+ });
+
+ it('replaces previous diagram markdown with new diagram markdown', () => {
+ expect(textArea.value).toBe(`diagram ${newDiagramMarkdown}`);
+ });
+
+ it('dispatches input event in the textarea', () => {
+ expect(textArea.dispatchEvent).toHaveBeenCalledWith(new Event('input'));
+ });
+ });
+
+ describe('insertDiagram', () => {
+ it('inserts markdown text and replaces any selected markdown in the textarea', () => {
+ textArea.value = `diagram ${diagramMarkdown}`;
+ textArea.setSelectionRange(0, 8);
+
+ textareaMarkdownEditor.insertDiagram({
+ uploadResults: { link: { markdown: newDiagramMarkdown } },
+ });
+
+ expect(textMarkdown.insertMarkdownText).toHaveBeenCalledWith({
+ textArea,
+ text: textArea.value,
+ tag: newDiagramMarkdown,
+ selected: textArea.value.substring(0, 8),
+ });
+ });
+ });
+
+ describe('uploadDiagram', () => {
+ it('sends a post request to the uploadsPath containing the diagram svg', async () => {
+ const link = { markdown: '![](diagram.drawio.svg)' };
+ const blob = new Blob([diagramSvg], { type: 'image/svg+xml' });
+ const formData = new FormData();
+
+ formData.append('file', blob, filename);
+
+ axiosMock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, {
+ link,
+ });
+
+ const response = await textareaMarkdownEditor.uploadDiagram({ diagramSvg, filename });
+
+ expect(response).toEqual({ link });
+ });
+ });
+});
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index fdd157dd09f..57debf79c7b 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -1,7 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import htmlNewMilestone from 'test_fixtures/milestones/new-milestone.html';
import mock from 'xhr-mock';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
@@ -48,9 +49,9 @@ describe('dropzone_input', () => {
};
beforeEach(() => {
- loadHTMLFixture('issues/new-issue.html');
+ setHTMLFixture(htmlNewMilestone);
- form = $('#new_issue');
+ form = $('#new_milestone');
form.data('uploads-path', TEST_UPLOAD_PATH);
dropzoneInput(form);
});
diff --git a/spec/frontend/editor/components/helpers.js b/spec/frontend/editor/components/helpers.js
index 12f90390c18..5cc66dd2ae0 100644
--- a/spec/frontend/editor/components/helpers.js
+++ b/spec/frontend/editor/components/helpers.js
@@ -1,4 +1,3 @@
-import { EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
import { apolloProvider } from '~/editor/components/source_editor_toolbar_graphql';
import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
@@ -9,7 +8,7 @@ export const buildButton = (id = 'foo-bar-btn', options = {}) => {
label: options.label || 'Foo Bar Button',
icon: options.icon || 'check',
selected: options.selected || false,
- group: options.group || EDITOR_TOOLBAR_RIGHT_GROUP,
+ group: options.group,
onClick: options.onClick || (() => {}),
category: options.category || 'primary',
selectedLabel: options.selectedLabel || 'smth',
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index ff377494312..b5944a52af7 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -21,11 +21,6 @@ describe('Source Editor Toolbar button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default', () => {
const defaultProps = {
category: 'primary',
@@ -38,17 +33,17 @@ describe('Source Editor Toolbar button', () => {
it('does not render the button if the props have not been passed', () => {
createComponent({});
- expect(findButton().vm).toBeUndefined();
+ expect(findButton().exists()).toBe(false);
});
- it('renders a default button without props', async () => {
+ it('renders a default button without props', () => {
createComponent();
const btn = findButton();
expect(btn.exists()).toBe(true);
expect(btn.props()).toMatchObject(defaultProps);
});
- it('renders a button based on the props passed', async () => {
+ it('renders a button based on the props passed', () => {
createComponent({
button: customProps,
});
@@ -112,34 +107,31 @@ describe('Source Editor Toolbar button', () => {
});
describe('click handler', () => {
- let clickEvent;
-
- beforeEach(() => {
- clickEvent = new Event('click');
- });
-
it('fires the click handler on the button when available', async () => {
- const spy = jest.fn();
+ const clickSpy = jest.fn();
+ const clickEvent = new Event('click');
createComponent({
button: {
- onClick: spy,
+ onClick: clickSpy,
},
});
- expect(spy).not.toHaveBeenCalled();
+ expect(wrapper.emitted('click')).toEqual(undefined);
findButton().vm.$emit('click', clickEvent);
await nextTick();
- expect(spy).toHaveBeenCalledWith(clickEvent);
+
+ expect(wrapper.emitted('click')).toEqual([[clickEvent]]);
+ expect(clickSpy).toHaveBeenCalledWith(clickEvent);
});
+
it('emits the "click" event, passing the event itself', async () => {
createComponent();
- jest.spyOn(wrapper.vm, '$emit');
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ expect(wrapper.emitted('click')).toEqual(undefined);
- findButton().vm.$emit('click', clickEvent);
+ findButton().vm.$emit('click');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', clickEvent);
+ expect(wrapper.emitted('click')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/editor/components/source_editor_toolbar_spec.js b/spec/frontend/editor/components/source_editor_toolbar_spec.js
index bead39ca744..95dc29c7916 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_spec.js
@@ -5,7 +5,7 @@ import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import SourceEditorToolbar from '~/editor/components/source_editor_toolbar.vue';
import SourceEditorToolbarButton from '~/editor/components/source_editor_toolbar_button.vue';
-import { EDITOR_TOOLBAR_LEFT_GROUP, EDITOR_TOOLBAR_RIGHT_GROUP } from '~/editor/constants';
+import { EDITOR_TOOLBAR_BUTTON_GROUPS } from '~/editor/constants';
import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
import { buildButton } from './helpers';
@@ -40,24 +40,24 @@ describe('Source Editor Toolbar', () => {
};
afterEach(() => {
- wrapper.destroy();
mockApollo = null;
});
describe('groups', () => {
it.each`
- group | expectedGroup
- ${EDITOR_TOOLBAR_LEFT_GROUP} | ${EDITOR_TOOLBAR_LEFT_GROUP}
- ${EDITOR_TOOLBAR_RIGHT_GROUP} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
- ${undefined} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
- ${'non-existing'} | ${EDITOR_TOOLBAR_RIGHT_GROUP}
+ group | expectedGroup
+ ${EDITOR_TOOLBAR_BUTTON_GROUPS.file} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.file}
+ ${EDITOR_TOOLBAR_BUTTON_GROUPS.edit} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.edit}
+ ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings}
+ ${undefined} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings}
+ ${'non-existing'} | ${EDITOR_TOOLBAR_BUTTON_GROUPS.settings}
`('puts item with group="$group" into $expectedGroup group', ({ group, expectedGroup }) => {
const item = buildButton('first', {
group,
});
createComponentWithApollo([item]);
expect(findButtons()).toHaveLength(1);
- [EDITOR_TOOLBAR_RIGHT_GROUP, EDITOR_TOOLBAR_LEFT_GROUP].forEach((g) => {
+ Object.keys(EDITOR_TOOLBAR_BUTTON_GROUPS).forEach((g) => {
if (g === expectedGroup) {
expect(wrapper.vm.getGroupItems(g)).toEqual([expect.objectContaining({ id: 'first' })]);
} else {
@@ -70,7 +70,7 @@ describe('Source Editor Toolbar', () => {
describe('buttons update', () => {
it('properly updates buttons on Apollo cache update', async () => {
const item = buildButton('first', {
- group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ group: EDITOR_TOOLBAR_BUTTON_GROUPS.edit,
});
createComponentWithApollo();
@@ -95,22 +95,25 @@ describe('Source Editor Toolbar', () => {
describe('click handler', () => {
it('emits the "click" event when a button is clicked', () => {
const item1 = buildButton('first', {
- group: EDITOR_TOOLBAR_LEFT_GROUP,
+ group: EDITOR_TOOLBAR_BUTTON_GROUPS.file,
});
const item2 = buildButton('second', {
- group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ group: EDITOR_TOOLBAR_BUTTON_GROUPS.edit,
});
- createComponentWithApollo([item1, item2]);
- jest.spyOn(wrapper.vm, '$emit');
- expect(wrapper.vm.$emit).not.toHaveBeenCalled();
+ const item3 = buildButton('third', {
+ group: EDITOR_TOOLBAR_BUTTON_GROUPS.settings,
+ });
+ createComponentWithApollo([item1, item2, item3]);
+ expect(wrapper.emitted('click')).toEqual(undefined);
findButtons().at(0).vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item1);
+ expect(wrapper.emitted('click')).toEqual([[item1]]);
findButtons().at(1).vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click', item2);
+ expect(wrapper.emitted('click')).toEqual([[item1], [item2]]);
- expect(wrapper.vm.$emit.mock.calls).toHaveLength(2);
+ findButtons().at(2).vm.$emit('click');
+ expect(wrapper.emitted('click')).toEqual([[item1], [item2], [item3]]);
});
});
});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index c822a0bfeaf..51fcf26c39a 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -27,12 +27,14 @@ import CacheYaml from './yaml_tests/positive_tests/cache.yml';
import FilterYaml from './yaml_tests/positive_tests/filter.yml';
import IncludeYaml from './yaml_tests/positive_tests/include.yml';
import RulesYaml from './yaml_tests/positive_tests/rules.yml';
+import RulesNeedsYaml from './yaml_tests/positive_tests/rules_needs.yml';
import ProjectPathYaml from './yaml_tests/positive_tests/project_path.yml';
import VariablesYaml from './yaml_tests/positive_tests/variables.yml';
import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml';
import IdTokensYaml from './yaml_tests/positive_tests/id_tokens.yml';
import HooksYaml from './yaml_tests/positive_tests/hooks.yml';
import SecretsYaml from './yaml_tests/positive_tests/secrets.yml';
+import ServicesYaml from './yaml_tests/positive_tests/services.yml';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
@@ -45,6 +47,7 @@ import ProjectPathIncludeLeadSlashYaml from './yaml_tests/negative_tests/project
import ProjectPathIncludeNoSlashYaml from './yaml_tests/negative_tests/project_path/include/no_slash.yml';
import ProjectPathIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/include/tailing_slash.yml';
import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml';
+import RulesNeedsNegativeYaml from './yaml_tests/negative_tests/rules_needs.yml';
import TriggerNegative from './yaml_tests/negative_tests/trigger.yml';
import VariablesInvalidOptionsYaml from './yaml_tests/negative_tests/variables/invalid_options.yml';
import VariablesInvalidSyntaxDescYaml from './yaml_tests/negative_tests/variables/invalid_syntax_desc.yml';
@@ -52,6 +55,7 @@ import VariablesWrongSyntaxUsageExpand from './yaml_tests/negative_tests/variabl
import IdTokensNegativeYaml from './yaml_tests/negative_tests/id_tokens.yml';
import HooksNegative from './yaml_tests/negative_tests/hooks.yml';
import SecretsNegativeYaml from './yaml_tests/negative_tests/secrets.yml';
+import ServicesNegativeYaml from './yaml_tests/negative_tests/services.yml';
const ajv = new Ajv({
strictTypes: false,
@@ -86,9 +90,11 @@ describe('positive tests', () => {
JobWhenYaml,
HooksYaml,
RulesYaml,
+ RulesNeedsYaml,
VariablesYaml,
ProjectPathYaml,
IdTokensYaml,
+ ServicesYaml,
SecretsYaml,
}),
)('schema validates %s', (_, input) => {
@@ -113,10 +119,13 @@ describe('negative tests', () => {
// YAML
ArtifactsNegativeYaml,
CacheKeyNeative,
+ HooksNegative,
IdTokensNegativeYaml,
IncludeNegativeYaml,
JobWhenNegativeYaml,
RulesNegativeYaml,
+ RulesNeedsNegativeYaml,
+ TriggerNegative,
VariablesInvalidOptionsYaml,
VariablesInvalidSyntaxDescYaml,
VariablesWrongSyntaxUsageExpand,
@@ -126,8 +135,7 @@ describe('negative tests', () => {
ProjectPathIncludeNoSlashYaml,
ProjectPathIncludeTailSlashYaml,
SecretsNegativeYaml,
- TriggerNegative,
- HooksNegative,
+ ServicesNegativeYaml,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules_needs.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules_needs.yml
new file mode 100644
index 00000000000..f2f1eb118f8
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules_needs.yml
@@ -0,0 +1,46 @@
+# invalid rules:needs
+lint_job:
+ script: exit 0
+ rules:
+ - if: $var == null
+ needs:
+
+# invalid rules:needs
+lint_job_2:
+ script: exit 0
+ rules:
+ - if: $var == null
+ needs: [20]
+
+# invalid rules:needs
+lint_job_3:
+ script: exit 0
+ rules:
+ - if: $var == null
+ needs:
+ - job:
+
+# invalid rules:needs
+lint_job_5:
+ script: exit 0
+ rules:
+ - if: $var == null
+ needs:
+ - pipeline: 5
+
+# invalid rules:needs
+lint_job_6:
+ script: exit 0
+ rules:
+ - if: $var == null
+ needs:
+ - project: namespace/group/project-name
+
+# invalid rules:needs
+lint_job_7:
+ script: exit 0
+ rules:
+ - if: $var == null
+ needs:
+ - pipeline: 5
+ job: lint_job_6
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml
new file mode 100644
index 00000000000..6761a603a0a
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/services.yml
@@ -0,0 +1,38 @@
+empty_services:
+ services:
+
+without_name:
+ services:
+ - alias: db-postgres
+ entrypoint: ["/usr/local/bin/db-postgres"]
+ command: ["start"]
+
+invalid_entrypoint:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ entrypoint: "/usr/local/bin/db-postgres" # must be array
+
+empty_entrypoint:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ entrypoint: []
+
+invalid_command:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ command: "start" # must be array
+
+empty_command:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ command: []
+
+empty_pull_policy:
+ script: echo "Multiple pull policies."
+ services:
+ - name: postgres:11.6
+ pull_policy: []
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules_needs.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules_needs.yml
new file mode 100644
index 00000000000..a4a5183dcf4
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules_needs.yml
@@ -0,0 +1,32 @@
+# valid workflow:rules:needs
+pre_lint_job:
+ script: exit 0
+ rules:
+ - if: $var == null
+
+lint_job:
+ script: exit 0
+ rules:
+ - if: $var == null
+
+rspec_job:
+ script: exit 0
+ rules:
+ - if: $var == null
+ needs: [lint_job]
+
+job:
+ needs: [rspec_job]
+ script: exit 0
+ rules:
+ - if: $var == null
+ needs:
+ - job: lint_job
+ artifacts: false
+ optional: true
+ - job: pre_lint_job
+ artifacts: true
+ optional: false
+ - rspec_job
+ - if: $var == true
+ needs: [lint_job, pre_lint_job] \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml
new file mode 100644
index 00000000000..8a0f59d1dfd
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/services.yml
@@ -0,0 +1,31 @@
+valid_services_list:
+ services:
+ - php:7
+ - node:latest
+ - golang:1.10
+
+valid_services_object:
+ services:
+ - name: my-postgres:11.7
+ alias: db-postgres
+ entrypoint: ["/usr/local/bin/db-postgres"]
+ command: ["start"]
+
+services_with_variables:
+ services:
+ - name: bitnami/nginx
+ alias: nginx
+ variables:
+ NGINX_HTTP_PORT_NUMBER: ${NGINX_HTTP_PORT_NUMBER}
+
+pull_policy_string:
+ script: echo "A single pull policy."
+ services:
+ - name: postgres:11.6
+ pull_policy: if-not-present
+
+pull_policy_array:
+ script: echo "Multiple pull policies."
+ services:
+ - name: postgres:11.6
+ pull_policy: [always, if-not-present]
diff --git a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
index 21f8979f1a9..e515285601b 100644
--- a/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
+++ b/spec/frontend/editor/source_editor_ci_schema_ext_spec.js
@@ -17,7 +17,6 @@ describe('~/editor/editor_ci_config_ext', () => {
let editor;
let instance;
let editorEl;
- let originalGitlabUrl;
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
setHTMLFixture('<div id="editor"></div>');
@@ -31,16 +30,8 @@ describe('~/editor/editor_ci_config_ext', () => {
instance.use({ definition: CiSchemaExtension });
};
- beforeAll(() => {
- originalGitlabUrl = gon.gitlab_url;
- gon.gitlab_url = TEST_HOST;
- });
-
- afterAll(() => {
- gon.gitlab_url = originalGitlabUrl;
- });
-
beforeEach(() => {
+ gon.gitlab_url = TEST_HOST;
createMockEditor();
});
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index eab39ccaba1..b1b8173188c 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -7,6 +7,7 @@ import {
EDITOR_TYPE_DIFF,
EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS,
EXTENSION_BASE_LINE_NUMBERS_CLASS,
+ EXTENSION_SOFTWRAP_ID,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import EditorInstance from '~/editor/source_editor_instance';
@@ -35,8 +36,18 @@ describe('The basis for an Source Editor extension', () => {
},
};
};
- const createInstance = (baseInstance = {}) => {
- return new EditorInstance(baseInstance);
+ const baseInstance = {
+ getOption: jest.fn(),
+ };
+
+ const createInstance = (base = baseInstance) => {
+ return new EditorInstance(base);
+ };
+
+ const toolbar = {
+ addItems: jest.fn(),
+ updateItem: jest.fn(),
+ removeItems: jest.fn(),
};
beforeEach(() => {
@@ -49,6 +60,66 @@ describe('The basis for an Source Editor extension', () => {
resetHTMLFixture();
});
+ describe('onSetup callback', () => {
+ let instance;
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ });
+
+ it('adds correct buttons to the toolbar', () => {
+ instance.use({ definition: SourceEditorExtension });
+ expect(instance.toolbar.addItems).toHaveBeenCalledWith([
+ expect.objectContaining({
+ id: EXTENSION_SOFTWRAP_ID,
+ }),
+ ]);
+ });
+
+ it('does not fail if toolbar is not available', () => {
+ instance.toolbar = null;
+ expect(() => instance.use({ definition: SourceEditorExtension })).not.toThrow();
+ });
+
+ it.each`
+ optionValue | expectSelected
+ ${'on'} | ${true}
+ ${'off'} | ${false}
+ ${'foo'} | ${false}
+ ${undefined} | ${false}
+ ${null} | ${false}
+ `(
+ 'correctly sets the initial state of the button when wordWrap option is "$optionValue"',
+ ({ optionValue, expectSelected }) => {
+ instance.getOption.mockReturnValue(optionValue);
+ instance.use({ definition: SourceEditorExtension });
+ expect(instance.toolbar.addItems).toHaveBeenCalledWith([
+ expect.objectContaining({
+ selected: expectSelected,
+ }),
+ ]);
+ },
+ );
+ });
+
+ describe('onBeforeUnuse', () => {
+ let instance;
+ let extension;
+
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ extension = instance.use({ definition: SourceEditorExtension });
+ });
+ it('removes the registered buttons from the toolbar', () => {
+ expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
+ instance.unuse(extension);
+ expect(instance.toolbar.removeItems).toHaveBeenCalledWith([EXTENSION_SOFTWRAP_ID]);
+ });
+ });
+
describe('onUse callback', () => {
it('initializes the line highlighting', () => {
const instance = createInstance();
@@ -66,6 +137,7 @@ describe('The basis for an Source Editor extension', () => {
'$description the line linking for $instanceType instance',
({ instanceType, shouldBeCalled }) => {
const instance = createInstance({
+ ...baseInstance,
getEditorType: jest.fn().mockReturnValue(instanceType),
onMouseMove: jest.fn(),
onMouseDown: jest.fn(),
@@ -82,10 +154,44 @@ describe('The basis for an Source Editor extension', () => {
);
});
+ describe('toggleSoftwrap', () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = createInstance();
+
+ instance.toolbar = toolbar;
+ instance.use({ definition: SourceEditorExtension });
+ });
+
+ it.each`
+ currentWordWrap | newWordWrap | expectSelected
+ ${'on'} | ${'off'} | ${false}
+ ${'off'} | ${'on'} | ${true}
+ ${'foo'} | ${'on'} | ${true}
+ ${undefined} | ${'on'} | ${true}
+ ${null} | ${'on'} | ${true}
+ `(
+ 'correctly updates wordWrap option in editor and the state of the button when currentWordWrap is "$currentWordWrap"',
+ ({ currentWordWrap, newWordWrap, expectSelected }) => {
+ instance.getOption.mockReturnValue(currentWordWrap);
+ instance.updateOptions = jest.fn();
+ instance.toggleSoftwrap();
+ expect(instance.updateOptions).toHaveBeenCalledWith({
+ wordWrap: newWordWrap,
+ });
+ expect(instance.toolbar.updateItem).toHaveBeenCalledWith(EXTENSION_SOFTWRAP_ID, {
+ selected: expectSelected,
+ });
+ },
+ );
+ });
+
describe('highlightLines', () => {
const revealSpy = jest.fn();
const decorationsSpy = jest.fn();
const instance = createInstance({
+ ...baseInstance,
revealLineInCenter: revealSpy,
deltaDecorations: decorationsSpy,
});
@@ -174,6 +280,7 @@ describe('The basis for an Source Editor extension', () => {
beforeEach(() => {
instance = createInstance({
+ ...baseInstance,
deltaDecorations: decorationsSpy,
lineDecorations,
});
@@ -188,6 +295,7 @@ describe('The basis for an Source Editor extension', () => {
describe('setupLineLinking', () => {
const instance = {
+ ...baseInstance,
onMouseMove: jest.fn(),
onMouseDown: jest.fn(),
deltaDecorations: jest.fn(),
diff --git a/spec/frontend/editor/source_editor_markdown_ext_spec.js b/spec/frontend/editor/source_editor_markdown_ext_spec.js
index 33e4b4bfc8e..b226ef03b33 100644
--- a/spec/frontend/editor/source_editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_ext_spec.js
@@ -17,6 +17,7 @@ describe('Markdown Extension for Source Editor', () => {
const thirdLine = 'string with some **markup**';
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
const markdownPath = 'foo.md';
+ let extensions;
const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => {
const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
@@ -38,7 +39,10 @@ describe('Markdown Extension for Source Editor', () => {
blobPath: markdownPath,
blobContent: text,
});
- instance.use([{ definition: ToolbarExtension }, { definition: EditorMarkdownExtension }]);
+ extensions = instance.use([
+ { definition: ToolbarExtension },
+ { definition: EditorMarkdownExtension },
+ ]);
});
afterEach(() => {
@@ -59,6 +63,25 @@ describe('Markdown Extension for Source Editor', () => {
});
});
+ describe('markdown keystrokes', () => {
+ it('registers all keystrokes as actions', () => {
+ EXTENSION_MARKDOWN_BUTTONS.forEach((button) => {
+ if (button.data.mdShortcuts) {
+ expect(instance.getAction(button.id)).toBeDefined();
+ }
+ });
+ });
+
+ it('disposes all keystrokes on unuse', () => {
+ instance.unuse(extensions[1]);
+ EXTENSION_MARKDOWN_BUTTONS.forEach((button) => {
+ if (button.data.mdShortcuts) {
+ expect(instance.getAction(button.id)).toBeNull();
+ }
+ });
+ });
+ });
+
describe('getSelectedText', () => {
it('does not fail if there is no selection and returns the empty string', () => {
jest.spyOn(instance, 'getSelection');
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index c42ac28c498..fb5fce92482 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -1,6 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import { Emitter } from 'monaco-editor';
-import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import {
@@ -12,14 +11,14 @@ import {
} from '~/editor/constants';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import SourceEditor from '~/editor/source_editor';
-import { createAlert } from '~/flash';
+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 syntaxHighlight from '~/syntax_highlight';
import { spyOnApi } from './helpers';
jest.mock('~/syntax_highlight');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Markdown Live Preview Extension for Source Editor', () => {
let editor;
@@ -28,6 +27,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let panelSpy;
let mockAxios;
let extension;
+ let resizeCallback;
const previewMarkdownPath = '/gitlab/fooGroup/barProj/preview_markdown';
const firstLine = 'This is a';
const secondLine = 'multiline';
@@ -35,6 +35,8 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
const markdownPath = 'foo.md';
const responseData = '<div>FooBar</div>';
+ const observeSpy = jest.fn();
+ const disconnectSpy = jest.fn();
const togglePreview = async () => {
instance.togglePreview();
@@ -43,8 +45,22 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- setHTMLFixture('<div id="editor" data-editor-loading></div>');
+ setHTMLFixture(
+ '<div style="width: 500px; height: 500px"><div id="editor" data-editor-loading></div></div>',
+ );
editorEl = document.getElementById('editor');
+ global.ResizeObserver = class {
+ constructor(callback) {
+ resizeCallback = callback;
+ this.observe = (node) => {
+ return observeSpy(node);
+ };
+ this.disconnect = () => {
+ return disconnectSpy();
+ };
+ }
+ };
+
editor = new SourceEditor();
instance = editor.createInstance({
el: editorEl,
@@ -77,9 +93,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
actions: expect.any(Object),
shown: false,
modelChangeListener: undefined,
- layoutChangeListener: {
- dispose: expect.anything(),
- },
path: previewMarkdownPath,
actionShowPreviewCondition: expect.any(Object),
eventEmitter: expect.any(Object),
@@ -94,36 +107,64 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(panelSpy).toHaveBeenCalled();
});
- describe('onDidLayoutChange', () => {
- const emitter = new Emitter();
- let layoutSpy;
+ describe('ResizeObserver handler', () => {
+ it('sets a ResizeObserver to observe the container DOM node', () => {
+ observeSpy.mockClear();
+ instance.togglePreview();
+ expect(observeSpy).toHaveBeenCalledWith(instance.getContainerDomNode());
+ });
- useFakeRequestAnimationFrame();
+ describe('disconnects the ResizeObserver when…', () => {
+ beforeEach(() => {
+ instance.togglePreview();
+ instance.markdownPreview.modelChangeListener = {
+ dispose: jest.fn(),
+ };
+ });
- beforeEach(() => {
- instance.unuse(extension);
- instance.onDidLayoutChange = emitter.event;
- extension = instance.use({
- definition: EditorMarkdownPreviewExtension,
- setupOptions: { previewMarkdownPath },
+ it('the preview gets closed', () => {
+ expect(disconnectSpy).not.toHaveBeenCalled();
+ instance.togglePreview();
+ expect(disconnectSpy).toHaveBeenCalled();
});
- layoutSpy = jest.spyOn(instance, 'layout');
- });
- it('does not trigger the layout when the preview is not active [default]', async () => {
- expect(instance.markdownPreview.shown).toBe(false);
- expect(layoutSpy).not.toHaveBeenCalled();
- await emitter.fire();
- expect(layoutSpy).not.toHaveBeenCalled();
+ it('the extension is unused', () => {
+ expect(disconnectSpy).not.toHaveBeenCalled();
+ instance.unuse(extension);
+ expect(disconnectSpy).toHaveBeenCalled();
+ });
});
- it('triggers the layout if the preview panel is opened', async () => {
- expect(layoutSpy).not.toHaveBeenCalled();
- instance.togglePreview();
- layoutSpy.mockReset();
+ describe('layout behavior', () => {
+ let layoutSpy;
+ let instanceDimensions;
+ let newInstanceWidth;
- await emitter.fire();
- expect(layoutSpy).toHaveBeenCalledTimes(1);
+ beforeEach(() => {
+ instanceDimensions = instance.getLayoutInfo();
+ });
+
+ it('does not trigger the layout if the preview panel is closed', () => {
+ layoutSpy = jest.spyOn(instance, 'layout');
+ newInstanceWidth = instanceDimensions.width + 100;
+
+ // Manually trigger the resize event
+ resizeCallback([{ contentRect: { width: newInstanceWidth } }]);
+ expect(layoutSpy).not.toHaveBeenCalled();
+ });
+
+ it('triggers the layout if the preview panel is opened, and width of the editor has changed', () => {
+ instance.togglePreview();
+ layoutSpy = jest.spyOn(instance, 'layout');
+ newInstanceWidth = instanceDimensions.width + 100;
+
+ // Manually trigger the resize event
+ resizeCallback([{ contentRect: { width: newInstanceWidth } }]);
+ expect(layoutSpy).toHaveBeenCalledWith({
+ width: newInstanceWidth * EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH,
+ height: instanceDimensions.height,
+ });
+ });
});
});
@@ -226,11 +267,10 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
expect(newWidth === width / EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH).toBe(true);
});
- it('disposes the layoutChange listener and does not re-layout on layout changes', () => {
- expect(instance.markdownPreview.layoutChangeListener).toBeDefined();
+ it('disconnects the ResizeObserver', () => {
instance.unuse(extension);
- expect(instance.markdownPreview?.layoutChangeListener).toBeUndefined();
+ expect(disconnectSpy).toHaveBeenCalled();
});
it('does not trigger the re-layout after instance is unused', async () => {
diff --git a/spec/frontend/editor/source_editor_webide_ext_spec.js b/spec/frontend/editor/source_editor_webide_ext_spec.js
index f418eab668a..7e4079c17f7 100644
--- a/spec/frontend/editor/source_editor_webide_ext_spec.js
+++ b/spec/frontend/editor/source_editor_webide_ext_spec.js
@@ -13,7 +13,6 @@ describe('Source Editor Web IDE Extension', () => {
editorEl = document.getElementById('editor');
editor = new SourceEditor();
});
- afterEach(() => {});
describe('onSetup', () => {
it.each`
diff --git a/spec/frontend/editor/utils_spec.js b/spec/frontend/editor/utils_spec.js
index e561cad1086..c9d6cbcaaa6 100644
--- a/spec/frontend/editor/utils_spec.js
+++ b/spec/frontend/editor/utils_spec.js
@@ -1,6 +1,8 @@
import { editor as monacoEditor } from 'monaco-editor';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import * as utils from '~/editor/utils';
+import languages from '~/ide/lib/languages';
+import { registerLanguages } from '~/ide/utils';
import { DEFAULT_THEME } from '~/ide/lib/themes';
describe('Source Editor utils', () => {
@@ -53,20 +55,24 @@ describe('Source Editor utils', () => {
});
describe('getBlobLanguage', () => {
+ beforeEach(() => {
+ registerLanguages(...languages);
+ });
+
it.each`
- path | expectedLanguage
- ${'foo.js'} | ${'javascript'}
- ${'foo.js.rb'} | ${'ruby'}
- ${'foo.bar'} | ${'plaintext'}
- ${undefined} | ${'plaintext'}
- `(
- 'sets the $expectedThemeName theme when $themeName is set in the user preference',
- ({ path, expectedLanguage }) => {
- const language = utils.getBlobLanguage(path);
+ path | expectedLanguage
+ ${'foo.js'} | ${'javascript'}
+ ${'foo.js.rb'} | ${'ruby'}
+ ${'foo.bar'} | ${'plaintext'}
+ ${undefined} | ${'plaintext'}
+ ${'foo/bar/foo.js'} | ${'javascript'}
+ ${'CODEOWNERS'} | ${'codeowners'}
+ ${'.gitlab/CODEOWNERS'} | ${'codeowners'}
+ `(`returns '$expectedLanguage' for '$path' path`, ({ path, expectedLanguage }) => {
+ const language = utils.getBlobLanguage(path);
- expect(language).toEqual(expectedLanguage);
- },
- );
+ expect(language).toEqual(expectedLanguage);
+ });
});
describe('setupCodeSnipet', () => {
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index 3e9b49707ed..65f2e813f19 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -119,7 +119,7 @@ describe('Awards app actions', () => {
mock.onPost(`${relativeRootUrl || ''}/awards`).reply(HTTP_STATUS_OK, { id: 1 });
});
- it('adds an optimistic award, removes it, and then commits ADD_NEW_AWARD', async () => {
+ it('adds an optimistic award, removes it, and then commits ADD_NEW_AWARD', () => {
testAction(actions.toggleAward, null, { path: '/awards', awards: [] }, [
makeOptimisticAddMutation(),
makeOptimisticRemoveMutation(),
@@ -156,7 +156,7 @@ describe('Awards app actions', () => {
mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(HTTP_STATUS_OK);
});
- it('commits REMOVE_AWARD', async () => {
+ it('commits REMOVE_AWARD', () => {
testAction(
actions.toggleAward,
'thumbsup',
diff --git a/spec/frontend/emoji/components/category_spec.js b/spec/frontend/emoji/components/category_spec.js
index 90816f28d5b..272c1a09a69 100644
--- a/spec/frontend/emoji/components/category_spec.js
+++ b/spec/frontend/emoji/components/category_spec.js
@@ -9,11 +9,12 @@ function factory(propsData = {}) {
wrapper = shallowMount(Category, { propsData });
}
-describe('Emoji category component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
+const triggerGlIntersectionObserver = () => {
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
+ return nextTick();
+};
+describe('Emoji category component', () => {
beforeEach(() => {
factory({
category: 'Activity',
@@ -26,25 +27,19 @@ describe('Emoji category component', () => {
});
it('renders group', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ renderGroup: true });
+ await triggerGlIntersectionObserver();
expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('renders group on appear', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
-
- await nextTick();
+ await triggerGlIntersectionObserver();
expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true');
});
it('emits appear event on appear', async () => {
- wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
-
- await nextTick();
+ await triggerGlIntersectionObserver();
expect(wrapper.emitted().appear[0]).toEqual(['Activity']);
});
diff --git a/spec/frontend/emoji/components/emoji_group_spec.js b/spec/frontend/emoji/components/emoji_group_spec.js
index 1aca2fbb8fc..75397ce25ff 100644
--- a/spec/frontend/emoji/components/emoji_group_spec.js
+++ b/spec/frontend/emoji/components/emoji_group_spec.js
@@ -15,10 +15,6 @@ function factory(propsData = {}) {
}
describe('Emoji group component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not render any buttons', () => {
factory({
emojis: [],
diff --git a/spec/frontend/emoji/components/emoji_list_spec.js b/spec/frontend/emoji/components/emoji_list_spec.js
index a72ba614d9f..f6f6062f8e8 100644
--- a/spec/frontend/emoji/components/emoji_list_spec.js
+++ b/spec/frontend/emoji/components/emoji_list_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EmojiList from '~/emoji/components/emoji_list.vue';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/emoji', () => ({
initEmojiMap: jest.fn(() => Promise.resolve()),
@@ -14,7 +14,8 @@ jest.mock('~/emoji', () => ({
}));
let wrapper;
-async function factory(render, propsData = { searchValue: '' }) {
+
+function factory(propsData = { searchValue: '' }) {
wrapper = extendedWrapper(
shallowMount(EmojiList, {
propsData,
@@ -23,35 +24,23 @@ async function factory(render, propsData = { searchValue: '' }) {
},
}),
);
-
- // Wait for categories to be set
- await nextTick();
-
- if (render) {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ render: true });
-
- // Wait for component to render
- await nextTick();
- }
}
const findDefaultSlot = () => wrapper.findByTestId('default-slot');
describe('Emoji list component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not render until render is set', async () => {
- await factory(false);
+ factory();
expect(findDefaultSlot().exists()).toBe(false);
+ await waitForPromises();
+ expect(findDefaultSlot().exists()).toBe(true);
});
it('renders with none filtered list', async () => {
- await factory(true);
+ factory();
+
+ await waitForPromises();
expect(JSON.parse(findDefaultSlot().text())).toEqual({
activity: {
@@ -63,7 +52,9 @@ describe('Emoji list component', () => {
});
it('renders filtered list of emojis', async () => {
- await factory(true, { searchValue: 'smile' });
+ factory({ searchValue: 'smile' });
+
+ await waitForPromises();
expect(JSON.parse(findDefaultSlot().text())).toEqual({
search: {
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 82e3b50aeb8..4e341b2bb2f 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -8,6 +8,7 @@ const {
setGlobalDateToRealDate,
} = require('./__helpers__/fake_date/fake_date');
const { TEST_HOST } = require('./__helpers__/test_constants');
+const { createGon } = require('./__helpers__/gon_helper');
const ROOT_PATH = path.resolve(__dirname, '../..');
@@ -20,8 +21,17 @@ class CustomEnvironment extends TestEnvironment {
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39496#note_503084332
setGlobalDateToFakeDate();
+ const { error: originalErrorFn } = context.console;
Object.assign(context.console, {
error(...args) {
+ if (
+ args?.[0]?.includes('[Vue warn]: Missing required prop') ||
+ args?.[0]?.includes('[Vue warn]: Invalid prop')
+ ) {
+ originalErrorFn.apply(context.console, args);
+ return;
+ }
+
throw new ErrorWithStack(
`Unexpected call of console.error() with:\n\n${args.join(', ')}`,
this.error,
@@ -29,7 +39,7 @@ class CustomEnvironment extends TestEnvironment {
},
warn(...args) {
- if (args[0].includes('The updateQuery callback for fetchMore is deprecated')) {
+ if (args?.[0]?.includes('The updateQuery callback for fetchMore is deprecated')) {
return;
}
throw new ErrorWithStack(
@@ -40,11 +50,12 @@ class CustomEnvironment extends TestEnvironment {
});
const { IS_EE } = projectConfig.testEnvironmentOptions;
- this.global.gon = {
- ee: IS_EE,
- };
+
this.global.IS_EE = IS_EE;
+ // Set up global `gon` object
+ this.global.gon = createGon(IS_EE);
+
// Set up global `gl` object
this.global.gl = {};
diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js
index 340740e6499..e0247731b63 100644
--- a/spec/frontend/environments/canary_ingress_spec.js
+++ b/spec/frontend/environments/canary_ingress_spec.js
@@ -23,7 +23,7 @@ describe('/environments/components/canary_ingress.vue', () => {
...props,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
...options,
});
@@ -33,14 +33,6 @@ describe('/environments/components/canary_ingress.vue', () => {
createComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
describe('stable weight', () => {
let stableWeightDropdown;
diff --git a/spec/frontend/environments/canary_update_modal_spec.js b/spec/frontend/environments/canary_update_modal_spec.js
index 31b1770da59..4fa7b34d817 100644
--- a/spec/frontend/environments/canary_update_modal_spec.js
+++ b/spec/frontend/environments/canary_update_modal_spec.js
@@ -30,14 +30,6 @@ describe('/environments/components/canary_update_modal.vue', () => {
modal = wrapper.findComponent(GlModal);
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
beforeEach(() => {
createComponent();
});
@@ -47,7 +39,7 @@ describe('/environments/components/canary_update_modal.vue', () => {
modalId: 'confirm-canary-change',
actionPrimary: {
text: 'Change ratio',
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
},
actionCancel: { text: 'Cancel' },
});
diff --git a/spec/frontend/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js
index 2163814528a..d6601447cff 100644
--- a/spec/frontend/environments/confirm_rollback_modal_spec.js
+++ b/spec/frontend/environments/confirm_rollback_modal_spec.js
@@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo';
import { trimText } from 'helpers/text_helper';
import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import eventHub from '~/environments/event_hub';
describe('Confirm Rollback Modal Component', () => {
@@ -53,6 +54,8 @@ describe('Confirm Rollback Modal Component', () => {
});
};
+ const findModal = () => component.findComponent(GlModal);
+
describe.each`
hasMultipleCommits | environmentData | retryUrl | primaryPropsAttrs
${true} | ${envWithLastDeployment} | ${null} | ${[{ variant: 'danger' }]}
@@ -73,7 +76,7 @@ describe('Confirm Rollback Modal Component', () => {
hasMultipleCommits,
retryUrl,
});
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
@@ -92,7 +95,7 @@ describe('Confirm Rollback Modal Component', () => {
hasMultipleCommits,
});
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
@@ -110,7 +113,7 @@ describe('Confirm Rollback Modal Component', () => {
});
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
modal.vm.$emit('ok');
expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', env);
@@ -155,7 +158,7 @@ describe('Confirm Rollback Modal Component', () => {
},
{ apolloProvider },
);
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(trimText(modal.text())).toContain('commit abc0123');
expect(modal.text()).toContain('Are you sure you want to continue?');
@@ -177,7 +180,7 @@ describe('Confirm Rollback Modal Component', () => {
},
{ apolloProvider },
);
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(modal.attributes('title')).toContain('Rollback');
expect(modal.attributes('title')).toContain('test');
@@ -201,7 +204,7 @@ describe('Confirm Rollback Modal Component', () => {
{ apolloProvider },
);
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
expect(modal.attributes('title')).toContain('Re-deploy');
expect(modal.attributes('title')).toContain('test');
@@ -220,7 +223,7 @@ describe('Confirm Rollback Modal Component', () => {
{ apolloProvider },
);
- const modal = component.findComponent(GlModal);
+ const modal = findModal();
modal.vm.$emit('ok');
await nextTick();
@@ -231,6 +234,25 @@ describe('Confirm Rollback Modal Component', () => {
expect.anything(),
);
});
+
+ it('should emit the "rollback" event when "ok" is clicked', async () => {
+ const env = { ...environmentData, isLastDeployment: true };
+
+ createComponent(
+ {
+ environment: env,
+ hasMultipleCommits,
+ graphql: true,
+ },
+ { apolloProvider },
+ );
+
+ const modal = findModal();
+ modal.vm.$emit('ok');
+
+ await waitForPromises();
+ expect(component.emitted('rollback')).toEqual([[]]);
+ });
},
);
});
diff --git a/spec/frontend/environments/delete_environment_modal_spec.js b/spec/frontend/environments/delete_environment_modal_spec.js
index cc18bf754eb..96f6ce52a9c 100644
--- a/spec/frontend/environments/delete_environment_modal_spec.js
+++ b/spec/frontend/environments/delete_environment_modal_spec.js
@@ -6,10 +6,10 @@ import { s__, sprintf } from '~/locale';
import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { resolvedEnvironment } from './graphql/mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
describe('~/environments/components/delete_environment_modal.vue', () => {
@@ -67,7 +67,7 @@ describe('~/environments/components/delete_environment_modal.vue', () => {
);
});
- it('should flash a message on error', async () => {
+ it('should alert a message on error', async () => {
createComponent({ apolloProvider: mockApollo });
deleteResolver.mockRejectedValue();
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index 73a366457fb..f50efada91a 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -61,7 +61,7 @@ describe('Deploy Board', () => {
const icon = iconSpan.findComponent(GlIcon);
expect(tooltip.props('target')()).toBe(iconSpan.element);
- expect(icon.props('name')).toBe('question');
+ expect(icon.props('name')).toBe('question-o');
});
it('renders the canary weight selector', () => {
@@ -116,7 +116,7 @@ describe('Deploy Board', () => {
const icon = iconSpan.findComponent(GlIcon);
expect(tooltip.props('target')()).toBe(iconSpan.element);
- expect(icon.props('name')).toBe('question');
+ expect(icon.props('name')).toBe('question-o');
});
it('renders the canary weight selector', () => {
diff --git a/spec/frontend/environments/deploy_freeze_alert_spec.js b/spec/frontend/environments/deploy_freeze_alert_spec.js
new file mode 100644
index 00000000000..b7202253e61
--- /dev/null
+++ b/spec/frontend/environments/deploy_freeze_alert_spec.js
@@ -0,0 +1,111 @@
+import { GlAlert, GlLink } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeployFreezeAlert from '~/environments/components/deploy_freeze_alert.vue';
+import deployFreezesQuery from '~/environments/graphql/queries/deploy_freezes.query.graphql';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
+
+const ENVIRONMENT_NAME = 'staging';
+
+Vue.use(VueApollo);
+describe('~/environments/components/deploy_freeze_alert.vue', () => {
+ let wrapper;
+
+ const createWrapper = (deployFreezes = []) => {
+ const mockApollo = createMockApollo([
+ [
+ deployFreezesQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ __typename: 'Project',
+ environment: {
+ id: '1',
+ __typename: 'Environment',
+ deployFreezes,
+ },
+ },
+ },
+ }),
+ ],
+ ]);
+ wrapper = mountExtended(DeployFreezeAlert, {
+ apolloProvider: mockApollo,
+ provide: {
+ projectFullPath: 'gitlab-org/gitlab',
+ },
+ propsData: {
+ name: ENVIRONMENT_NAME,
+ },
+ });
+ };
+
+ describe('with deploy freezes', () => {
+ let deployFreezes;
+ let alert;
+
+ beforeEach(async () => {
+ deployFreezes = [
+ {
+ __typename: 'CiFreezePeriod',
+ startTime: new Date('2020-02-01'),
+ endTime: new Date('2020-02-02'),
+ },
+ {
+ __typename: 'CiFreezePeriod',
+ startTime: new Date('2020-01-01'),
+ endTime: new Date('2020-01-02'),
+ },
+ ];
+
+ createWrapper(deployFreezes);
+
+ await waitForPromises();
+
+ alert = wrapper.findComponent(GlAlert);
+ });
+
+ it('shows an alert', () => {
+ expect(alert.exists()).toBe(true);
+ });
+
+ it('shows the start time of the most recent freeze period', () => {
+ expect(alert.text()).toContain(`from ${formatDate(deployFreezes[1].startTime)}`);
+ });
+
+ it('shows the end time of the most recent freeze period', () => {
+ expect(alert.text()).toContain(`to ${formatDate(deployFreezes[1].endTime)}`);
+ });
+
+ it('shows a link to the docs', () => {
+ const link = alert.findComponent(GlLink);
+ expect(link.attributes('href')).toBe(
+ '/help/user/project/releases/index#prevent-unintentional-releases-by-setting-a-deploy-freeze',
+ );
+ expect(link.text()).toBe('deploy freeze documentation');
+ });
+ });
+
+ describe('without deploy freezes', () => {
+ let deployFreezes;
+ let alert;
+
+ beforeEach(async () => {
+ deployFreezes = [];
+
+ createWrapper(deployFreezes);
+
+ await waitForPromises();
+
+ alert = wrapper.findComponent(GlAlert);
+ });
+
+ it('does not show an alert', () => {
+ expect(alert.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index fb1a8b8c00a..34f338fabe6 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import EditEnvironment from '~/environments/components/edit_environment.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
const DEFAULT_OPTS = {
provide: {
@@ -37,7 +37,6 @@ describe('~/environments/components/edit.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
const findNameInput = () => wrapper.findByLabelText('Name');
diff --git a/spec/frontend/environments/empty_state_spec.js b/spec/frontend/environments/empty_state_spec.js
index 02cf2dc3c68..593200859e4 100644
--- a/spec/frontend/environments/empty_state_spec.js
+++ b/spec/frontend/environments/empty_state_spec.js
@@ -11,12 +11,17 @@ describe('~/environments/components/empty_state.vue', () => {
const findNewEnvironmentLink = () =>
wrapper.findByRole('link', {
- name: s__('Environments|New environment'),
+ name: s__('Environments|Create an environment'),
});
const findDocsLink = () =>
wrapper.findByRole('link', {
- name: s__('Environments|How do I create an environment?'),
+ name: 'Learn more',
+ });
+
+ const finfEnablingReviewButton = () =>
+ wrapper.findByRole('button', {
+ name: s__('Environments|Enable review apps'),
});
const createWrapper = ({ propsData = {} } = {}) =>
@@ -29,42 +34,44 @@ describe('~/environments/components/empty_state.vue', () => {
provide: { newEnvironmentPath: NEW_PATH },
});
- afterEach(() => {
- wrapper.destroy();
- });
+ describe('without search term', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
- it('shows an empty state for available environments', () => {
- wrapper = createWrapper();
+ it('shows an empty state environments', () => {
+ const title = wrapper.findByRole('heading', {
+ name: s__('Environments|Get started with environments'),
+ });
- const title = wrapper.findByRole('heading', {
- name: s__("Environments|You don't have any environments."),
+ expect(title.exists()).toBe(true);
});
- expect(title.exists()).toBe(true);
- });
-
- it('shows an empty state for stopped environments', () => {
- wrapper = createWrapper({ propsData: { scope: ENVIRONMENTS_SCOPE.STOPPED } });
+ it('shows a link to the the help path', () => {
+ const link = findDocsLink();
- const title = wrapper.findByRole('heading', {
- name: s__("Environments|You don't have any stopped environments."),
+ expect(link.attributes('href')).toBe(HELP_PATH);
});
- expect(title.exists()).toBe(true);
- });
+ it('shows a link to creating a new environment', () => {
+ const link = findNewEnvironmentLink();
- it('shows a link to the the help path', () => {
- wrapper = createWrapper();
+ expect(link.attributes('href')).toBe(NEW_PATH);
+ });
- const link = findDocsLink();
+ it('shows a button to enable review apps', () => {
+ const button = finfEnablingReviewButton();
- expect(link.attributes('href')).toBe(HELP_PATH);
- });
+ expect(button.exists()).toBe(true);
+ });
+
+ it('should emit enable review', () => {
+ const button = finfEnablingReviewButton();
- it('hides a link to creating a new environment', () => {
- const link = findNewEnvironmentLink();
+ button.vm.$emit('click');
- expect(link.exists()).toBe(false);
+ expect(wrapper.emitted('enable-review')).toBeDefined();
+ });
});
describe('with search term', () => {
@@ -90,10 +97,16 @@ describe('~/environments/components/empty_state.vue', () => {
expect(link.exists()).toBe(false);
});
- it('shows a link to create a new environment', () => {
+ it('hide a link to create a new environment', () => {
const link = findNewEnvironmentLink();
- expect(link.attributes('href')).toBe(NEW_PATH);
+ expect(link.exists()).toBe(false);
+ });
+
+ it('hide a button to enable review apps', () => {
+ const button = finfEnablingReviewButton();
+
+ expect(button.exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/environments/enable_review_app_modal_spec.js b/spec/frontend/environments/enable_review_app_modal_spec.js
index 7939bd600dc..f5571609931 100644
--- a/spec/frontend/environments/enable_review_app_modal_spec.js
+++ b/spec/frontend/environments/enable_review_app_modal_spec.js
@@ -10,7 +10,7 @@ jest.mock('lodash/uniqueId', () => (x) => `${x}77`);
const EXPECTED_COPY_PRE_ID = 'enable-review-app-copy-string-77';
-describe('Enable Review App Modal', () => {
+describe('Enable Review Apps Modal', () => {
let wrapper;
let modal;
@@ -18,10 +18,6 @@ describe('Enable Review App Modal', () => {
const findInstructionAt = (i) => wrapper.findAll('ol li').at(i);
const findCopyString = () => wrapper.find(`#${EXPECTED_COPY_PRE_ID}`);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders the modal', () => {
beforeEach(() => {
wrapper = extendedWrapper(
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 48483152f7a..b7e192839da 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,14 +1,8 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
-import eventHub from '~/environments/event_hub';
-import actionMutation from '~/environments/graphql/mutations/action.mutation.graphql';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import createMockApollo from 'helpers/mock_apollo_helper';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
@@ -29,15 +23,9 @@ const expiredJobAction = {
describe('EnvironmentActions Component', () => {
let wrapper;
- const findEnvironmentActionsButton = () =>
- wrapper.find('[data-testid="environment-actions-button"]');
-
- function createComponent(props, { mountFn = shallowMount, options = {} } = {}) {
- wrapper = mountFn(EnvironmentActions, {
+ function createComponent(props, { options = {} } = {}) {
+ wrapper = mount(EnvironmentActions, {
propsData: { actions: [], ...props },
- directives: {
- GlTooltip: createMockDirective(),
- },
...options,
});
}
@@ -46,30 +34,26 @@ describe('EnvironmentActions Component', () => {
return createComponent({ actions: [scheduledJobAction, expiredJobAction] }, opts);
}
+ const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findDropdownItem = (action) => {
- const buttons = wrapper.findAllComponents(GlDropdownItem);
- return buttons.filter((button) => button.text().startsWith(action.name)).at(0);
+ const items = findDropdownItems();
+ return items.filter((item) => item.text().startsWith(action.name)).at(0);
};
afterEach(() => {
- wrapper.destroy();
confirmAction.mockReset();
});
it('should render a dropdown button with 2 icons', () => {
- createComponent({}, { mountFn: mount });
- expect(wrapper.findComponent(GlDropdown).findAllComponents(GlIcon).length).toBe(2);
- });
-
- it('should render a dropdown button with aria-label description', () => {
createComponent();
- expect(wrapper.findComponent(GlDropdown).attributes('aria-label')).toBe('Deploy to...');
+ expect(wrapper.findComponent(GlDisclosureDropdown).findAllComponents(GlIcon).length).toBe(2);
});
- it('should render a tooltip', () => {
+ it('should render a dropdown button with aria-label description', () => {
createComponent();
- const tooltip = getBinding(findEnvironmentActionsButton().element, 'gl-tooltip');
- expect(tooltip).toBeDefined();
+ expect(wrapper.findComponent(GlDisclosureDropdown).attributes('aria-label')).toBe(
+ 'Deploy to...',
+ );
});
describe('manual actions', () => {
@@ -94,96 +78,31 @@ describe('EnvironmentActions Component', () => {
});
it('should render a dropdown with the provided list of actions', () => {
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(actions.length);
+ expect(findDropdownItems()).toHaveLength(actions.length);
});
it("should render a disabled action when it's not playable", () => {
- const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
+ const dropdownItems = findDropdownItems();
const lastDropdownItem = dropdownItems.at(dropdownItems.length - 1);
- expect(lastDropdownItem.attributes('disabled')).toBe('true');
+ expect(lastDropdownItem.find('button').attributes('disabled')).toBeDefined();
});
});
describe('scheduled jobs', () => {
- let emitSpy;
-
- const clickAndConfirm = async ({ confirm = true } = {}) => {
- confirmAction.mockResolvedValueOnce(confirm);
-
- findDropdownItem(scheduledJobAction).vm.$emit('click');
- await nextTick();
- };
-
beforeEach(() => {
- emitSpy = jest.fn();
- eventHub.$on('postAction', emitSpy);
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
});
- describe('when postAction event is confirmed', () => {
- beforeEach(async () => {
- createComponentWithScheduledJobs({ mountFn: mount });
- clickAndConfirm();
- });
-
- it('emits postAction event', () => {
- expect(confirmAction).toHaveBeenCalled();
- expect(emitSpy).toHaveBeenCalledWith({ endpoint: scheduledJobAction.playPath });
- });
-
- it('should render a dropdown button with a loading icon', () => {
- expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true);
- });
- });
-
- describe('when postAction event is denied', () => {
- beforeEach(async () => {
- createComponentWithScheduledJobs({ mountFn: mount });
- clickAndConfirm({ confirm: false });
- });
-
- it('does not emit postAction event if confirmation is cancelled', () => {
- expect(confirmAction).toHaveBeenCalled();
- expect(emitSpy).not.toHaveBeenCalled();
- });
- });
-
it('displays the remaining time in the dropdown', () => {
+ confirmAction.mockResolvedValueOnce(true);
createComponentWithScheduledJobs();
expect(findDropdownItem(scheduledJobAction).text()).toContain('24:00:00');
});
it('displays 00:00:00 for expired jobs in the dropdown', () => {
+ confirmAction.mockResolvedValueOnce(true);
createComponentWithScheduledJobs();
expect(findDropdownItem(expiredJobAction).text()).toContain('00:00:00');
});
});
-
- describe('graphql', () => {
- Vue.use(VueApollo);
-
- const action = {
- name: 'bar',
- play_path: 'https://gitlab.com/play',
- };
-
- let mockApollo;
-
- beforeEach(() => {
- mockApollo = createMockApollo();
- createComponent(
- { actions: [action], graphql: true },
- { options: { apolloProvider: mockApollo } },
- );
- });
-
- it('should trigger a graphql mutation on click', () => {
- jest.spyOn(mockApollo.defaultClient, 'mutate');
- findDropdownItem(action).vm.$emit('click');
- expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
- mutation: actionMutation,
- variables: { action },
- });
- });
- });
});
diff --git a/spec/frontend/environments/environment_details/components/deployment_actions_spec.js b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
index 725c8c6479e..a0eb4c494e6 100644
--- a/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
@@ -1,8 +1,15 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlButton } from '@gitlab/ui';
import DeploymentActions from '~/environments/environment_details/components/deployment_actions.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { translations } from '~/environments/environment_details/constants';
import ActionsComponent from '~/environments/components/environment_actions.vue';
describe('~/environments/environment_details/components/deployment_actions.vue', () => {
+ Vue.use(VueApollo);
let wrapper;
const actionsData = [
@@ -14,34 +21,116 @@ describe('~/environments/environment_details/components/deployment_actions.vue',
},
];
- const createWrapper = ({ actions }) => {
+ const rollbackData = {
+ id: '123',
+ name: 'enironment-name',
+ lastDeployment: {
+ commit: {
+ shortSha: 'abcd1234',
+ },
+ isLast: true,
+ },
+ retryUrl: 'deployment/retry',
+ };
+
+ const mockSetEnvironmentToRollback = jest.fn();
+ const mockResolvers = {
+ Mutation: {
+ setEnvironmentToRollback: mockSetEnvironmentToRollback,
+ },
+ };
+ const createWrapper = ({ actions, rollback, approvalEnvironment }) => {
+ const mockApollo = createMockApollo([], mockResolvers);
return mountExtended(DeploymentActions, {
+ apolloProvider: mockApollo,
+ provide: {
+ projectPath: 'fullProjectPath',
+ },
propsData: {
actions,
+ rollback,
+ approvalEnvironment,
},
});
};
- describe('when there is no actions provided', () => {
- beforeEach(() => {
- wrapper = createWrapper({ actions: [] });
+ const findRollbackButton = () => wrapper.findComponent(GlButton);
+
+ describe('deployment actions', () => {
+ describe('when there is no actions provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ actions: [] });
+ });
+
+ it('should not render actions component', () => {
+ const actionsComponent = wrapper.findComponent(ActionsComponent);
+ expect(actionsComponent.exists()).toBe(false);
+ });
});
- it('should not render actions component', () => {
- const actionsComponent = wrapper.findComponent(ActionsComponent);
- expect(actionsComponent.exists()).toBe(false);
+ describe('when there are actions provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ actions: actionsData });
+ });
+
+ it('should render actions component', () => {
+ const actionsComponent = wrapper.findComponent(ActionsComponent);
+ expect(actionsComponent.exists()).toBe(true);
+ expect(actionsComponent.props().actions).toBe(actionsData);
+ });
});
});
- describe('when there are actions provided', () => {
- beforeEach(() => {
- wrapper = createWrapper({ actions: actionsData });
+ describe('rollback action', () => {
+ describe('when there is no rollback data available', () => {
+ it('should not show a rollback button', () => {
+ wrapper = createWrapper({ actions: [] });
+ const button = findRollbackButton();
+ expect(button.exists()).toBe(false);
+ });
});
- it('should render actions component', () => {
- const actionsComponent = wrapper.findComponent(ActionsComponent);
- expect(actionsComponent.exists()).toBe(true);
- expect(actionsComponent.props().actions).toBe(actionsData);
- });
+ describe.each([
+ { isLast: true, buttonTitle: translations.redeployButtonTitle, icon: 'repeat' },
+ { isLast: false, buttonTitle: translations.rollbackButtonTitle, icon: 'redo' },
+ ])(
+ `when there is a rollback data available and the deployment isLast=$isLast`,
+ ({ isLast, buttonTitle, icon }) => {
+ let rollback;
+ beforeEach(() => {
+ const lastDeployment = { ...rollbackData.lastDeployment, isLast };
+ rollback = { ...rollbackData };
+ rollback.lastDeployment = lastDeployment;
+ wrapper = createWrapper({ actions: [], rollback });
+ });
+
+ it('should show the rollback button', () => {
+ const button = findRollbackButton();
+ expect(button.exists()).toBe(true);
+ });
+
+ it(`the rollback button should have "${icon}" icon`, () => {
+ const button = findRollbackButton();
+ expect(button.props().icon).toBe(icon);
+ });
+
+ it(`the rollback button should have "${buttonTitle}" title`, () => {
+ const button = findRollbackButton();
+ expect(button.attributes().title).toBe(buttonTitle);
+ });
+
+ it(`the rollback button click should send correct mutation`, async () => {
+ const button = findRollbackButton();
+ button.vm.$emit('click');
+ await waitForPromises();
+ expect(mockSetEnvironmentToRollback).toHaveBeenCalledWith(
+ expect.anything(),
+ { environment: rollback },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ },
+ );
});
});
diff --git a/spec/frontend/environments/environment_details/deployments_table_spec.js b/spec/frontend/environments/environment_details/deployments_table_spec.js
new file mode 100644
index 00000000000..7dad5617383
--- /dev/null
+++ b/spec/frontend/environments/environment_details/deployments_table_spec.js
@@ -0,0 +1,58 @@
+import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Commit from '~/vue_shared/components/commit.vue';
+import DeploymentStatusLink from '~/environments/environment_details/components/deployment_status_link.vue';
+import DeploymentJob from '~/environments/environment_details/components/deployment_job.vue';
+import DeploymentTriggerer from '~/environments/environment_details/components/deployment_triggerer.vue';
+import DeploymentActions from '~/environments/environment_details/components/deployment_actions.vue';
+import DeploymentsTable from '~/environments/environment_details/deployments_table.vue';
+import { convertToDeploymentTableRow } from '~/environments/helpers/deployment_data_transformation_helper';
+
+const { environment } = resolvedEnvironmentDetails.data.project;
+const deployments = environment.deployments.nodes.map((d) =>
+ convertToDeploymentTableRow(d, environment),
+);
+
+describe('~/environments/environment_details/index.vue', () => {
+ let wrapper;
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = mountExtended(DeploymentsTable, {
+ propsData: {
+ deployments,
+ ...propsData,
+ },
+ });
+ };
+
+ describe('deployment row', () => {
+ const [, , deployment] = deployments;
+
+ let row;
+
+ beforeEach(() => {
+ createWrapper();
+
+ row = wrapper.find('tr:nth-child(3)');
+ });
+
+ it.each`
+ cell | component | props
+ ${'status'} | ${DeploymentStatusLink} | ${{ deploymentJob: deployment.job, status: deployment.status }}
+ ${'triggerer'} | ${DeploymentTriggerer} | ${{ triggerer: deployment.triggerer }}
+ ${'commit'} | ${Commit} | ${deployment.commit}
+ ${'job'} | ${DeploymentJob} | ${{ job: deployment.job }}
+ ${'created date'} | ${'[data-testid="deployment-created-at"]'} | ${{ time: deployment.created }}
+ ${'deployed date'} | ${'[data-testid="deployment-deployed-at"]'} | ${{ time: deployment.deployed }}
+ ${'deployment actions'} | ${DeploymentActions} | ${{ actions: deployment.actions, rollback: deployment.rollback, approvalEnvironment: deployment.deploymentApproval }}
+ `('should show the correct component for $cell', ({ component, props }) => {
+ expect(row.findComponent(component).props()).toMatchObject(props);
+ });
+
+ it('hides the deployed at timestamp for not-finished deployments', () => {
+ row = wrapper.find('tr');
+
+ expect(row.find('[data-testid="deployment-deployed-at"]').exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/page_spec.js b/spec/frontend/environments/environment_details/index_spec.js
index 3a1a3238abe..4bf5194b86e 100644
--- a/spec/frontend/environments/environment_details/page_spec.js
+++ b/spec/frontend/environments/environment_details/index_spec.js
@@ -5,31 +5,63 @@ import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graph
import emptyEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.empty.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentsDetailPage from '~/environments/environment_details/index.vue';
+import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
import EmptyState from '~/environments/environment_details/empty_state.vue';
import getEnvironmentDetails from '~/environments/graphql/queries/environment_details.query.graphql';
-import createMockApollo from '../../__helpers__/mock_apollo_helper';
-import waitForPromises from '../../__helpers__/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
-describe('~/environments/environment_details/page.vue', () => {
+const GRAPHQL_ETAG_KEY = '/graphql/environments';
+
+describe('~/environments/environment_details/index.vue', () => {
Vue.use(VueApollo);
let wrapper;
+ let routerMock;
+
+ const emptyEnvironmentToRollbackData = { id: '', name: '', lastDeployment: null, retryUrl: '' };
+ const environmentToRollbackMock = jest.fn();
+
+ const mockResolvers = {
+ Query: {
+ environmentToRollback: environmentToRollbackMock,
+ },
+ };
const defaultWrapperParameters = {
resolvedData: resolvedEnvironmentDetails,
+ environmentToRollbackData: emptyEnvironmentToRollbackData,
};
- const createWrapper = ({ resolvedData } = defaultWrapperParameters) => {
- const mockApollo = createMockApollo([
- [getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedData)],
- ]);
+ const createWrapper = ({
+ resolvedData,
+ environmentToRollbackData,
+ } = defaultWrapperParameters) => {
+ const mockApollo = createMockApollo(
+ [[getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedData)]],
+ mockResolvers,
+ );
+ environmentToRollbackMock.mockReturnValue(
+ environmentToRollbackData || emptyEnvironmentToRollbackData,
+ );
+ const projectFullPath = 'gitlab-group/test-project';
+ routerMock = {
+ push: jest.fn(),
+ };
return mountExtended(EnvironmentsDetailPage, {
apolloProvider: mockApollo,
+ provide: {
+ projectPath: projectFullPath,
+ graphqlEtagKey: GRAPHQL_ETAG_KEY,
+ },
propsData: {
- projectFullPath: 'gitlab-group/test-project',
+ projectFullPath,
environmentName: 'test-environment-name',
},
+ mocks: {
+ $router: routerMock,
+ },
});
};
@@ -48,10 +80,18 @@ describe('~/environments/environment_details/page.vue', () => {
wrapper = createWrapper();
await waitForPromises();
});
- it('should render a table when query is loaded', async () => {
+ it('should render a table when query is loaded', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).not.toBe(true);
expect(wrapper.findComponent(GlTableLite).exists()).toBe(true);
});
+
+ describe('on rollback', () => {
+ it('sets the page back to default', () => {
+ wrapper.findComponent(ConfirmRollbackModal).vm.$emit('rollback');
+
+ expect(routerMock.push).toHaveBeenCalledWith({ query: {} });
+ });
+ });
});
describe('and there are no deployments', () => {
@@ -60,7 +100,7 @@ describe('~/environments/environment_details/page.vue', () => {
await waitForPromises();
});
- it('should render empty state component', async () => {
+ it('should render empty state component', () => {
expect(wrapper.findComponent(GlTableLite).exists()).toBe(false);
expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
});
diff --git a/spec/frontend/environments/environment_external_url_spec.js b/spec/frontend/environments/environment_external_url_spec.js
index 5966993166b..2cccbb3b63c 100644
--- a/spec/frontend/environments/environment_external_url_spec.js
+++ b/spec/frontend/environments/environment_external_url_spec.js
@@ -1,35 +1,18 @@
import { mount } from '@vue/test-utils';
-import { s__, __ } from '~/locale';
+import { GlButton } from '@gitlab/ui';
import ExternalUrlComp from '~/environments/components/environment_external_url.vue';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('External URL Component', () => {
let wrapper;
- let externalUrl;
+ const externalUrl = 'https://gitlab.com';
- describe('with safe link', () => {
- beforeEach(() => {
- externalUrl = 'https://gitlab.com';
- wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
- });
-
- it('should link to the provided externalUrl prop', () => {
- expect(wrapper.attributes('href')).toBe(externalUrl);
- expect(wrapper.find('a').exists()).toBe(true);
- });
+ beforeEach(() => {
+ wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
});
- describe('with unsafe link', () => {
- beforeEach(() => {
- externalUrl = 'postgres://gitlab';
- wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
- });
-
- it('should show a copy button instead', () => {
- const button = wrapper.findComponent(ModalCopyButton);
- expect(button.props('text')).toBe(externalUrl);
- expect(button.text()).toBe(__('Copy URL'));
- expect(button.props('title')).toBe(s__('Environments|Copy live environment URL'));
- });
+ it('should link to the provided externalUrl prop', () => {
+ const button = wrapper.findComponent(GlButton);
+ expect(button.attributes('href')).toEqual(externalUrl);
+ expect(button.props('isUnsafeLink')).toBe(true);
});
});
diff --git a/spec/frontend/environments/environment_folder_spec.js b/spec/frontend/environments/environment_folder_spec.js
index a37515bc3f7..4716f807657 100644
--- a/spec/frontend/environments/environment_folder_spec.js
+++ b/spec/frontend/environments/environment_folder_spec.js
@@ -35,10 +35,10 @@ describe('~/environments/components/environments_folder.vue', () => {
...propsData,
},
stubs: { transition: stubTransition() },
- provide: { helpPagePath: '/help' },
+ provide: { helpPagePath: '/help', projectId: '1' },
});
- beforeEach(async () => {
+ beforeEach(() => {
environmentFolderMock = jest.fn();
[nestedEnvironment] = resolvedEnvironmentsApp.environments;
environmentFolderMock.mockReturnValue(resolvedFolder);
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index b9b34bee80f..50e4e637aa3 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -15,19 +15,16 @@ const PROVIDE = { protectedEnvironmentSettingsPath: '/projects/not_real/settings
describe('~/environments/components/form.vue', () => {
let wrapper;
- const createWrapper = (propsData = {}) =>
+ const createWrapper = (propsData = {}, options = {}) =>
mountExtended(EnvironmentForm, {
provide: PROVIDE,
+ ...options,
propsData: {
...DEFAULT_PROPS,
...propsData,
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
wrapper = createWrapper();
@@ -105,6 +102,7 @@ describe('~/environments/components/form.vue', () => {
wrapper = createWrapper({ loading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+
describe('when a new environment is being created', () => {
beforeEach(() => {
wrapper = createWrapper({
@@ -133,6 +131,18 @@ describe('~/environments/components/form.vue', () => {
});
});
+ describe('when no protected environment link is provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ provide: {},
+ });
+ });
+
+ it('does not show protected environment documentation', () => {
+ expect(wrapper.findByRole('link', { name: 'Protected environments' }).exists()).toBe(false);
+ });
+ });
+
describe('when an existing environment is being edited', () => {
beforeEach(() => {
wrapper = createWrapper({
diff --git a/spec/frontend/environments/environment_item_spec.js b/spec/frontend/environments/environment_item_spec.js
index dd909cf4473..e2b184adc8a 100644
--- a/spec/frontend/environments/environment_item_spec.js
+++ b/spec/frontend/environments/environment_item_spec.js
@@ -19,10 +19,6 @@ describe('Environment item', () => {
let tracking;
const factory = (options = {}) => {
- // This destroys any wrappers created before a nested call to factory reassigns it
- if (wrapper && wrapper.destroy) {
- wrapper.destroy();
- }
wrapper = mount(EnvironmentItem, {
...options,
});
@@ -55,10 +51,7 @@ describe('Environment item', () => {
const findUpcomingDeploymentAvatarLink = () =>
findUpcomingDeployment().findComponent(GlAvatarLink);
const findUpcomingDeploymentAvatar = () => findUpcomingDeployment().findComponent(GlAvatar);
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findMonitoringLink = () => wrapper.find('[data-testid="environment-monitoring"]');
describe('when item is not folder', () => {
it('should render environment name', () => {
@@ -390,10 +383,6 @@ describe('Environment item', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render folder icon and name', () => {
expect(wrapper.find('.folder-name').text()).toContain(folder.name);
expect(wrapper.find('.folder-icon')).toBeDefined();
@@ -446,4 +435,25 @@ describe('Environment item', () => {
});
});
});
+
+ describe.each([true, false])(
+ 'when `remove_monitor_metrics` flag is %p',
+ (removeMonitorMetrics) => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ model: {
+ metrics_path: 'http://0.0.0.0:3000/flightjs/Flight/-/metrics?environment=6',
+ },
+ tableData,
+ },
+ provide: { glFeatures: { removeMonitorMetrics } },
+ });
+ });
+
+ it(`${removeMonitorMetrics ? 'does not render' : 'renders'} link to metrics`, () => {
+ expect(findMonitoringLink().exists()).toBe(!removeMonitorMetrics);
+ });
+ },
+ );
});
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index 170036b5b00..ee195b41bc8 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -11,10 +11,6 @@ describe('Pin Component', () => {
let wrapper;
const factory = (options = {}) => {
- // This destroys any wrappers created before a nested call to factory reassigns it
- if (wrapper && wrapper.destroy) {
- wrapper.destroy();
- }
wrapper = shallowMount(PinComponent, {
...options,
});
@@ -31,10 +27,6 @@ describe('Pin Component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
});
@@ -64,10 +56,6 @@ describe('Pin Component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
});
diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js
index 851e24c22cc..3e27b8822e1 100644
--- a/spec/frontend/environments/environment_stop_spec.js
+++ b/spec/frontend/environments/environment_stop_spec.js
@@ -73,7 +73,7 @@ describe('Stop Component', () => {
});
});
- it('should show a loading icon if the environment is currently stopping', async () => {
+ it('should show a loading icon if the environment is currently stopping', () => {
expect(findButton().props('loading')).toBe(true);
});
});
diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js
index a86cfdd56ba..f41d1324b81 100644
--- a/spec/frontend/environments/environment_table_spec.js
+++ b/spec/frontend/environments/environment_table_spec.js
@@ -16,10 +16,6 @@ describe('Environment table', () => {
let wrapper;
const factory = (options = {}) => {
- // This destroys any wrappers created before a nested call to factory reassigns it
- if (wrapper && wrapper.destroy) {
- wrapper.destroy();
- }
wrapper = mount(EnvironmentTable, {
...options,
});
@@ -34,10 +30,6 @@ describe('Environment table', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('Should render a table', async () => {
const mockItem = {
name: 'review',
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index 986ecca4e84..dc450eb2aa7 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -96,10 +96,6 @@ describe('~/environments/components/environments_app.vue', () => {
paginationMock = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should request available environments if the scope is invalid', async () => {
await createWrapperWithMocked({
environmentsApp: resolvedEnvironmentsApp,
@@ -174,12 +170,8 @@ describe('~/environments/components/environments_app.vue', () => {
folder: resolvedFolder,
});
- const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
- button.trigger('click');
-
- await nextTick();
-
- expect(wrapper.findByText(s__('ReviewApp|Enable Review App')).exists()).toBe(true);
+ const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
+ expect(button.exists()).toBe(true);
});
it('should not show a button to open the review app modal if review apps are configured', async () => {
@@ -191,7 +183,7 @@ describe('~/environments/components/environments_app.vue', () => {
folder: resolvedFolder,
});
- const button = wrapper.findByRole('button', { name: s__('Environments|Enable review app') });
+ const button = wrapper.findByRole('button', { name: s__('Environments|Enable review apps') });
expect(button.exists()).toBe(false);
});
@@ -426,7 +418,7 @@ describe('~/environments/components/environments_app.vue', () => {
);
});
- it('should sync search term from query params on load', async () => {
+ it('should sync search term from query params on load', () => {
expect(searchBox.element.value).toBe('prod');
});
});
diff --git a/spec/frontend/environments/environments_detail_header_spec.js b/spec/frontend/environments/environments_detail_header_spec.js
index 1f233c05fbf..9464aeff028 100644
--- a/spec/frontend/environments/environments_detail_header_spec.js
+++ b/spec/frontend/environments/environments_detail_header_spec.js
@@ -1,12 +1,11 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { __, s__ } from '~/locale';
import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
import EnvironmentsDetailHeader from '~/environments/components/environments_detail_header.vue';
import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import DeployFreezeAlert from '~/environments/components/deploy_freeze_alert.vue';
import { createEnvironment } from './mock_data';
describe('Environments detail header component', () => {
@@ -22,13 +21,14 @@ describe('Environments detail header component', () => {
const findCancelAutoStopAtButton = () => wrapper.findByTestId('cancel-auto-stop-button');
const findCancelAutoStopAtForm = () => wrapper.findByTestId('cancel-auto-stop-form');
const findTerminalButton = () => wrapper.findByTestId('terminal-button');
- const findExternalUrlButton = () => wrapper.findByTestId('external-url-button');
+ const findExternalUrlButton = () => wrapper.findComponentByTestId('external-url-button');
const findMetricsButton = () => wrapper.findByTestId('metrics-button');
const findEditButton = () => wrapper.findByTestId('edit-button');
const findStopButton = () => wrapper.findByTestId('stop-button');
const findDestroyButton = () => wrapper.findByTestId('destroy-button');
const findStopEnvironmentModal = () => wrapper.findComponent(StopEnvironmentModal);
const findDeleteEnvironmentModal = () => wrapper.findComponent(DeleteEnvironmentModal);
+ const findDeployFreezeAlert = () => wrapper.findComponent(DeployFreezeAlert);
const buttons = [
['Cancel Auto Stop At', findCancelAutoStopAtButton],
@@ -40,14 +40,17 @@ describe('Environments detail header component', () => {
['Destroy', findDestroyButton],
];
- const createWrapper = ({ props }) => {
+ const createWrapper = ({ props, glFeatures = {} }) => {
wrapper = shallowMountExtended(EnvironmentsDetailHeader, {
stubs: {
GlSprintf,
TimeAgo,
},
+ provide: {
+ glFeatures,
+ },
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
canAdminEnvironment: false,
@@ -59,10 +62,6 @@ describe('Environments detail header component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default state with minimal access', () => {
beforeEach(() => {
createWrapper({ props: { environment: createEnvironment({ externalUrl: null }) } });
@@ -175,6 +174,7 @@ describe('Environments detail header component', () => {
it('displays the external url button with correct path', () => {
expect(findExternalUrlButton().attributes('href')).toBe(externalUrl);
+ expect(findExternalUrlButton().props('isUnsafeLink')).toBe(true);
});
});
@@ -199,6 +199,25 @@ describe('Environments detail header component', () => {
expect(tooltip).toBeDefined();
expect(button.attributes('title')).toBe('See metrics');
});
+
+ describe.each([true, false])(
+ 'and `remove_monitor_metrics` flag is %p',
+ (removeMonitorMetrics) => {
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ environment: createEnvironment({ metricsUrl: 'my metrics url' }),
+ metricsPath,
+ },
+ glFeatures: { removeMonitorMetrics },
+ });
+ });
+
+ it(`${removeMonitorMetrics ? 'does not render' : 'renders'} Metrics button`, () => {
+ expect(findMetricsButton().exists()).toBe(!removeMonitorMetrics);
+ });
+ },
+ );
});
describe('when has all admin rights', () => {
@@ -246,22 +265,12 @@ describe('Environments detail header component', () => {
});
});
- describe('when the environment has an unsafe external url', () => {
- const externalUrl = 'postgres://staging';
-
- beforeEach(() => {
- createWrapper({
- props: {
- environment: createEnvironment({ externalUrl }),
- },
- });
- });
+ describe('deploy freeze alert', () => {
+ it('passes the environment name to the alert', () => {
+ const environment = createEnvironment();
+ createWrapper({ props: { environment } });
- it('should show a copy button instead', () => {
- const button = wrapper.findComponent(ModalCopyButton);
- expect(button.props('title')).toBe(s__('Environments|Copy live environment URL'));
- expect(button.props('text')).toBe(externalUrl);
- expect(button.text()).toBe(__('Copy URL'));
+ expect(findDeployFreezeAlert().props('name')).toBe(environment.name);
});
});
});
diff --git a/spec/frontend/environments/environments_folder_view_spec.js b/spec/frontend/environments/environments_folder_view_spec.js
index a87060f83d8..75fb3a31120 100644
--- a/spec/frontend/environments/environments_folder_view_spec.js
+++ b/spec/frontend/environments/environments_folder_view_spec.js
@@ -24,7 +24,6 @@ describe('Environments Folder View', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('successful request', () => {
diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js
index 23506eb018d..6a40c68397b 100644
--- a/spec/frontend/environments/folder/environments_folder_view_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_view_spec.js
@@ -123,22 +123,4 @@ describe('Environments Folder View', () => {
expect(tabTable.find('.badge').text()).toContain('0');
});
});
-
- describe('methods', () => {
- beforeEach(() => {
- mockEnvironments([]);
- createWrapper();
- jest.spyOn(window.history, 'pushState').mockImplementation(() => {});
- return axios.waitForAll();
- });
-
- describe('updateContent', () => {
- it('should set given parameters', () =>
- wrapper.vm.updateContent({ scope: 'stopped', page: '4' }).then(() => {
- expect(wrapper.vm.page).toEqual('4');
- expect(wrapper.vm.scope).toEqual('stopped');
- expect(wrapper.vm.requestData.page).toEqual('4');
- }));
- });
- });
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 5ea0be41614..addbf2c21dc 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -798,3 +798,112 @@ export const resolvedDeploymentDetails = {
},
},
};
+
+export const agent = {
+ project: 'agent-project',
+ id: 'gid://gitlab/ClusterAgent/1',
+ name: 'agent-name',
+ kubernetesNamespace: 'agent-namespace',
+};
+
+const runningPod = { status: { phase: 'Running' } };
+const pendingPod = { status: { phase: 'Pending' } };
+const succeededPod = { status: { phase: 'Succeeded' } };
+const failedPod = { status: { phase: 'Failed' } };
+
+export const k8sPodsMock = [runningPod, runningPod, pendingPod, succeededPod, failedPod, failedPod];
+
+export const k8sServicesMock = [
+ {
+ metadata: {
+ name: 'my-first-service',
+ namespace: 'default',
+ creationTimestamp: new Date(),
+ },
+ spec: {
+ ports: [
+ {
+ name: 'https',
+ protocol: 'TCP',
+ port: 443,
+ targetPort: 8443,
+ },
+ ],
+ clusterIP: '10.96.0.1',
+ externalIP: '-',
+ type: 'ClusterIP',
+ },
+ },
+ {
+ metadata: {
+ name: 'my-second-service',
+ namespace: 'default',
+ creationTimestamp: '2020-07-03T14:06:04Z',
+ },
+ spec: {
+ ports: [
+ {
+ name: 'http',
+ protocol: 'TCP',
+ appProtocol: 'http',
+ port: 80,
+ targetPort: 'http',
+ nodePort: 31989,
+ },
+ {
+ name: 'https',
+ protocol: 'TCP',
+ appProtocol: 'https',
+ port: 443,
+ targetPort: 'https',
+ nodePort: 32679,
+ },
+ ],
+ clusterIP: '10.105.219.238',
+ externalIP: '-',
+ type: 'NodePort',
+ },
+ },
+];
+
+const readyDeployment = {
+ status: {
+ conditions: [
+ { type: 'Available', status: 'True' },
+ { type: 'Progressing', status: 'True' },
+ ],
+ },
+};
+const failedDeployment = {
+ status: {
+ conditions: [
+ { type: 'Available', status: 'False' },
+ { type: 'Progressing', status: 'False' },
+ ],
+ },
+};
+const readyDaemonSet = {
+ status: { numberReady: 1, desiredNumberScheduled: 1, numberMisscheduled: 0 },
+};
+const failedDaemonSet = {
+ status: { numberMisscheduled: 1, numberReady: 0, desiredNumberScheduled: 1 },
+};
+const readySet = { spec: { replicas: 2 }, status: { readyReplicas: 2 } };
+const failedSet = { spec: { replicas: 2 }, status: { readyReplicas: 1 } };
+const completedJob = { spec: { completions: 1 }, status: { succeeded: 1, failed: 0 } };
+const failedJob = { spec: { completions: 1 }, status: { succeeded: 0, failed: 1 } };
+const completedCronJob = {
+ spec: { suspend: 0 },
+ status: { active: 0, lastScheduleTime: new Date().toString() },
+};
+const suspendedCronJob = { spec: { suspend: 1 }, status: { active: 0, lastScheduleTime: '' } };
+const failedCronJob = { spec: { suspend: 0 }, status: { active: 2, lastScheduleTime: '' } };
+
+export const k8sWorkloadsMock = {
+ DeploymentList: [readyDeployment, failedDeployment],
+ DaemonSetList: [readyDaemonSet, failedDaemonSet, failedDaemonSet],
+ StatefulSetList: [readySet, readySet, failedSet],
+ ReplicaSetList: [readySet, failedSet],
+ JobList: [completedJob, completedJob, failedJob],
+ CronJobList: [completedCronJob, suspendedCronJob, failedCronJob],
+};
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 2c223d3a1a7..edffc00e185 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -17,6 +18,8 @@ import {
resolvedEnvironment,
folder,
resolvedFolder,
+ k8sPodsMock,
+ k8sServicesMock,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
@@ -27,6 +30,14 @@ describe('~/frontend/environments/graphql/resolvers', () => {
let mockApollo;
let localState;
+ const configuration = {
+ basePath: 'kas-proxy/',
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+ const namespace = 'default';
+
beforeEach(() => {
mockResolvers = resolvers(ENDPOINT);
mock = new MockAdapter(axios);
@@ -143,13 +154,178 @@ describe('~/frontend/environments/graphql/resolvers', () => {
expect(environmentFolder).toEqual(resolvedFolder);
});
});
- describe('stopEnvironment', () => {
+ describe('k8sPods', () => {
+ const mockPodsListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sPodsMock,
+ },
+ });
+ });
+
+ const mockNamespacedPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
+ const mockAllPodsListFn = jest.fn().mockImplementation(mockPodsListFn);
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedPod')
+ .mockImplementation(mockNamespacedPodsListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockImplementation(mockAllPodsListFn);
+ });
+
+ it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace });
+
+ expect(mockNamespacedPodsListFn).toHaveBeenCalledWith(namespace);
+ expect(mockAllPodsListFn).not.toHaveBeenCalled();
+
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should request all pods from the cluster_client library if namespace is not specified', async () => {
+ const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace: '' });
+
+ expect(mockAllPodsListFn).toHaveBeenCalled();
+ expect(mockNamespacedPodsListFn).not.toHaveBeenCalled();
+
+ expect(pods).toEqual(k8sPodsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1PodForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(mockResolvers.Query.k8sPods(null, { configuration })).rejects.toThrow(
+ 'API error',
+ );
+ });
+ });
+ describe('k8sServices', () => {
+ const mockServicesListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sServicesMock,
+ },
+ });
+ });
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockImplementation(mockServicesListFn);
+ });
+
+ it('should request services from the cluster_client library', async () => {
+ const services = await mockResolvers.Query.k8sServices(null, { configuration });
+
+ expect(mockServicesListFn).toHaveBeenCalled();
+
+ expect(services).toEqual(k8sServicesMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(mockResolvers.Query.k8sServices(null, { configuration })).rejects.toThrow(
+ 'API error',
+ );
+ });
+ });
+ describe('k8sWorkloads', () => {
+ const emptyImplementation = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: [],
+ },
+ });
+ });
+
+ const [
+ mockNamespacedDeployment,
+ mockNamespacedDaemonSet,
+ mockNamespacedStatefulSet,
+ mockNamespacedReplicaSet,
+ mockNamespacedJob,
+ mockNamespacedCronJob,
+ mockAllDeployment,
+ mockAllDaemonSet,
+ mockAllStatefulSet,
+ mockAllReplicaSet,
+ mockAllJob,
+ mockAllCronJob,
+ ] = Array(12).fill(emptyImplementation);
+
+ const namespacedMocks = [
+ { method: 'listAppsV1NamespacedDeployment', api: AppsV1Api, spy: mockNamespacedDeployment },
+ { method: 'listAppsV1NamespacedDaemonSet', api: AppsV1Api, spy: mockNamespacedDaemonSet },
+ { method: 'listAppsV1NamespacedStatefulSet', api: AppsV1Api, spy: mockNamespacedStatefulSet },
+ { method: 'listAppsV1NamespacedReplicaSet', api: AppsV1Api, spy: mockNamespacedReplicaSet },
+ { method: 'listBatchV1NamespacedJob', api: BatchV1Api, spy: mockNamespacedJob },
+ { method: 'listBatchV1NamespacedCronJob', api: BatchV1Api, spy: mockNamespacedCronJob },
+ ];
+
+ const allMocks = [
+ { method: 'listAppsV1DeploymentForAllNamespaces', api: AppsV1Api, spy: mockAllDeployment },
+ { method: 'listAppsV1DaemonSetForAllNamespaces', api: AppsV1Api, spy: mockAllDaemonSet },
+ { method: 'listAppsV1StatefulSetForAllNamespaces', api: AppsV1Api, spy: mockAllStatefulSet },
+ { method: 'listAppsV1ReplicaSetForAllNamespaces', api: AppsV1Api, spy: mockAllReplicaSet },
+ { method: 'listBatchV1JobForAllNamespaces', api: BatchV1Api, spy: mockAllJob },
+ { method: 'listBatchV1CronJobForAllNamespaces', api: BatchV1Api, spy: mockAllCronJob },
+ ];
+
+ beforeEach(() => {
+ [...namespacedMocks, ...allMocks].forEach((workloadMock) => {
+ jest
+ .spyOn(workloadMock.api.prototype, workloadMock.method)
+ .mockImplementation(workloadMock.spy);
+ });
+ });
+
+ it('should request namespaced workload types from the cluster_client library if namespace is specified', async () => {
+ await mockResolvers.Query.k8sWorkloads(null, { configuration, namespace });
+
+ namespacedMocks.forEach((workloadMock) => {
+ expect(workloadMock.spy).toHaveBeenCalledWith(namespace);
+ });
+ });
+
+ it('should request all workload types from the cluster_client library if namespace is not specified', async () => {
+ await mockResolvers.Query.k8sWorkloads(null, { configuration, namespace: '' });
+
+ allMocks.forEach((workloadMock) => {
+ expect(workloadMock.spy).toHaveBeenCalled();
+ });
+ });
+ it('should pass fulfilled calls data if one of the API calls fail', async () => {
+ jest
+ .spyOn(AppsV1Api.prototype, 'listAppsV1DeploymentForAllNamespaces')
+ .mockRejectedValue(new Error('API error'));
+
+ await expect(
+ mockResolvers.Query.k8sWorkloads(null, { configuration }),
+ ).resolves.toBeDefined();
+ });
+ it('should throw an error if all the API calls fail', async () => {
+ [...allMocks].forEach((workloadMock) => {
+ jest
+ .spyOn(workloadMock.api.prototype, workloadMock.method)
+ .mockRejectedValue(new Error('API error'));
+ });
+
+ await expect(mockResolvers.Query.k8sWorkloads(null, { configuration })).rejects.toThrow(
+ 'API error',
+ );
+ });
+ });
+ describe('stopEnvironmentREST', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
- await mockResolvers.Mutation.stopEnvironment(null, { environment }, { client });
+ await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
@@ -166,7 +342,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
- await mockResolvers.Mutation.stopEnvironment(null, { environment }, { client });
+ await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
diff --git a/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
index 326a28bd769..ec0fe0c5541 100644
--- a/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
+++ b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
@@ -26,11 +26,37 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "2022-10-17T07:44:43Z",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": Object {
"label": "deploy-prod (#860)",
"webPath": "/gitlab-org/pipelinestest/-/jobs/860",
},
+ "rollback": Object {
+ "id": "gid://gitlab/Deployment/76",
+ "lastDeployment": Object {
+ "commit": Object {
+ "author": Object {
+ "avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
+ "id": "gid://gitlab/User/1",
+ "name": "Administrator",
+ "webUrl": "http://gdk.test:3000/root",
+ },
+ "authorEmail": "admin@example.com",
+ "authorGravatar": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "authorName": "Administrator",
+ "id": "gid://gitlab/CommitPresenter/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ "message": "Update .gitlab-ci.yml file",
+ "shortId": "0cb48dd5",
+ "webUrl": "http://gdk.test:3000/gitlab-org/pipelinestest/-/commit/0cb48dd5deddb7632fd7c3defb16075fc6c3ca74",
+ },
+ "isLast": false,
+ },
+ "name": undefined,
+ "retryUrl": "/gitlab-org/pipelinestest/-/jobs/860/retry",
+ },
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -60,8 +86,12 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "2022-10-17T07:44:43Z",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": undefined,
+ "rollback": null,
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -91,8 +121,12 @@ Object {
},
"created": "2022-10-17T07:44:17Z",
"deployed": "",
+ "deploymentApproval": Object {
+ "isApprovalActionAvailable": false,
+ },
"id": "31",
"job": null,
+ "rollback": null,
"status": "success",
"triggerer": Object {
"avatarUrl": "/uploads/-/system/user/avatar/1/avatar.png",
diff --git a/spec/frontend/environments/kubernetes_agent_info_spec.js b/spec/frontend/environments/kubernetes_agent_info_spec.js
new file mode 100644
index 00000000000..b1795065281
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_agent_info_spec.js
@@ -0,0 +1,124 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
+import { AGENT_STATUSES, ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getK8sClusterAgentQuery from '~/environments/graphql/queries/k8s_cluster_agent.query.graphql';
+
+Vue.use(VueApollo);
+
+const propsData = {
+ agentName: 'my-agent',
+ agentId: '1',
+ agentProjectPath: 'path/to/agent-config-project',
+};
+
+const mockClusterAgent = {
+ id: '1',
+ name: 'token-1',
+ webPath: 'path/to/agent-page',
+};
+
+const connectedTimeNow = new Date();
+const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
+
+describe('~/environments/components/kubernetes_agent_info.vue', () => {
+ let wrapper;
+ let agentQueryResponse;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAgentLink = () => wrapper.findComponent(GlLink);
+ const findAgentStatus = () => wrapper.findByTestId('agent-status');
+ const findAgentStatusIcon = () => findAgentStatus().findComponent(GlIcon);
+ const findAgentLastUsedDate = () => wrapper.findByTestId('agent-last-used-date');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createWrapper = ({ tokens = [], queryResponse = null } = {}) => {
+ const clusterAgent = { ...mockClusterAgent, tokens: { nodes: tokens } };
+
+ agentQueryResponse =
+ queryResponse ||
+ jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } });
+ const apolloProvider = createMockApollo([[getK8sClusterAgentQuery, agentQueryResponse]]);
+
+ wrapper = extendedWrapper(
+ shallowMount(KubernetesAgentInfo, {
+ apolloProvider,
+ propsData,
+ stubs: { TimeAgoTooltip, GlSprintf },
+ }),
+ );
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows loading icon while fetching the agent details', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ await waitForPromises();
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('sends expected params', async () => {
+ await waitForPromises();
+
+ const variables = {
+ agentName: propsData.agentName,
+ projectPath: propsData.agentProjectPath,
+ };
+
+ expect(agentQueryResponse).toHaveBeenCalledWith(variables);
+ });
+
+ it('renders the agent name with the link', async () => {
+ await waitForPromises();
+
+ expect(findAgentLink().attributes('href')).toBe(mockClusterAgent.webPath);
+ expect(findAgentLink().text()).toContain(mockClusterAgent.id);
+ });
+ });
+
+ describe.each`
+ lastUsedAt | status | lastUsedText
+ ${null} | ${'unused'} | ${KubernetesAgentInfo.i18n.neverConnectedText}
+ ${connectedTimeNow} | ${'active'} | ${'just now'}
+ ${connectedTimeInactive} | ${'inactive'} | ${'8 minutes ago'}
+ `('when agent connection status is "$status"', ({ lastUsedAt, status, lastUsedText }) => {
+ beforeEach(async () => {
+ const tokens = [{ id: 'token-id', lastUsedAt }];
+ createWrapper({ tokens });
+ await waitForPromises();
+ });
+
+ it('displays correct status text', () => {
+ expect(findAgentStatus().text()).toBe(AGENT_STATUSES[status].name);
+ });
+
+ it('displays correct status icon', () => {
+ expect(findAgentStatusIcon().props('name')).toBe(AGENT_STATUSES[status].icon);
+ expect(findAgentStatusIcon().attributes('class')).toBe(AGENT_STATUSES[status].class);
+ });
+
+ it('displays correct last used date status', () => {
+ expect(findAgentLastUsedDate().text()).toBe(lastUsedText);
+ });
+ });
+
+ describe('when the agent query has errored', () => {
+ beforeEach(() => {
+ createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() });
+ return waitForPromises();
+ });
+
+ it('displays an alert message', () => {
+ expect(findAlert().text()).toBe(KubernetesAgentInfo.i18n.loadingError);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
new file mode 100644
index 00000000000..394fd200edf
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -0,0 +1,131 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlCollapse, GlButton, GlAlert } from '@gitlab/ui';
+import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
+import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
+import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
+import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue';
+import { agent } from './graphql/mock_data';
+import { mockKasTunnelUrl } from './mock_data';
+
+const propsData = {
+ agentId: agent.id,
+ agentName: agent.name,
+ agentProjectPath: agent.project,
+ namespace: agent.kubernetesNamespace,
+};
+
+const provide = {
+ kasTunnelUrl: mockKasTunnelUrl,
+};
+
+const configuration = {
+ basePath: provide.kasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+};
+
+describe('~/environments/components/kubernetes_overview.vue', () => {
+ let wrapper;
+
+ const findCollapse = () => wrapper.findComponent(GlCollapse);
+ const findCollapseButton = () => wrapper.findComponent(GlButton);
+ const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo);
+ const findKubernetesPods = () => wrapper.findComponent(KubernetesPods);
+ const findKubernetesTabs = () => wrapper.findComponent(KubernetesTabs);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const createWrapper = () => {
+ wrapper = shallowMount(KubernetesOverview, {
+ propsData,
+ provide,
+ });
+ };
+
+ const toggleCollapse = async () => {
+ findCollapseButton().vm.$emit('click');
+ await nextTick();
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders the kubernetes overview title', () => {
+ expect(wrapper.text()).toBe(KubernetesOverview.i18n.sectionTitle);
+ });
+ });
+
+ describe('collapse', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('is collapsed by default', () => {
+ expect(findCollapse().props('visible')).toBeUndefined();
+ expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.expand);
+ expect(findCollapseButton().props('icon')).toBe('chevron-right');
+ });
+
+ it("doesn't render components when the collapse is not visible", () => {
+ expect(findAgentInfo().exists()).toBe(false);
+ expect(findKubernetesPods().exists()).toBe(false);
+ });
+
+ it('opens on click', async () => {
+ findCollapseButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findCollapse().attributes('visible')).toBe('true');
+ expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.collapse);
+ expect(findCollapseButton().props('icon')).toBe('chevron-down');
+ });
+ });
+
+ describe('when section is expanded', () => {
+ beforeEach(() => {
+ createWrapper();
+ toggleCollapse();
+ });
+
+ it('renders kubernetes agent info', () => {
+ expect(findAgentInfo().props()).toEqual({
+ agentName: agent.name,
+ agentId: agent.id,
+ agentProjectPath: agent.project,
+ });
+ });
+
+ it('renders kubernetes pods', () => {
+ expect(findKubernetesPods().props()).toEqual({
+ namespace: agent.kubernetesNamespace,
+ configuration,
+ });
+ });
+
+ it('renders kubernetes tabs', () => {
+ expect(findKubernetesTabs().props()).toEqual({
+ namespace: agent.kubernetesNamespace,
+ configuration,
+ });
+ });
+ });
+
+ describe('on cluster error', () => {
+ beforeEach(() => {
+ createWrapper();
+ toggleCollapse();
+ });
+
+ it('shows alert with the error message', async () => {
+ const error = 'Error message from pods';
+
+ findKubernetesPods().vm.$emit('cluster-error', error);
+ await nextTick();
+
+ expect(findAlert().text()).toBe(error);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_pods_spec.js b/spec/frontend/environments/kubernetes_pods_spec.js
new file mode 100644
index 00000000000..137309d7853
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_pods_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
+import { mockKasTunnelUrl } from './mock_data';
+import { k8sPodsMock } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/kubernetes_pods.vue', () => {
+ let wrapper;
+
+ const namespace = 'my-kubernetes-namespace';
+ const configuration = {
+ basePath: mockKasTunnelUrl,
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAllStats = () => wrapper.findAllComponents(GlSingleStat);
+ const findSingleStat = (at) => findAllStats().at(at);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sPods: jest.fn().mockReturnValue(k8sPodsMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMount(KubernetesPods, {
+ propsData: { namespace, configuration },
+ apolloProvider,
+ });
+ };
+
+ describe('mounted', () => {
+ it('shows the loading icon', () => {
+ createWrapper();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('hides the loading icon when the list of pods loaded', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when gets pods data', () => {
+ it('renders stats', async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findAllStats()).toHaveLength(4);
+ });
+
+ it.each`
+ count | title | index
+ ${2} | ${KubernetesPods.i18n.runningPods} | ${0}
+ ${1} | ${KubernetesPods.i18n.pendingPods} | ${1}
+ ${1} | ${KubernetesPods.i18n.succeededPods} | ${2}
+ ${2} | ${KubernetesPods.i18n.failedPods} | ${3}
+ `(
+ 'renders stat with title "$title" and count "$count" at index $index',
+ async ({ count, title, index }) => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findSingleStat(index).props()).toMatchObject({
+ value: count,
+ title,
+ });
+ },
+ );
+ });
+
+ describe('when gets an error from the cluster_client API', () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sPods: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ beforeEach(async () => {
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+ });
+
+ it("doesn't show pods stats", () => {
+ expect(findAllStats()).toHaveLength(0);
+ });
+
+ it('emits an error message', () => {
+ expect(wrapper.emitted('cluster-error')).toMatchObject([[error]]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_summary_spec.js b/spec/frontend/environments/kubernetes_summary_spec.js
new file mode 100644
index 00000000000..53b83079486
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_summary_spec.js
@@ -0,0 +1,115 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTab, GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import KubernetesSummary from '~/environments/components/kubernetes_summary.vue';
+import { mockKasTunnelUrl } from './mock_data';
+import { k8sWorkloadsMock } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/kubernetes_summary.vue', () => {
+ let wrapper;
+
+ const namespace = 'my-kubernetes-namespace';
+ const configuration = {
+ basePath: mockKasTunnelUrl,
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTab = () => wrapper.findComponent(GlTab);
+ const findSummaryListItem = (at) => wrapper.findAllByTestId('summary-list-item').at(at);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sWorkloads: jest.fn().mockReturnValue(k8sWorkloadsMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMountExtended(KubernetesSummary, {
+ propsData: { configuration, namespace },
+ apolloProvider,
+ stubs: {
+ GlTab,
+ GlBadge,
+ },
+ });
+ };
+
+ describe('mounted', () => {
+ it('renders summary tab', () => {
+ createWrapper();
+
+ expect(findTab().text()).toMatchInterpolatedText(`${KubernetesSummary.i18n.summaryTitle} 0`);
+ });
+
+ it('shows the loading icon', () => {
+ createWrapper();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ describe('when workloads data is loaded', () => {
+ beforeEach(async () => {
+ await createWrapper();
+ await waitForPromises();
+ });
+
+ it('hides the loading icon when the list of workload types loaded', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it.each`
+ type | successText | successCount | failedCount | suspendedCount | index
+ ${'Deployments'} | ${'ready'} | ${1} | ${1} | ${0} | ${0}
+ ${'DaemonSets'} | ${'ready'} | ${1} | ${2} | ${0} | ${1}
+ ${'StatefulSets'} | ${'ready'} | ${2} | ${1} | ${0} | ${2}
+ ${'ReplicaSets'} | ${'ready'} | ${1} | ${1} | ${0} | ${3}
+ ${'Jobs'} | ${'completed'} | ${2} | ${1} | ${0} | ${4}
+ ${'CronJobs'} | ${'ready'} | ${1} | ${1} | ${1} | ${5}
+ `(
+ 'populates view with the correct badges for workload type $type',
+ ({ type, successText, successCount, failedCount, suspendedCount, index }) => {
+ const findAllBadges = () => findSummaryListItem(index).findAllComponents(GlBadge);
+ const findBadgeByVariant = (variant) =>
+ findAllBadges().wrappers.find((badge) => badge.props('variant') === variant);
+
+ expect(findSummaryListItem(index).text()).toContain(type);
+ expect(findBadgeByVariant('success').text()).toBe(`${successCount} ${successText}`);
+ expect(findBadgeByVariant('danger').text()).toBe(`${failedCount} failed`);
+ if (suspendedCount > 0) {
+ expect(findBadgeByVariant('neutral').text()).toBe(`${suspendedCount} suspended`);
+ }
+ },
+ );
+ });
+
+ it('emits an error message when gets an error from the cluster_client API', async () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sWorkloads: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+
+ expect(wrapper.emitted('cluster-error')).toEqual([[error]]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/kubernetes_tabs_spec.js b/spec/frontend/environments/kubernetes_tabs_spec.js
new file mode 100644
index 00000000000..429f267347b
--- /dev/null
+++ b/spec/frontend/environments/kubernetes_tabs_spec.js
@@ -0,0 +1,168 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTabs, GlTab, GlTable, GlPagination, GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import { useFakeDate } from 'helpers/fake_date';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue';
+import KubernetesSummary from '~/environments/components/kubernetes_summary.vue';
+import { SERVICES_LIMIT_PER_PAGE } from '~/environments/constants';
+import { mockKasTunnelUrl } from './mock_data';
+import { k8sServicesMock } from './graphql/mock_data';
+
+Vue.use(VueApollo);
+
+describe('~/environments/components/kubernetes_tabs.vue', () => {
+ let wrapper;
+
+ const namespace = 'my-kubernetes-namespace';
+ const configuration = {
+ basePath: mockKasTunnelUrl,
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ },
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findTab = () => wrapper.findComponent(GlTab);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const findKubernetesSummary = () => wrapper.findComponent(KubernetesSummary);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest.fn().mockReturnValue(k8sServicesMock),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ const createWrapper = (apolloProvider = createApolloProvider()) => {
+ wrapper = shallowMountExtended(KubernetesTabs, {
+ propsData: { configuration, namespace },
+ apolloProvider,
+ stubs: {
+ GlTab,
+ GlTable: stubComponent(GlTable, {
+ props: ['items', 'per-page'],
+ }),
+ GlBadge,
+ },
+ });
+ };
+
+ describe('mounted', () => {
+ it('shows tabs', () => {
+ createWrapper();
+
+ expect(findTabs().exists()).toBe(true);
+ });
+
+ it('renders summary tab', () => {
+ createWrapper();
+
+ expect(findKubernetesSummary().props()).toEqual({ namespace, configuration });
+ });
+
+ it('renders services tab', () => {
+ createWrapper();
+
+ expect(findTab().text()).toMatchInterpolatedText(`${KubernetesTabs.i18n.servicesTitle} 0`);
+ });
+ });
+
+ describe('services tab', () => {
+ useFakeDate(2020, 6, 6);
+ it('shows the loading icon', () => {
+ createWrapper();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ describe('when services data is loaded', () => {
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('hides the loading icon when the list of services loaded', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('renders services table when gets services data', () => {
+ expect(findTable().props('perPage')).toBe(SERVICES_LIMIT_PER_PAGE);
+ expect(findTable().props('items')).toMatchObject([
+ {
+ name: 'my-first-service',
+ namespace: 'default',
+ type: 'ClusterIP',
+ clusterIP: '10.96.0.1',
+ externalIP: '-',
+ ports: '443/TCP',
+ age: '0s',
+ },
+ {
+ name: 'my-second-service',
+ namespace: 'default',
+ type: 'NodePort',
+ clusterIP: '10.105.219.238',
+ externalIP: '-',
+ ports: '80:31989/TCP, 443:32679/TCP',
+ age: '2d',
+ },
+ ]);
+ });
+
+ it("doesn't render pagination when services are less then SERVICES_LIMIT_PER_PAGE", async () => {
+ createWrapper();
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ it('shows pagination when services are more then SERVICES_LIMIT_PER_PAGE', async () => {
+ const createApolloProviderWithPagination = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest
+ .fn()
+ .mockReturnValue(
+ Array.from({ length: 6 }, () => k8sServicesMock).flatMap((array) => array),
+ ),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ createWrapper(createApolloProviderWithPagination());
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('emits an error message when gets an error from the cluster_client API', async () => {
+ const error = new Error('Error from the cluster_client API');
+ const createErroredApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ k8sServices: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ createWrapper(createErroredApolloProvider());
+ await waitForPromises();
+
+ expect(wrapper.emitted('cluster-error')).toEqual([[error]]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/mock_data.js b/spec/frontend/environments/mock_data.js
index a6d67c26304..bd2c6b7c892 100644
--- a/spec/frontend/environments/mock_data.js
+++ b/spec/frontend/environments/mock_data.js
@@ -313,6 +313,8 @@ const createEnvironment = (data = {}) => ({
...data,
});
+const mockKasTunnelUrl = 'https://kas.gitlab.com/k8s-proxy';
+
export {
environment,
environmentsList,
@@ -321,4 +323,5 @@ export {
tableData,
deployBoardMockData,
createEnvironment,
+ mockKasTunnelUrl,
};
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 76cd09cfb4e..5583e737dd8 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -7,9 +7,12 @@ import { stubTransition } from 'helpers/stub_transition';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
+import EnvironmentActions from '~/environments/components/environment_actions.vue';
import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
-import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data';
+import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
+import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
+import { mockKasTunnelUrl } from './mock_data';
Vue.use(VueApollo);
@@ -20,15 +23,24 @@ describe('~/environments/components/new_environment_item.vue', () => {
return createMockApollo();
};
- const createWrapper = ({ propsData = {}, apolloProvider } = {}) =>
+ const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) =>
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
- provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1' },
+ provide: {
+ helpPagePath: '/help',
+ projectId: '1',
+ projectPath: '/1',
+ kasTunnelUrl: mockKasTunnelUrl,
+ ...provideData,
+ },
stubs: { transition: stubTransition() },
});
const findDeployment = () => wrapper.findComponent(Deployment);
+ const findActions = () => wrapper.findComponent(EnvironmentActions);
+ const findKubernetesOverview = () => wrapper.findComponent(KubernetesOverview);
+ const findMonitoringLink = () => wrapper.find('[data-testid="environment-monitoring"]');
const expandCollapsedSection = async () => {
const button = wrapper.findByRole('button', { name: __('Expand') });
@@ -37,10 +49,6 @@ describe('~/environments/components/new_environment_item.vue', () => {
return button;
};
- afterEach(() => {
- wrapper?.destroy();
- });
-
it('displays the name when not in a folder', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
@@ -126,9 +134,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('shows a dropdown if there are actions to perform', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
- const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
-
- expect(actions.exists()).toBe(true);
+ expect(findActions().exists()).toBe(true);
});
it('does not show a dropdown if there are no actions to perform', () => {
@@ -142,22 +148,20 @@ describe('~/environments/components/new_environment_item.vue', () => {
},
});
- const actions = wrapper.findByRole('button', { name: __('Deploy to...') });
-
- expect(actions.exists()).toBe(false);
+ expect(findActions().exists()).toBe(false);
});
it('passes all the actions down to the action component', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
- const action = wrapper.findByRole('menuitem', { name: 'deploy-staging' });
-
- expect(action.exists()).toBe(true);
+ expect(findActions().props('actions')).toMatchObject(
+ resolvedEnvironment.lastDeployment.manualActions,
+ );
});
});
describe('stop', () => {
- it('shows a buton to stop the environment if the environment is available', () => {
+ it('shows a button to stop the environment if the environment is available', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
@@ -165,7 +169,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(stop.exists()).toBe(true);
});
- it('does not show a buton to stop the environment if the environment is stopped', () => {
+ it('does not show a button to stop the environment if the environment is stopped', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, canStop: false } },
apolloProvider: createApolloProvider(),
@@ -311,7 +315,25 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(rollback.exists()).toBe(false);
});
+
+ describe.each([true, false])(
+ 'when `remove_monitor_metrics` flag is %p',
+ (removeMonitorMetrics) => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ propsData: { environment: { ...resolvedEnvironment, metricsPath: '/metrics' } },
+ apolloProvider: createApolloProvider(),
+ provideData: { glFeatures: { removeMonitorMetrics } },
+ });
+ });
+
+ it(`${removeMonitorMetrics ? 'does not render' : 'renders'} link to metrics`, () => {
+ expect(findMonitoringLink().exists()).toBe(!removeMonitorMetrics);
+ });
+ },
+ );
});
+
describe('terminal', () => {
it('shows the link to the terminal if set up', () => {
wrapper = createWrapper({
@@ -384,6 +406,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
const button = await expandCollapsedSection();
expect(button.attributes('aria-label')).toBe(__('Collapse'));
+ expect(button.props('category')).toBe('secondary');
expect(collapse.attributes('visible')).toBe('visible');
expect(icon.props('name')).toBe('chevron-lg-down');
expect(environmentName.classes('gl-font-weight-bold')).toBe(true);
@@ -515,4 +538,72 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(deployBoard.exists()).toBe(false);
});
});
+
+ describe('kubernetes overview', () => {
+ const environmentWithAgent = {
+ ...resolvedEnvironment,
+ agent,
+ };
+
+ it('should render if the feature flag is enabled and the environment has an agent object with the required data specified', () => {
+ wrapper = createWrapper({
+ propsData: { environment: environmentWithAgent },
+ provideData: {
+ glFeatures: {
+ kasUserAccessProject: true,
+ },
+ },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().props()).toMatchObject({
+ agentProjectPath: agent.project,
+ agentName: agent.name,
+ agentId: agent.id,
+ namespace: agent.kubernetesNamespace,
+ });
+ });
+
+ it('should not render if the feature flag is not enabled', () => {
+ wrapper = createWrapper({
+ propsData: { environment: environmentWithAgent },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+
+ it('should not render if the environment has no agent object', () => {
+ wrapper = createWrapper({
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+
+ it('should not render if the environment has an agent object without agent id specified', () => {
+ const environment = {
+ ...resolvedEnvironment,
+ agent: {
+ project: agent.project,
+ name: agent.name,
+ },
+ };
+
+ wrapper = createWrapper({
+ propsData: { environment },
+ apolloProvider: createApolloProvider(),
+ });
+
+ expandCollapsedSection();
+
+ expect(findKubernetesOverview().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index a8cc05b297b..743f4ad6786 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -3,13 +3,13 @@ import MockAdapter from 'axios-mock-adapter';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
const DEFAULT_OPTS = {
provide: {
@@ -41,7 +41,6 @@ describe('~/environments/components/new.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
diff --git a/spec/frontend/environments/stop_stale_environments_modal_spec.js b/spec/frontend/environments/stop_stale_environments_modal_spec.js
index a2ab4f707b5..3d28ceba318 100644
--- a/spec/frontend/environments/stop_stale_environments_modal_spec.js
+++ b/spec/frontend/environments/stop_stale_environments_modal_spec.js
@@ -18,7 +18,6 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
let wrapper;
let mock;
let before;
- let originalGon;
const createWrapper = (opts = {}) =>
shallowMount(StopStaleEnvironmentsModal, {
@@ -28,8 +27,7 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
});
beforeEach(() => {
- originalGon = window.gon;
- window.gon = { api_version: 'v4' };
+ window.gon.api_version = 'v4';
mock = new MockAdapter(axios);
jest.spyOn(axios, 'post');
@@ -39,17 +37,15 @@ describe('~/environments/components/stop_stale_environments_modal.vue', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
jest.resetAllMocks();
- window.gon = originalGon;
});
- it('sets the correct min and max dates', async () => {
+ it('sets the correct min and max dates', () => {
expect(before.props().minDate.toISOString()).toBe(TEN_YEARS_AGO.toISOString());
expect(before.props().maxDate.toISOString()).toBe(ONE_WEEK_AGO.toISOString());
});
- it('requests cleanup when submit is clicked', async () => {
+ it('requests cleanup when submit is clicked', () => {
mock.onPost().replyOnce(HTTP_STATUS_OK);
wrapper.findComponent(GlModal).vm.$emit('primary');
const url = STOP_STALE_ENVIRONMENTS_PATH.replace(':id', 1).replace(':version', 'v4');
diff --git a/spec/frontend/error_tracking/components/error_details_info_spec.js b/spec/frontend/error_tracking/components/error_details_info_spec.js
new file mode 100644
index 00000000000..4a741a4c31e
--- /dev/null
+++ b/spec/frontend/error_tracking/components/error_details_info_spec.js
@@ -0,0 +1,190 @@
+import { GlLink, GlCard } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import ErrorDetailsInfo from '~/error_tracking/components/error_details_info.vue';
+import { trackClickErrorLinkToSentryOptions } from '~/error_tracking/events_tracking';
+import Tracking from '~/tracking';
+
+jest.mock('~/tracking');
+
+describe('ErrorDetails', () => {
+ let wrapper;
+
+ const MOCK_DEFAULT_ERROR = {
+ id: 'gid://gitlab/Gitlab::ErrorTracking::DetailedError/129381',
+ sentryId: 129381,
+ title: 'Issue title',
+ externalUrl: 'http://sentry.gitlab.net/gitlab',
+ firstSeen: '2017-05-26T13:32:48Z',
+ lastSeen: '2018-05-26T13:32:48Z',
+ count: 12,
+ userCount: 2,
+ integrated: false,
+ };
+
+ function mountComponent(error = {}) {
+ wrapper = shallowMountExtended(ErrorDetailsInfo, {
+ stubs: { GlCard },
+ propsData: {
+ error: {
+ ...MOCK_DEFAULT_ERROR,
+ ...error,
+ },
+ },
+ });
+ }
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('should render a card with error counts', () => {
+ expect(wrapper.findByTestId('error-count-card').text()).toContain('Events 12');
+ });
+
+ it('should render a card with user counts', () => {
+ expect(wrapper.findByTestId('user-count-card').text()).toContain('Users 2');
+ });
+
+ describe('release links', () => {
+ it('if firstReleaseVersion is missing, does not render a card', () => {
+ expect(wrapper.findByTestId('first-release-card').exists()).toBe(false);
+ });
+
+ describe('if firstReleaseVersion link exists', () => {
+ it('renders the first release card', () => {
+ mountComponent({
+ firstReleaseVersion: 'first-release-version',
+ });
+ const card = wrapper.findByTestId('first-release-card');
+ expect(card.exists()).toBe(true);
+ expect(card.text()).toContain('First seen');
+ expect(card.findComponent(GlLink).exists()).toBe(true);
+ expect(card.findComponent(TimeAgoTooltip).exists()).toBe(true);
+ });
+
+ it('renders a link to the commit if error is integrated', () => {
+ mountComponent({
+ externalBaseUrl: 'external-base-url',
+ firstReleaseVersion: 'first-release-version',
+ firstSeen: '2023-04-20T17:02:06+00:00',
+ integrated: true,
+ });
+ expect(
+ wrapper.findByTestId('first-release-card').findComponent(GlLink).attributes('href'),
+ ).toBe('external-base-url/-/commit/first-release-version');
+ });
+
+ it('renders a link to the release if error is not integrated', () => {
+ mountComponent({
+ externalBaseUrl: 'external-base-url',
+ firstReleaseVersion: 'first-release-version',
+ firstSeen: '2023-04-20T17:02:06+00:00',
+ integrated: false,
+ });
+ expect(
+ wrapper.findByTestId('first-release-card').findComponent(GlLink).attributes('href'),
+ ).toBe('external-base-url/releases/first-release-version');
+ });
+ });
+
+ it('if lastReleaseVersion is missing, does not render a card', () => {
+ expect(wrapper.findByTestId('last-release-card').exists()).toBe(false);
+ });
+
+ describe('if lastReleaseVersion link exists', () => {
+ it('renders the last release card', () => {
+ mountComponent({
+ lastReleaseVersion: 'last-release-version',
+ });
+ const card = wrapper.findByTestId('last-release-card');
+ expect(card.exists()).toBe(true);
+ expect(card.text()).toContain('Last seen');
+ expect(card.findComponent(GlLink).exists()).toBe(true);
+ expect(card.findComponent(TimeAgoTooltip).exists()).toBe(true);
+ });
+
+ it('renders a link to the commit if error is integrated', () => {
+ mountComponent({
+ externalBaseUrl: 'external-base-url',
+ lastReleaseVersion: 'last-release-version',
+ lastSeen: '2023-04-20T17:02:06+00:00',
+ integrated: true,
+ });
+ expect(
+ wrapper.findByTestId('last-release-card').findComponent(GlLink).attributes('href'),
+ ).toBe('external-base-url/-/commit/last-release-version');
+ });
+
+ it('renders a link to the release if error is integrated', () => {
+ mountComponent({
+ externalBaseUrl: 'external-base-url',
+ lastReleaseVersion: 'last-release-version',
+ lastSeen: '2023-04-20T17:02:06+00:00',
+ integrated: false,
+ });
+ expect(
+ wrapper.findByTestId('last-release-card').findComponent(GlLink).attributes('href'),
+ ).toBe('external-base-url/releases/last-release-version');
+ });
+ });
+ });
+
+ describe('gitlab commit link', () => {
+ it('does not render a card with gitlab commit link, if gitlabCommitPath does not exist', () => {
+ expect(wrapper.findByTestId('gitlab-commit-card').exists()).toBe(false);
+ });
+
+ it('should render a card with gitlab commit link, if gitlabCommitPath exists', () => {
+ mountComponent({
+ gitlabCommit: 'gitlab-long-commit',
+ gitlabCommitPath: 'commit-path',
+ });
+ const card = wrapper.findByTestId('gitlab-commit-card');
+ expect(card.exists()).toBe(true);
+ expect(card.text()).toContain('GitLab commit');
+ const link = card.findComponent(GlLink);
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe('commit-path');
+ expect(link.text()).toBe('gitlab-lon');
+ });
+ });
+
+ describe('external url link', () => {
+ const findExternalUrlLink = () => wrapper.findByTestId('external-url-link');
+
+ it('should not render an external link if integrated', () => {
+ mountComponent({
+ integrated: true,
+ externalUrl: 'external-url',
+ });
+ expect(findExternalUrlLink().exists()).toBe(false);
+ });
+
+ it('should render an external link if not integrated', () => {
+ mountComponent({
+ integrated: false,
+ externalUrl: 'external-url',
+ });
+ const link = findExternalUrlLink();
+ expect(link.exists()).toBe(true);
+ expect(link.text()).toContain('external-url');
+ });
+
+ it('should track external Sentry link views', async () => {
+ Tracking.event.mockClear();
+
+ mountComponent({
+ integrated: false,
+ externalUrl: 'external-url',
+ });
+ await findExternalUrlLink().trigger('click');
+
+ const { category, action, label, property } = trackClickErrorLinkToSentryOptions(
+ 'external-url',
+ );
+ expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 9d6e46be8c4..8700301ef73 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -13,16 +13,17 @@ import Vuex from 'vuex';
import { severityLevel, severityLevelVariant, errorStatus } from '~/error_tracking/constants';
import ErrorDetails from '~/error_tracking/components/error_details.vue';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
+import ErrorDetailsInfo from '~/error_tracking/components/error_details_info.vue';
import {
- trackClickErrorLinkToSentryOptions,
trackErrorDetailsViewsOptions,
trackErrorStatusUpdateOptions,
-} from '~/error_tracking/utils';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+ trackCreateIssueFromError,
+} from '~/error_tracking/events_tracking';
+import { createAlert, VARIANT_WARNING } from '~/alert';
import { __ } from '~/locale';
import Tracking from '~/tracking';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(Vuex);
@@ -45,7 +46,6 @@ describe('ErrorDetails', () => {
wrapper.find('[data-testid="update-ignore-status-btn"]');
const findUpdateResolveStatusButton = () =>
wrapper.find('[data-testid="update-resolve-status-btn"]');
- const findExternalUrl = () => wrapper.find('[data-testid="external-url-link"]');
const findAlert = () => wrapper.findComponent(GlAlert);
function mountComponent() {
@@ -109,12 +109,6 @@ describe('ErrorDetails', () => {
};
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('loading', () => {
beforeEach(() => {
mountComponent();
@@ -148,7 +142,7 @@ describe('ErrorDetails', () => {
expect(mocks.$apollo.queries.error.stopPolling).not.toHaveBeenCalled();
});
- it('when timeout is hit and no apollo result, stops loading and shows flash', async () => {
+ it('when timeout is hit and no apollo result, stops loading and shows alert', async () => {
Date.now.mockReturnValue(endTime + 1);
wrapper.vm.onNoApolloResult();
@@ -187,14 +181,6 @@ describe('ErrorDetails', () => {
});
});
- it('should show Sentry error details without stacktrace', () => {
- expect(wrapper.findComponent(GlLink).exists()).toBe(true);
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.findComponent(Stacktrace).exists()).toBe(false);
- expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
- expect(wrapper.findAllComponents(GlButton)).toHaveLength(3);
- });
-
describe('unsafe chars for culprit field', () => {
const findReportedText = () => wrapper.find('[data-qa-selector="reported_text"]');
const culprit = '<script>console.log("surprise!")</script>';
@@ -276,6 +262,16 @@ describe('ErrorDetails', () => {
});
});
+ describe('ErrorDetailsInfo', () => {
+ it('should show ErrorDetailsInfo', async () => {
+ store.state.details.loadingStacktrace = false;
+ await nextTick();
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(wrapper.findComponent(ErrorDetailsInfo).exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
describe('Stacktrace', () => {
it('should show stacktrace', async () => {
store.state.details.loadingStacktrace = false;
@@ -477,91 +473,6 @@ describe('ErrorDetails', () => {
});
});
});
-
- describe('GitLab commit link', () => {
- const gitlabCommit = '7975be0116940bf2ad4321f79d02a55c5f7779aa';
- const gitlabCommitPath =
- '/gitlab-org/gitlab-test/commit/7975be0116940bf2ad4321f79d02a55c5f7779aa';
- const findGitLabCommitLink = () => wrapper.find(`[href$="${gitlabCommitPath}"]`);
-
- it('should display a link', async () => {
- mocks.$apollo.queries.error.loading = false;
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- error: {
- gitlabCommit,
- gitlabCommitPath,
- },
- });
- await nextTick();
- expect(findGitLabCommitLink().exists()).toBe(true);
- });
-
- it('should not display a link', async () => {
- mocks.$apollo.queries.error.loading = false;
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- error: {
- gitlabCommit: null,
- },
- });
- await nextTick();
- expect(findGitLabCommitLink().exists()).toBe(false);
- });
- });
-
- describe('Release links', () => {
- const firstReleaseVersion = '7975be01';
- const firstCommitLink = '/gitlab/-/commit/7975be01';
- const firstReleaseLink = '/sentry/releases/7975be01';
- const findFirstCommitLink = () => wrapper.find(`[href$="${firstCommitLink}"]`);
- const findFirstReleaseLink = () => wrapper.find(`[href$="${firstReleaseLink}"]`);
-
- const lastReleaseVersion = '6ca5a5c1';
- const lastCommitLink = '/gitlab/-/commit/6ca5a5c1';
- const lastReleaseLink = '/sentry/releases/6ca5a5c1';
- const findLastCommitLink = () => wrapper.find(`[href$="${lastCommitLink}"]`);
- const findLastReleaseLink = () => wrapper.find(`[href$="${lastReleaseLink}"]`);
-
- it('should display links to Sentry', async () => {
- mocks.$apollo.queries.error.loading = false;
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({
- error: {
- firstReleaseVersion,
- lastReleaseVersion,
- externalBaseUrl: '/sentry',
- },
- });
-
- expect(findFirstReleaseLink().exists()).toBe(true);
- expect(findLastReleaseLink().exists()).toBe(true);
- expect(findFirstCommitLink().exists()).toBe(false);
- expect(findLastCommitLink().exists()).toBe(false);
- });
-
- it('should display links to GitLab when integrated', async () => {
- mocks.$apollo.queries.error.loading = false;
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({
- error: {
- firstReleaseVersion,
- lastReleaseVersion,
- integrated: true,
- externalBaseUrl: '/gitlab',
- },
- });
-
- expect(findFirstCommitLink().exists()).toBe(true);
- expect(findLastCommitLink().exists()).toBe(true);
- expect(findFirstReleaseLink().exists()).toBe(false);
- expect(findLastReleaseLink().exists()).toBe(false);
- });
- });
});
describe('Snowplow tracking', () => {
@@ -582,24 +493,21 @@ describe('ErrorDetails', () => {
});
it('should track IGNORE status update', async () => {
- Tracking.event.mockClear();
await findUpdateIgnoreStatusButton().trigger('click');
const { category, action } = trackErrorStatusUpdateOptions('ignored');
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
it('should track RESOLVE status update', async () => {
- Tracking.event.mockClear();
await findUpdateResolveStatusButton().trigger('click');
const { category, action } = trackErrorStatusUpdateOptions('resolved');
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
- it('should track external Sentry link views', async () => {
- Tracking.event.mockClear();
- await findExternalUrl().trigger('click');
- const { category, action, label, property } = trackClickErrorLinkToSentryOptions(externalUrl);
- expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property });
+ it('should track create issue button click', async () => {
+ await wrapper.find('[data-qa-selector="create_issue_button"]').vm.$emit('click');
+ const { category, action } = trackCreateIssueFromError;
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
});
diff --git a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
index 5f6c9ddb4d7..d959d73c86b 100644
--- a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
@@ -29,12 +29,6 @@ describe('Error Tracking Actions', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
const findButtons = () => wrapper.findAllComponents(GlButton);
describe('when error status is unresolved', () => {
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 31473899145..6d4e92cf91f 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -5,7 +5,12 @@ import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
-import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '~/error_tracking/utils';
+import {
+ trackErrorListViewsOptions,
+ trackErrorStatusUpdateOptions,
+ trackErrorStatusFilterOptions,
+ trackErrorSortedByField,
+} from '~/error_tracking/events_tracking';
import Tracking from '~/tracking';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import errorsList from './list_mock.json';
@@ -98,12 +103,6 @@ describe('ErrorTrackingList', () => {
});
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('loading', () => {
beforeEach(() => {
store.state.list.loading = true;
@@ -452,32 +451,34 @@ describe('ErrorTrackingList', () => {
describe('When pagination is required', () => {
describe('and previous cursor is not available', () => {
- beforeEach(async () => {
+ beforeEach(() => {
store.state.list.loading = false;
delete store.state.list.pagination.previous;
mountComponent();
});
- it('disables Prev button in the pagination', async () => {
+ it('disables Prev button in the pagination', () => {
expect(findPagination().props('prevPage')).toBe(null);
expect(findPagination().props('nextPage')).not.toBe(null);
});
});
describe('and next cursor is not available', () => {
- beforeEach(async () => {
+ beforeEach(() => {
store.state.list.loading = false;
delete store.state.list.pagination.next;
mountComponent();
});
- it('disables Next button in the pagination', async () => {
+ it('disables Next button in the pagination', () => {
expect(findPagination().props('prevPage')).not.toBe(null);
expect(findPagination().props('nextPage')).toBe(null);
});
});
describe('and the user is not on the first page', () => {
describe('and the previous button is clicked', () => {
- beforeEach(async () => {
+ const currentPage = 2;
+
+ beforeEach(() => {
store.state.list.loading = false;
mountComponent({
stubs: {
@@ -485,15 +486,12 @@ describe('ErrorTrackingList', () => {
GlPagination: false,
},
});
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ pageValue: 2 });
- await nextTick();
+ findPagination().vm.$emit('input', currentPage);
});
it('fetches the previous page of results', () => {
expect(wrapper.find('.prev-page-item').attributes('aria-disabled')).toBe(undefined);
- wrapper.vm.goToPrevPage();
+ findPagination().vm.$emit('input', currentPage - 1);
expect(actions.fetchPaginatedResults).toHaveBeenCalled();
expect(actions.fetchPaginatedResults).toHaveBeenLastCalledWith(
expect.anything(),
@@ -531,6 +529,8 @@ describe('ErrorTrackingList', () => {
stubs: {
GlTable: false,
GlLink: false,
+ GlDropdown: false,
+ GlDropdownItem: false,
},
});
});
@@ -541,7 +541,6 @@ describe('ErrorTrackingList', () => {
});
it('should track status updates', async () => {
- Tracking.event.mockClear();
const status = 'ignored';
findErrorActions().vm.$emit('update-issue-status', {
errorId: 1,
@@ -553,5 +552,19 @@ describe('ErrorTrackingList', () => {
const { category, action } = trackErrorStatusUpdateOptions(status);
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
+
+ it('should track error filter', () => {
+ const findStatusFilter = () => findStatusFilterDropdown().find('.dropdown-item');
+ findStatusFilter().trigger('click');
+ const { category, action } = trackErrorStatusFilterOptions('unresolved');
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
+ });
+
+ it('should track error sorting', () => {
+ const findSortItem = () => findSortDropdown().find('.dropdown-item');
+ findSortItem().trigger('click');
+ const { category, action } = trackErrorSortedByField('last_seen');
+ expect(Tracking.event).toHaveBeenCalledWith(category, action);
+ });
});
});
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
index 0de4277b08a..45fc1ad04ff 100644
--- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -28,12 +28,6 @@ describe('Stacktrace Entry', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
it('should render stacktrace entry collapsed', () => {
mountComponent({ lines });
expect(wrapper.findComponent(StackTraceEntry).exists()).toBe(true);
diff --git a/spec/frontend/error_tracking/components/stacktrace_spec.js b/spec/frontend/error_tracking/components/stacktrace_spec.js
index cd5a57f5683..29301c3e5ee 100644
--- a/spec/frontend/error_tracking/components/stacktrace_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_spec.js
@@ -25,12 +25,6 @@ describe('ErrorDetails', () => {
}
describe('Stacktrace', () => {
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
it('should render single Stacktrace entry', () => {
mountComponent([stackTraceEntry]);
expect(wrapper.findAllComponents(StackTraceEntry).length).toBe(1);
diff --git a/spec/frontend/error_tracking/utils_spec.js b/spec/frontend/error_tracking/events_tracking_spec.js
index a0d6f7f009d..10479d863cf 100644
--- a/spec/frontend/error_tracking/utils_spec.js
+++ b/spec/frontend/error_tracking/events_tracking_spec.js
@@ -1,4 +1,4 @@
-import * as errorTrackingUtils from '~/error_tracking/utils';
+import * as errorTrackingUtils from '~/error_tracking/events_tracking';
const externalUrl = 'https://sentry.io/organizations/test-sentry-nk/issues/1/?project=1';
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index 3ec43010d80..44db4780ba9 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -2,12 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
let mock;
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
index 383d8aaeb20..0aeb8b19a9e 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
HTTP_STATUS_BAD_REQUEST,
@@ -14,7 +14,7 @@ import Poll from '~/lib/utils/poll';
let mockedAdapter;
let mockedRestart;
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('Sentry error details store actions', () => {
@@ -48,7 +48,7 @@ describe('Sentry error details store actions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mockedAdapter.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 590983bd93d..24a26476455 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('error tracking actions', () => {
let mock;
@@ -38,7 +38,7 @@ describe('error tracking actions', () => {
);
});
- it('should show flash on API error', async () => {
+ it('should show alert on API error', async () => {
mock.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index 7a714cc1ebc..9b7701d46bc 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -68,12 +68,6 @@ describe('error tracking settings app', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('section', () => {
it('renders the form and dropdown', () => {
expect(wrapper.findComponent(ErrorTrackingForm).exists()).toBe(true);
@@ -92,7 +86,7 @@ describe('error tracking settings app', () => {
store.state.settingsLoading = true;
await nextTick();
- expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBe('true');
+ expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
index 69d684faec2..b1cf5d673f1 100644
--- a/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
+++ b/spec/frontend/error_tracking_settings/components/error_tracking_form_spec.js
@@ -24,12 +24,6 @@ describe('error tracking settings form', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('an empty form', () => {
it('is rendered', () => {
expect(wrapper.findAllComponents(GlFormInput).length).toBe(2);
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index 8653ebac20d..03d090c5314 100644
--- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -33,12 +33,6 @@ describe('error tracking settings project dropdown', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('empty project list', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
diff --git a/spec/frontend/experimentation/components/gitlab_experiment_spec.js b/spec/frontend/experimentation/components/gitlab_experiment_spec.js
index f52ebf0f3c4..73db4b9503c 100644
--- a/spec/frontend/experimentation/components/gitlab_experiment_spec.js
+++ b/spec/frontend/experimentation/components/gitlab_experiment_spec.js
@@ -9,7 +9,6 @@ const defaultSlots = {
};
describe('ExperimentComponent', () => {
- const oldGon = window.gon;
let wrapper;
const createComponent = (propsData = defaultProps, slots = defaultSlots) => {
@@ -20,12 +19,6 @@ describe('ExperimentComponent', () => {
window.gon = { experiment: { experiment_name: { variant: expectedVariant } } };
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- window.gon = oldGon;
- });
-
describe('when variant and experiment is set', () => {
it('renders control when it is the active variant', () => {
mockVariant('control');
diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js
index 0d663fd055e..6d9c9dfe65a 100644
--- a/spec/frontend/experimentation/utils_spec.js
+++ b/spec/frontend/experimentation/utils_spec.js
@@ -10,18 +10,15 @@ describe('experiment Utilities', () => {
const ABC_KEY = 'abc';
const DEF_KEY = 'def';
- let origGon;
let origGl;
beforeEach(() => {
- origGon = window.gon;
origGl = window.gl;
window.gon.experiment = {};
window.gl.experiments = {};
});
afterEach(() => {
- window.gon = origGon;
window.gl = origGl;
});
diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
index c1051a14a08..b75e2f653e9 100644
--- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
+++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
@@ -42,7 +42,6 @@ describe('Configure Feature Flags Modal', () => {
wrapper.findAllComponents(GlAlert).filter((c) => c.props('variant') === 'danger');
describe('idle', () => {
- afterEach(() => wrapper.destroy());
beforeEach(factory);
it('should have Primary and Secondary actions', () => {
@@ -51,7 +50,7 @@ describe('Configure Feature Flags Modal', () => {
});
it('should default disable the primary action', () => {
- const [{ disabled }] = findSecondaryAction().attributes;
+ const { disabled } = findSecondaryAction().attributes;
expect(disabled).toBe(true);
});
@@ -112,52 +111,48 @@ describe('Configure Feature Flags Modal', () => {
});
describe('verified', () => {
- afterEach(() => wrapper.destroy());
beforeEach(factory);
it('should enable the secondary action', async () => {
findProjectNameInput().vm.$emit('input', provide.projectName);
await nextTick();
- const [{ disabled }] = findSecondaryAction().attributes;
+ const { disabled } = findSecondaryAction().attributes;
expect(disabled).toBe(false);
});
});
describe('cannot rotate token', () => {
- afterEach(() => wrapper.destroy());
beforeEach(factory.bind(null, { canUserRotateToken: false }));
it('should not display the primary action', () => {
expect(findSecondaryAction()).toBe(null);
});
- it('should not display regenerating instance ID', async () => {
+ it('should not display regenerating instance ID', () => {
expect(findDangerGlAlert().exists()).toBe(false);
});
- it('should disable the project name input', async () => {
+ it('should disable the project name input', () => {
expect(findProjectNameInput().exists()).toBe(false);
});
});
describe('has rotate error', () => {
- afterEach(() => wrapper.destroy());
beforeEach(() => {
factory({ hasRotateError: true });
});
- it('should display an error', async () => {
+ it('should display an error', () => {
expect(wrapper.findByTestId('rotate-error').exists()).toBe(true);
expect(wrapper.find('[name="warning"]').exists()).toBe(true);
});
});
describe('is rotating', () => {
- afterEach(() => wrapper.destroy());
beforeEach(factory.bind(null, { isRotating: true }));
- it('should disable the project name input', async () => {
- expect(findProjectNameInput().attributes('disabled')).toBe('true');
+ it('should disable the project name input', () => {
+ expect(findProjectNameInput().attributes('disabled')).toBeDefined();
});
});
});
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index cf4605e21ea..b8d058e7bc5 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -24,10 +24,6 @@ describe('Edit feature flag form', () => {
});
const factory = (provide = { searchPath: '/search' }) => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
wrapper = shallowMount(EditFeatureFlag, {
store,
provide,
@@ -53,7 +49,6 @@ describe('Edit feature flag form', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/feature_flags/components/empty_state_spec.js b/spec/frontend/feature_flags/components/empty_state_spec.js
index e3cc6f703c4..4aa0b261e2a 100644
--- a/spec/frontend/feature_flags/components/empty_state_spec.js
+++ b/spec/frontend/feature_flags/components/empty_state_spec.js
@@ -44,14 +44,6 @@ describe('feature_flags/components/feature_flags_tab.vue', () => {
},
);
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
describe('alerts', () => {
let alerts;
diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
index a4738fed37e..9fc0119a6c8 100644
--- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
@@ -27,7 +27,6 @@ describe('Feature flags > Environments dropdown', () => {
const findDropdownMenu = () => wrapper.find('.dropdown-menu');
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index e80f9c559c4..c0cfec384f0 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -1,9 +1,9 @@
import { GlAlert, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'spec/test_constants';
import ConfigureFeatureFlagsModal from '~/feature_flags/components/configure_feature_flags_modal.vue';
import EmptyState from '~/feature_flags/components/empty_state.vue';
@@ -11,7 +11,7 @@ import FeatureFlagsComponent from '~/feature_flags/components/feature_flags.vue'
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
import createStore from '~/feature_flags/store/index';
import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { getRequestData } from '../mock_data';
@@ -43,7 +43,7 @@ describe('Feature flags', () => {
let mock;
let store;
- const factory = (provide = mockData, fn = mount) => {
+ const factory = (provide = mockData, fn = mountExtended) => {
store = createStore(mockState);
wrapper = fn(FeatureFlagsComponent, {
store,
@@ -54,10 +54,13 @@ describe('Feature flags', () => {
});
};
- const configureButton = () => wrapper.find('[data-testid="ff-configure-button"]');
- const newButton = () => wrapper.find('[data-testid="ff-new-button"]');
- const userListButton = () => wrapper.find('[data-testid="ff-user-list-button"]');
+ const configureButton = () => wrapper.findByTestId('ff-configure-button');
+ const newButton = () => wrapper.findByTestId('ff-new-button');
+ const userListButton = () => wrapper.findByTestId('ff-user-list-button');
const limitAlert = () => wrapper.findComponent(GlAlert);
+ const findTablePagination = () => wrapper.findComponent(TablePagination);
+ const findFeatureFlagsTable = () => wrapper.findComponent(FeatureFlagsTable);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -65,8 +68,6 @@ describe('Feature flags', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
- wrapper = null;
});
describe('when limit exceeded', () => {
@@ -83,7 +84,7 @@ describe('Feature flags', () => {
it('makes the new feature flag button do nothing if clicked', () => {
expect(newButton().exists()).toBe(true);
expect(newButton().props('disabled')).toBe(false);
- expect(newButton().props('href')).toBe(undefined);
+ expect(newButton().props('href')).toBeUndefined();
});
it('shows a feature flags limit reached alert', () => {
@@ -96,7 +97,7 @@ describe('Feature flags', () => {
await limitAlert().vm.$emit('dismiss');
});
- it('hides the alert', async () => {
+ it('hides the alert', () => {
expect(limitAlert().exists()).toBe(false);
});
@@ -173,12 +174,11 @@ describe('Feature flags', () => {
factory();
await waitForPromises();
- await nextTick();
- emptyState = wrapper.findComponent(GlEmptyState);
+ emptyState = findEmptyState();
});
- it('should render the empty state', async () => {
+ it('should render the empty state', () => {
expect(emptyState.exists()).toBe(true);
});
@@ -221,7 +221,7 @@ describe('Feature flags', () => {
});
it('should render a table with feature flags', () => {
- const table = wrapper.findComponent(FeatureFlagsTable);
+ const table = findFeatureFlagsTable();
expect(table.exists()).toBe(true);
expect(table.props('featureFlags')).toEqual(
expect.arrayContaining([
@@ -234,7 +234,7 @@ describe('Feature flags', () => {
});
it('should toggle a flag when receiving the toggle-flag event', () => {
- const table = wrapper.findComponent(FeatureFlagsTable);
+ const table = findFeatureFlagsTable();
const [flag] = table.props('featureFlags');
table.vm.$emit('toggle-flag', flag);
@@ -257,15 +257,15 @@ describe('Feature flags', () => {
describe('pagination', () => {
it('should render pagination', () => {
- expect(wrapper.findComponent(TablePagination).exists()).toBe(true);
+ expect(findTablePagination().exists()).toBe(true);
});
it('should make an API request when page is clicked', () => {
- jest.spyOn(wrapper.vm, 'updateFeatureFlagOptions');
- wrapper.findComponent(TablePagination).vm.change(4);
+ const axiosGet = jest.spyOn(axios, 'get');
+ findTablePagination().vm.change(4);
- expect(wrapper.vm.updateFeatureFlagOptions).toHaveBeenCalledWith({
- page: '4',
+ expect(axiosGet).toHaveBeenCalledWith('http://test.host/endpoint.json', {
+ params: { page: '4' },
});
});
});
@@ -274,16 +274,12 @@ describe('Feature flags', () => {
describe('unsuccessful request', () => {
beforeEach(() => {
- mock
- .onGet(mockState.endpoint, { params: { page: '1' } })
- .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
-
factory();
return waitForPromises();
});
it('should render error state', () => {
- const emptyState = wrapper.findComponent(GlEmptyState);
+ const emptyState = findEmptyState();
expect(emptyState.props('title')).toEqual('There was an error fetching the feature flags.');
expect(emptyState.props('description')).toEqual(
'Try again in a few moments or contact your support team.',
@@ -305,20 +301,12 @@ describe('Feature flags', () => {
});
describe('rotate instance id', () => {
- beforeEach(() => {
- mock
- .onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .reply(HTTP_STATUS_OK, getRequestData, {});
- factory();
- return waitForPromises();
- });
-
it('should fire the rotate action when a `token` event is received', () => {
- const actionSpy = jest.spyOn(wrapper.vm, 'rotateInstanceId');
- const modal = wrapper.findComponent(ConfigureFeatureFlagsModal);
- modal.vm.$emit('token');
+ factory();
+ const axiosPost = jest.spyOn(axios, 'post');
+ wrapper.findComponent(ConfigureFeatureFlagsModal).vm.$emit('token');
- expect(actionSpy).toHaveBeenCalled();
+ expect(axiosPost).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/feature_flags/components/feature_flags_table_spec.js b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
index f23bca54b55..02a8e38dc2a 100644
--- a/spec/frontend/feature_flags/components/feature_flags_table_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_table_spec.js
@@ -1,5 +1,6 @@
-import { GlToggle } from '@gitlab/ui';
+import { GlIcon, GlToggle } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import { mockTracking } from 'helpers/tracking_helper';
@@ -46,6 +47,13 @@ const getDefaultProps = () => ({
},
],
},
+ {
+ id: 2,
+ iid: 2,
+ active: true,
+ name: 'flag without description',
+ description: '',
+ },
],
});
@@ -61,6 +69,9 @@ describe('Feature flag table', () => {
csrfToken: 'fakeToken',
},
...opts,
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
@@ -105,10 +116,6 @@ describe('Feature flag table', () => {
it('Should render a feature flag column', () => {
expect(wrapper.find('.js-feature-flag-title').exists()).toBe(true);
expect(trimText(wrapper.find('.feature-flag-name').text())).toEqual('flag name');
-
- expect(trimText(wrapper.find('.feature-flag-description').text())).toEqual(
- 'flag description',
- );
});
it('should render an environments specs label', () => {
@@ -125,6 +132,37 @@ describe('Feature flag table', () => {
});
});
+ describe.each(getDefaultProps().featureFlags)('description tooltip', (featureFlag) => {
+ beforeEach(() => {
+ createWrapper(props);
+ });
+
+ const haveInfoIcon = Boolean(featureFlag.description);
+
+ it(`${haveInfoIcon ? 'displays' : "doesn't display"} an information icon`, () => {
+ expect(
+ wrapper
+ .findByTestId(featureFlag.id)
+ .find('.feature-flag-description')
+ .findComponent(GlIcon)
+ .exists(),
+ ).toBe(haveInfoIcon);
+ });
+
+ if (haveInfoIcon) {
+ it('includes a tooltip', () => {
+ const icon = wrapper
+ .findByTestId(featureFlag.id)
+ .find('.feature-flag-description')
+ .findComponent(GlIcon);
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(featureFlag.description);
+ });
+ }
+ });
+
describe('when active and with an update toggle', () => {
let toggle;
let spy;
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js
index 7dd7c709c94..f66e25698e6 100644
--- a/spec/frontend/feature_flags/components/form_spec.js
+++ b/spec/frontend/feature_flags/components/form_spec.js
@@ -42,10 +42,6 @@ describe('feature flag form', () => {
Api.fetchFeatureFlagUserLists.mockResolvedValue({ data: [] });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render provided submitText', () => {
factory(requiredProps);
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 14e1f34bc59..6156addd63f 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -22,10 +22,6 @@ describe('New Environments Dropdown', () => {
afterEach(() => {
axiosMock.restore();
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
});
describe('before results', () => {
diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
index 300d0e47082..c5418477661 100644
--- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
@@ -22,10 +22,6 @@ describe('New feature flag form', () => {
});
const factory = (opts = {}) => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
wrapper = shallowMount(NewFeatureFlag, {
store,
provide: {
@@ -46,10 +42,6 @@ describe('New feature flag form', () => {
factory();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with error', () => {
it('should render the error', async () => {
store.dispatch('receiveCreateFeatureFlagError', { message: ['The name is required'] });
diff --git a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
index 70a9156b5a9..a6eb81ef6f0 100644
--- a/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/flexible_rollout_spec.js
@@ -20,14 +20,6 @@ describe('feature_flags/components/strategies/flexible_rollout.vue', () => {
const factory = (props = {}) =>
mount(FlexibleRollout, { propsData: { ...DEFAULT_PROPS, ...props } });
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
describe('with valid percentage', () => {
beforeEach(() => {
wrapper = factory();
diff --git a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
index 23ad0d3a08d..8ad70466e90 100644
--- a/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/parameter_form_group_spec.js
@@ -24,14 +24,6 @@ describe('~/feature_flags/strategies/parameter_form_group.vue', () => {
slot = wrapper.find('[data-testid="slot"]');
});
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
it('should display the default slot', () => {
expect(slot.exists()).toBe(true);
});
diff --git a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
index cb422a018f9..e00869fdd09 100644
--- a/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/percent_rollout_spec.js
@@ -18,14 +18,6 @@ describe('~/feature_flags/components/strategies/percent_rollout.vue', () => {
const factory = (props = {}) =>
mount(PercentRollout, { propsData: { ...DEFAULT_PROPS, ...props } });
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
describe('with valid percentage', () => {
beforeEach(() => {
wrapper = factory();
diff --git a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
index 0a72714c22a..f3b8535a650 100644
--- a/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
+++ b/spec/frontend/feature_flags/components/strategies/users_with_id_spec.js
@@ -18,14 +18,6 @@ describe('~/feature_flags/components/users_with_id.vue', () => {
textarea = wrapper.findComponent(GlFormTextarea);
});
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
it('should display the current value of the parameters', () => {
expect(textarea.element.value).toBe(usersWithIdStrategy.parameters.userIds);
});
diff --git a/spec/frontend/feature_flags/components/strategy_parameters_spec.js b/spec/frontend/feature_flags/components/strategy_parameters_spec.js
index d0f1f7d0e2a..bc34888d1c1 100644
--- a/spec/frontend/feature_flags/components/strategy_parameters_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_parameters_spec.js
@@ -28,14 +28,6 @@ describe('~/feature_flags/components/strategy_parameters.vue', () => {
},
});
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
-
- wrapper = null;
- });
-
describe.each`
name | component
${ROLLOUT_STRATEGY_ALL_USERS} | ${Default}
diff --git a/spec/frontend/feature_flags/components/strategy_spec.js b/spec/frontend/feature_flags/components/strategy_spec.js
index 84d4180fe63..1428d99aa76 100644
--- a/spec/frontend/feature_flags/components/strategy_spec.js
+++ b/spec/frontend/feature_flags/components/strategy_spec.js
@@ -44,10 +44,6 @@ describe('Feature flags strategy', () => {
provide,
},
) => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
wrapper = mount(Strategy, { store: createStore({ projectId: '1' }), ...opts });
};
@@ -55,13 +51,6 @@ describe('Feature flags strategy', () => {
Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [userList] });
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
describe('helper links', () => {
const propsData = { strategy: {}, index: 0, userLists: [userList] };
factory({ propsData, provide });
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index 4d5cb26810e..4609bfc23d7 100644
--- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_CREATED, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('feature highlight helper', () => {
describe('dismiss', () => {
@@ -26,7 +26,7 @@ describe('feature highlight helper', () => {
await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything());
});
- it('triggers flash when dismiss request fails', async () => {
+ it('triggers an alert when dismiss request fails', async () => {
mockAxios
.onPost(endpoint, { feature_name: highlightId })
.replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
diff --git a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
index 650f9eb1bbc..66ea22cece3 100644
--- a/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_popover_spec.js
@@ -29,11 +29,6 @@ describe('feature_highlight/feature_highlight_popover', () => {
buildWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders popover target', () => {
expect(findPopoverTarget().exists()).toBe(true);
});
diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
index ebed477fa2f..5f0e928e1fe 100644
--- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -22,11 +22,6 @@ describe('Recent Searches Dropdown Content', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when local storage is not available', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js
index 26f12673f68..8ddf8390431 100644
--- a/spec/frontend/filtered_search/dropdown_user_spec.js
+++ b/spec/frontend/filtered_search/dropdown_user_spec.js
@@ -1,4 +1,5 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import DropdownUser from '~/filtered_search/dropdown_user';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer';
@@ -68,20 +69,14 @@ describe('Dropdown User', () => {
'/gitlab_directory/-/autocomplete/users.json',
);
});
-
- afterEach(() => {
- window.gon = {};
- });
});
describe('hideCurrentUser', () => {
- const fixtureTemplate = 'merge_requests/merge_request_list.html';
-
let dropdown;
let authorFilterDropdownElement;
beforeEach(() => {
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(htmlMergeRequestList);
authorFilterDropdownElement = document.querySelector('#js-dropdown-author');
const dummyInput = document.createElement('div');
dropdown = new DropdownUser({
diff --git a/spec/frontend/filtered_search/dropdown_utils_spec.js b/spec/frontend/filtered_search/dropdown_utils_spec.js
index 2030b45b44c..d8a5b493b7a 100644
--- a/spec/frontend/filtered_search/dropdown_utils_spec.js
+++ b/spec/frontend/filtered_search/dropdown_utils_spec.js
@@ -1,12 +1,11 @@
-import { loadHTMLFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlMergeRequestList from 'test_fixtures/merge_requests/merge_request_list.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Dropdown Utils', () => {
- const issuableListFixture = 'merge_requests/merge_request_list.html';
-
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
const escaped = DropdownUtils.getEscapedText('textWithoutSpace');
@@ -355,7 +354,7 @@ describe('Dropdown Utils', () => {
let authorToken;
beforeEach(() => {
- loadHTMLFixture(issuableListFixture);
+ setHTMLFixture(htmlMergeRequestList);
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 26af7af701b..8c16ff100eb 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -8,11 +8,11 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
import { visitUrl, getParameterByName } from '~/lib/utils/url_utility';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
getParameterByName: jest.fn(),
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index d3fa8fae9ab..138a4e183a9 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -5,11 +5,11 @@ import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import { TEST_HOST } from 'helpers/test_constants';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import VisualTokenValue from '~/filtered_search/visual_token_value';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Filtered Search Visual Tokens', () => {
const findElements = (tokenElement) => {
diff --git a/spec/frontend/fixtures/abuse_reports.rb b/spec/frontend/fixtures/abuse_reports.rb
index d8c8737b125..ad0fb9be8dc 100644
--- a/spec/frontend/fixtures/abuse_reports.rb
+++ b/spec/frontend/fixtures/abuse_reports.rb
@@ -14,6 +14,8 @@ RSpec.describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :co
render_views
before do
+ stub_feature_flags(abuse_reports_list: false)
+
sign_in(admin)
enable_admin_mode!(admin)
end
diff --git a/spec/frontend/fixtures/api_deploy_keys.rb b/spec/frontend/fixtures/api_deploy_keys.rb
index 5ffc726f086..8c926296817 100644
--- a/spec/frontend/fixtures/api_deploy_keys.rb
+++ b/spec/frontend/fixtures/api_deploy_keys.rb
@@ -7,6 +7,7 @@ RSpec.describe API::DeployKeys, '(JavaScript fixtures)', type: :request do
include JavaScriptFixturesHelpers
let_it_be(:admin) { create(:admin) }
+ let_it_be(:path) { "/deploy_keys" }
let_it_be(:project) { create(:project) }
let_it_be(:project2) { create(:project) }
let_it_be(:deploy_key) { create(:deploy_key, public: true) }
@@ -17,8 +18,10 @@ RSpec.describe API::DeployKeys, '(JavaScript fixtures)', type: :request do
let_it_be(:deploy_keys_project3) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key2) }
let_it_be(:deploy_keys_project4) { create(:deploy_keys_project, :write_access, project: project2, deploy_key: deploy_key2) }
+ it_behaves_like 'GET request permissions for admin mode'
+
it 'api/deploy_keys/index.json' do
- get api("/deploy_keys", admin)
+ get api("/deploy_keys", admin, admin_mode: true)
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
index d1dfd223419..24c47d8d139 100644
--- a/spec/frontend/fixtures/api_projects.rb
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -6,10 +6,11 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
- let(:namespace) { create(:namespace, name: 'gitlab-test') }
- let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
- let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
- let(:user) { project.owner }
+ let_it_be(:namespace) { create(:namespace, name: 'gitlab-test') }
+ let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+ let_it_be(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
+ let_it_be(:user) { project.owner }
+ let_it_be(:personal_projects) { create_list(:project, 3, namespace: user.namespace, topics: create_list(:topic, 5)) }
it 'api/projects/get.json' do
get api("/projects/#{project.id}", user)
@@ -28,4 +29,10 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
expect(response).to be_successful
end
+
+ it 'api/users/projects/get.json' do
+ get api("/users/#{user.id}/projects", user)
+
+ expect(response).to be_successful
+ end
end
diff --git a/spec/frontend/fixtures/comment_templates.rb b/spec/frontend/fixtures/comment_templates.rb
new file mode 100644
index 00000000000..32f425d7ebd
--- /dev/null
+++ b/spec/frontend/fixtures/comment_templates.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile do
+ include JavaScriptFixturesHelpers
+ include ApiHelpers
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ context 'when user has no comment templates' do
+ base_input_path = 'comment_templates/queries/'
+ base_output_path = 'graphql/comment_templates/'
+ query_name = 'saved_replies.query.graphql'
+
+ it "#{base_output_path}saved_replies_empty.query.graphql.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when user has comment templates' do
+ base_input_path = 'comment_templates/queries/'
+ base_output_path = 'graphql/comment_templates/'
+ query_name = 'saved_replies.query.graphql'
+
+ it "#{base_output_path}saved_replies.query.graphql.json" do
+ create(:saved_reply, user: current_user)
+ create(:saved_reply, user: current_user)
+
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when user creates comment template' do
+ base_input_path = 'comment_templates/queries/'
+ base_output_path = 'graphql/comment_templates/'
+ query_name = 'create_saved_reply.mutation.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user, variables: { name: "Test", content: "Test content" })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when user creates comment template and it errors' do
+ base_input_path = 'comment_templates/queries/'
+ base_output_path = 'graphql/comment_templates/'
+ query_name = 'create_saved_reply.mutation.graphql'
+
+ it "#{base_output_path}create_saved_reply_with_errors.mutation.graphql.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user, variables: { name: nil, content: nil })
+
+ expect(flattened_errors).not_to be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/environments.rb b/spec/frontend/fixtures/environments.rb
index 77e2a96b328..81f1eb11e3e 100644
--- a/spec/frontend/fixtures/environments.rb
+++ b/spec/frontend/fixtures/environments.rb
@@ -44,7 +44,7 @@ RSpec.describe 'Environments (JavaScript fixtures)', feature_category: :environm
end
let_it_be(:deployment_success) do
- create(:deployment, :success, environment: environment, deployable: build)
+ create(:deployment, :success, environment: environment, deployable: build, finished_at: 1.hour.since)
end
let_it_be(:deployment_failed) do
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index 1e6baf30a76..e85e683b599 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -20,15 +20,6 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', :with_licens
remove_repository(project)
end
- it 'issues/new-issue.html' do
- get :new, params: {
- namespace_id: project.namespace.to_param,
- project_id: project
- }
-
- expect(response).to be_successful
- end
-
it 'issues/open-issue.html' do
render_issue(create(:issue, project: project))
end
diff --git a/spec/frontend/fixtures/job_artifacts.rb b/spec/frontend/fixtures/job_artifacts.rb
index e53cdbbaaa5..6dadd6750f1 100644
--- a/spec/frontend/fixtures/job_artifacts.rb
+++ b/spec/frontend/fixtures/job_artifacts.rb
@@ -12,7 +12,7 @@ RSpec.describe 'Job Artifacts (GraphQL fixtures)' do
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let_it_be(:user) { create(:user) }
- job_artifacts_query_path = 'artifacts/graphql/queries/get_job_artifacts.query.graphql'
+ job_artifacts_query_path = 'ci/artifacts/graphql/queries/get_job_artifacts.query.graphql'
it "graphql/#{job_artifacts_query_path}.json" do
create(:ci_build, :failed, :artifacts, :trace_artifact, pipeline: pipeline)
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index 6d452bf1bff..6c0b87c5a68 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -48,49 +48,71 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
let!(:with_artifact) { create(:ci_build, :success, name: 'with_artifact', job_artifacts: [artifact], pipeline: pipeline) }
let!(:with_coverage) { create(:ci_build, :success, name: 'with_coverage', coverage: 40.0, pipeline: pipeline) }
- fixtures_path = 'graphql/jobs/'
- get_jobs_query = 'get_jobs.query.graphql'
- full_path = 'frontend-fixtures/builds-project'
+ shared_examples 'graphql queries' do |path, jobs_query, skip_non_defaults = false|
+ let_it_be(:variables) { {} }
+ let_it_be(:success_path) { '' }
- let_it_be(:query) do
- get_graphql_query_as_string("jobs/components/table/graphql/queries/#{get_jobs_query}")
- end
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{path}/#{jobs_query}")
+ end
- it "#{fixtures_path}#{get_jobs_query}.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path
- })
+ fixtures_path = 'graphql/jobs/'
- expect_graphql_errors_to_be_empty
- end
+ it "#{fixtures_path}#{jobs_query}.json", :aggregate_failures do
+ post_graphql(query, current_user: user, variables: variables)
+
+ expect(graphql_data.dig(*success_path)).not_to be_nil
+ expect_graphql_errors_to_be_empty
+ end
+
+ context 'with non default fixtures', if: !skip_non_defaults do
+ it "#{fixtures_path}#{jobs_query}.as_guest.json" do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ post_graphql(query, current_user: guest, variables: variables)
+
+ expect_graphql_errors_to_be_empty
+ end
- it "#{fixtures_path}#{get_jobs_query}.as_guest.json" do
- guest = create(:user)
- project.add_guest(guest)
+ it "#{fixtures_path}#{jobs_query}.paginated.json" do
+ post_graphql(query, current_user: user, variables: variables.merge({ first: 2 }))
- post_graphql(query, current_user: guest, variables: {
- fullPath: full_path
- })
+ expect_graphql_errors_to_be_empty
+ end
- expect_graphql_errors_to_be_empty
+ it "#{fixtures_path}#{jobs_query}.empty.json" do
+ post_graphql(query, current_user: user, variables: variables.merge({ first: 0 }))
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
- it "#{fixtures_path}#{get_jobs_query}.paginated.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path,
- first: 2
- })
+ it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs.query.graphql' do
+ let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } }
+ let(:success_path) { %w[project jobs] }
+ end
- expect_graphql_errors_to_be_empty
+ it_behaves_like 'graphql queries', 'jobs/components/table/graphql/queries', 'get_jobs_count.query.graphql', true do
+ let(:variables) { { fullPath: 'frontend-fixtures/builds-project' } }
+ let(:success_path) { %w[project jobs] }
end
- it "#{fixtures_path}#{get_jobs_query}.empty.json" do
- post_graphql(query, current_user: user, variables: {
- fullPath: full_path,
- first: 0
- })
+ it_behaves_like 'graphql queries', 'pages/admin/jobs/components/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
+ let(:variables) { { statuses: %w[PENDING RUNNING] } }
+ let(:user) { create(:admin) }
+ let(:success_path) { %w[cancelable count] }
+ end
- expect_graphql_errors_to_be_empty
+ it_behaves_like 'graphql queries', 'pages/admin/jobs/components/table/graphql/queries', 'get_all_jobs_count.query.graphql', true do
+ let(:user) { create(:admin) }
+ let(:success_path) { 'jobs' }
end
end
end
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 7ee89ca3694..b6f6d149756 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -151,7 +151,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
context 'merge request with no approvals' do
base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
base_output_path = 'graphql/merge_requests/approvals/'
- query_name = 'approved_by.query.graphql'
+ query_name = 'approvals.query.graphql'
it "#{base_output_path}#{query_name}_no_approvals.json" do
query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
@@ -165,7 +165,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
context 'merge request approved by current user' do
base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
base_output_path = 'graphql/merge_requests/approvals/'
- query_name = 'approved_by.query.graphql'
+ query_name = 'approvals.query.graphql'
it "#{base_output_path}#{query_name}.json" do
merge_request.approved_by_users << user
@@ -181,7 +181,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
context 'merge request approved by multiple users' do
base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
base_output_path = 'graphql/merge_requests/approvals/'
- query_name = 'approved_by.query.graphql'
+ query_name = 'approvals.query.graphql'
it "#{base_output_path}#{query_name}_multiple_users.json" do
merge_request.approved_by_users << user
diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb
index 109b016d980..036ce9eea3a 100644
--- a/spec/frontend/fixtures/metrics_dashboard.rb
+++ b/spec/frontend/fixtures/metrics_dashboard.rb
@@ -17,6 +17,7 @@ RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do
end
before do
+ stub_feature_flags(remove_monitor_metrics: false)
sign_in(user)
project.add_maintainer(user)
diff --git a/spec/frontend/fixtures/milestones.rb b/spec/frontend/fixtures/milestones.rb
new file mode 100644
index 00000000000..5e39dcf190a
--- /dev/null
+++ b/spec/frontend/fixtures/milestones.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::MilestonesController, '(JavaScript fixtures)', :with_license, feature_category: :team_planning, type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
+ let_it_be(:project) { create(:project_empty_repo, namespace: namespace, path: 'milestones-project') }
+
+ render_views
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ after do
+ remove_repository(project)
+ end
+
+ it 'milestones/new-milestone.html' do
+ get :new, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ expect(response).to be_successful
+ end
+
+ private
+
+ def render_milestone(milestone)
+ get :show, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: milestone.to_param
+ }
+
+ expect(response).to be_successful
+ end
+end
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 768934d6278..24a6f6f7de6 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do
+ include ApiHelpers
+ include GraphqlHelpers
include JavaScriptFixturesHelpers
let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
@@ -56,4 +58,27 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
expect(response).to be_successful
end
+
+ describe GraphQL::Query, type: :request do
+ fixtures_path = 'graphql/pipelines/'
+ get_pipeline_actions_query = 'get_pipeline_actions.query.graphql'
+
+ let!(:pipeline_with_manual_actions) { create(:ci_pipeline, project: project, user: user) }
+ let!(:build_scheduled) { create(:ci_build, :scheduled, pipeline: pipeline_with_manual_actions, stage: 'test') }
+ let!(:build_manual) { create(:ci_build, :manual, pipeline: pipeline_with_manual_actions, stage: 'build') }
+ let!(:build_manual_cannot_play) do
+ create(:ci_build, :manual, :skipped, pipeline: pipeline_with_manual_actions, stage: 'build')
+ end
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("pipelines/graphql/queries/#{get_pipeline_actions_query}")
+ end
+
+ it "#{fixtures_path}#{get_pipeline_actions_query}.json" do
+ post_graphql(query, current_user: user,
+ variables: { fullPath: project.full_path, iid: pipeline_with_manual_actions.iid })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 2ccf2c0392f..8cd651c5b36 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -67,7 +67,7 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
end
end
- describe 'Storage', feature_category: :subscription_cost_management do
+ describe 'Storage', feature_category: :consumables_cost_management do
describe GraphQL::Query, type: :request do
include GraphqlHelpers
context 'project storage statistics query' do
diff --git a/spec/frontend/fixtures/prometheus_integration.rb b/spec/frontend/fixtures/prometheus_integration.rb
index 13130c00118..fcba8b596a8 100644
--- a/spec/frontend/fixtures/prometheus_integration.rb
+++ b/spec/frontend/fixtures/prometheus_integration.rb
@@ -14,6 +14,7 @@ RSpec.describe Projects::Settings::IntegrationsController, '(JavaScript fixtures
before do
sign_in(user)
+ stub_feature_flags(remove_monitor_metrics: false)
end
after do
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index f60e4991292..099df607487 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Runner (JavaScript fixtures)' do
+RSpec.describe 'Runner (JavaScript fixtures)', feature_category: :runner_fleet do
include AdminModeHelper
include ApiHelpers
include JavaScriptFixturesHelpers
@@ -13,7 +13,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:project_2) { create(:project, :repository, :public) }
- let_it_be(:runner) { create(:ci_runner, :instance, description: 'My Runner', version: '1.0.0') }
+ let_it_be(:runner) { create(:ci_runner, :instance, description: 'My Runner', creator: admin, version: '1.0.0') }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], version: '2.0.0') }
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], version: '2.0.0') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], version: '2.0.0') }
@@ -58,6 +58,13 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
+
+ it "#{fixtures_path}#{all_runners_query}.with_creator.json" do
+ # "last: 1" fetches the first runner created, with admin as "creator"
+ post_graphql(query, current_user: admin, variables: { last: 1 })
+
+ expect_graphql_errors_to_be_empty
+ end
end
describe 'all_runners_count.query.graphql', type: :request do
@@ -145,6 +152,43 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
end
+
+ describe 'runner_for_registration.query.graphql', :freeze_time, type: :request do
+ runner_for_registration_query = 'register/runner_for_registration.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{runner_for_registration_query}")
+ end
+
+ it "#{fixtures_path}#{runner_for_registration_query}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ describe 'runner_create.mutation.graphql', type: :request do
+ runner_create_mutation = 'new/runner_create.mutation.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{runner_create_mutation}")
+ end
+
+ context 'with runnerType set to INSTANCE_TYPE' do
+ it "#{fixtures_path}#{runner_create_mutation}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ input: {
+ runnerType: 'INSTANCE_TYPE',
+ description: 'My dummy runner'
+ }
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
end
describe 'as group owner', GraphQL::Query do
diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/saved_replies.rb
deleted file mode 100644
index c80ba06bca1..00000000000
--- a/spec/frontend/fixtures/saved_replies.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile do
- include JavaScriptFixturesHelpers
- include ApiHelpers
- include GraphqlHelpers
-
- let_it_be(:current_user) { create(:user) }
-
- before do
- sign_in(current_user)
- end
-
- context 'when user has no saved replies' do
- base_input_path = 'saved_replies/queries/'
- base_output_path = 'graphql/saved_replies/'
- query_name = 'saved_replies.query.graphql'
-
- it "#{base_output_path}saved_replies_empty.query.graphql.json" do
- query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
-
- post_graphql(query, current_user: current_user)
-
- expect_graphql_errors_to_be_empty
- end
- end
-
- context 'when user has saved replies' do
- base_input_path = 'saved_replies/queries/'
- base_output_path = 'graphql/saved_replies/'
- query_name = 'saved_replies.query.graphql'
-
- it "#{base_output_path}saved_replies.query.graphql.json" do
- create(:saved_reply, user: current_user)
- create(:saved_reply, user: current_user)
-
- query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
-
- post_graphql(query, current_user: current_user)
-
- expect_graphql_errors_to_be_empty
- end
- end
-end
diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb
index bd2d63a1827..5b09e1c9495 100644
--- a/spec/frontend/fixtures/startup_css.rb
+++ b/spec/frontend/fixtures/startup_css.rb
@@ -16,7 +16,6 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
before do
# We want vNext badge to be included and com/canary don't remove/hide any other elements.
# This is why we're turning com and canary on by default for now.
- allow(Gitlab).to receive(:com?).and_return(true)
allow(Gitlab).to receive(:canary?).and_return(true)
sign_in(user)
end
@@ -41,12 +40,12 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
expect(response).to be_successful
end
- # This Feature Flag is on by default
- # This ensures that the correct css is generated
- # When the feature flag is on, the general startup will capture it
- # This will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/339348
- it "startup_css/project-#{type}-search-ff-off.html" do
- stub_feature_flags(new_header_search: false)
+ # This Feature Flag is off by default
+ # This ensures that the correct css is generated for super sidebar
+ # When the feature flag is off, the general startup will capture it
+ it "startup_css/project-#{type}-super-sidebar.html" do
+ stub_feature_flags(super_sidebar_nav: true)
+ user.update!(use_new_navigation: true)
get :show, params: {
namespace_id: project.namespace.to_param,
@@ -57,11 +56,11 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do
end
end
- describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
+ describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do
it_behaves_like 'startup css project fixtures', 'general'
end
- describe ProjectsController, '(Startup CSS fixtures)', type: :controller do
+ describe ProjectsController, '(Startup CSS fixtures)', :saas, type: :controller do
before do
user.update!(theme_id: 11)
end
diff --git a/spec/frontend/fixtures/static/oauth_remember_me.html b/spec/frontend/fixtures/static/oauth_remember_me.html
index 0b4d482925d..60277ecf66e 100644
--- a/spec/frontend/fixtures/static/oauth_remember_me.html
+++ b/spec/frontend/fixtures/static/oauth_remember_me.html
@@ -1,5 +1,5 @@
<div id="oauth-container">
- <input id="remember_me" type="checkbox" />
+ <input id="remember_me_omniauth" type="checkbox" />
<form method="post" action="http://example.com/">
<button class="js-oauth-login twitter" type="submit">
diff --git a/spec/frontend/fixtures/static/search_autocomplete.html b/spec/frontend/fixtures/static/search_autocomplete.html
deleted file mode 100644
index 29db9020424..00000000000
--- a/spec/frontend/fixtures/static/search_autocomplete.html
+++ /dev/null
@@ -1,15 +0,0 @@
-<div class="search search-form">
-<form class="form-inline">
-<div class="search-input-container">
-<div class="search-input-wrap">
-<div class="dropdown">
-<input class="search-input dropdown-menu-toggle" id="search">
-<div class="dropdown-menu dropdown-select">
-<div class="dropdown-content"></div>
-</div>
-</div>
-</div>
-</div>
-<input class="js-search-project-options" type="hidden">
-</form>
-</div>
diff --git a/spec/frontend/fixtures/timelogs.rb b/spec/frontend/fixtures/timelogs.rb
new file mode 100644
index 00000000000..c66e2447ea6
--- /dev/null
+++ b/spec/frontend/fixtures/timelogs.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Timelogs (GraphQL fixtures)', feature_category: :team_planning do
+ describe GraphQL::Query, type: :request do
+ include ApiHelpers
+ include GraphqlHelpers
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+
+ context 'for time tracking timelogs' do
+ let_it_be(:project) { create(:project_empty_repo, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ let(:query_path) { 'time_tracking/components/queries/get_timelogs.query.graphql' }
+ let(:query) { get_graphql_query_as_string(query_path) }
+
+ before_all do
+ project.add_guest(guest)
+ project.add_developer(developer)
+ end
+
+ it "graphql/get_timelogs_empty_response.json" do
+ post_graphql(query, current_user: guest, variables: { username: guest.username })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ context 'with 20 or less timelogs' do
+ let_it_be(:timelogs) { create_list(:timelog, 6, user: developer, issue: issue, time_spent: 4 * 60 * 60) }
+
+ it "graphql/get_non_paginated_timelogs_response.json" do
+ post_graphql(query, current_user: guest, variables: { username: developer.username })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'with more than 20 timelogs' do
+ let_it_be(:timelogs) { create_list(:timelog, 30, user: developer, issue: issue, time_spent: 4 * 60 * 60) }
+
+ it "graphql/get_paginated_timelogs_response.json" do
+ post_graphql(query, current_user: guest, variables: { username: developer.username, first: 25 })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/u2f.rb b/spec/frontend/fixtures/u2f.rb
deleted file mode 100644
index 96820c9ae80..00000000000
--- a/spec/frontend/fixtures/u2f.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.context 'U2F' do
- include JavaScriptFixturesHelpers
-
- let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') }
-
- before do
- stub_feature_flags(webauthn: false)
- end
-
- describe SessionsController, '(JavaScript fixtures)', type: :controller do
- include DeviseHelpers
-
- render_views
-
- before do
- set_devise_mapping(context: @request)
- end
-
- it 'u2f/authenticate.html' do
- allow(controller).to receive(:find_user).and_return(user)
-
- post :create, params: { user: { login: user.username, password: user.password } }
-
- expect(response).to be_successful
- end
- end
-
- describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do
- render_views
-
- before do
- sign_in(user)
- allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance|
- allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
- end
- end
-
- it 'u2f/register.html' do
- get :show
-
- expect(response).to be_successful
- end
- end
-end
diff --git a/spec/frontend/fixtures/users.rb b/spec/frontend/fixtures/users.rb
new file mode 100644
index 00000000000..0e9d7475bf9
--- /dev/null
+++ b/spec/frontend/fixtures/users.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Users (GraphQL fixtures)', feature_category: :user_profile do
+ describe GraphQL::Query, type: :request do
+ include ApiHelpers
+ include GraphqlHelpers
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ context 'for user achievements' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:private_group) { create(:group, :private) }
+ let_it_be(:achievement1) { create(:achievement, namespace: group) }
+ let_it_be(:achievement2) { create(:achievement, namespace: group) }
+ let_it_be(:achievement3) { create(:achievement, namespace: group) }
+ let_it_be(:achievement_from_private_group) { create(:achievement, namespace: private_group) }
+ let_it_be(:achievement_with_avatar_and_description) do
+ create(:achievement,
+ namespace: group,
+ description: 'Description',
+ avatar: File.new(Rails.root.join('db/fixtures/development/rocket.jpg'), 'r'))
+ end
+
+ let(:user_achievements_query_path) { 'profile/components/graphql/get_user_achievements.query.graphql' }
+ let(:query) { get_graphql_query_as_string(user_achievements_query_path) }
+
+ before_all do
+ group.add_guest(user)
+ end
+
+ it "graphql/get_user_achievements_empty_response.json" do
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "graphql/get_user_achievements_with_avatar_and_description_response.json" do
+ create(:user_achievement, user: user, achievement: achievement_with_avatar_and_description)
+
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "graphql/get_user_achievements_without_avatar_or_description_response.json" do
+ create(:user_achievement, user: user, achievement: achievement1)
+
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it 'graphql/get_user_achievements_from_private_group.json' do
+ create(:user_achievement, user: user, achievement: achievement_from_private_group)
+
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
+ it "graphql/get_user_achievements_long_response.json" do
+ [achievement1, achievement2, achievement3, achievement_with_avatar_and_description].each do |achievement|
+ create(:user_achievement, user: user, achievement: achievement)
+ end
+
+ post_graphql(query, current_user: user, variables: { id: user.to_global_id })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/webauthn.rb b/spec/frontend/fixtures/webauthn.rb
index c6e9b41b584..ed6180118f0 100644
--- a/spec/frontend/fixtures/webauthn.rb
+++ b/spec/frontend/fixtures/webauthn.rb
@@ -32,6 +32,7 @@ RSpec.context 'WebAuthn' do
allow_next_instance_of(Profiles::TwoFactorAuthsController) do |instance|
allow(instance).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
end
+ stub_feature_flags(webauthn_without_totp: false)
end
it 'webauthn/register.html' do
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index e1890555de0..a8ae72eb4b3 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -33,7 +33,6 @@ describe('Frequent Items App Component', () => {
const createComponent = (props = {}) => {
const session = currentSession[TEST_NAMESPACE];
gon.api_version = session.apiVersion;
- gon.features = { fullPathProjectSearch: true };
wrapper = mountExtended(App, {
store,
@@ -69,7 +68,6 @@ describe('Frequent Items App Component', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('default', () => {
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index c54a2a1d039..7c8592fdf0c 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -59,8 +59,6 @@ describe('FrequentItemsListItemComponent', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
- wrapper = null;
});
describe('computed', () => {
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
index d024925f62b..dd6dd80af4f 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
@@ -29,10 +29,6 @@ describe('FrequentItemsListComponent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('isListEmpty', () => {
it('should return `true` or `false` representing whether if `items` is empty or not with projects', async () => {
@@ -52,7 +48,7 @@ describe('FrequentItemsListComponent', () => {
});
describe('fetched item messages', () => {
- it('should show default empty list message', async () => {
+ it('should show default empty list message', () => {
createComponent({
items: [],
});
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index c228bca4973..2feb488da2c 100644
--- a/spec/frontend/frequent_items/store/actions_spec.js
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -25,7 +25,6 @@ describe('Frequent Items Dropdown Store Actions', () => {
mockedState.namespace = mockNamespace;
mockedState.storageKey = mockStorageKey;
- gon.features = { fullPathProjectSearch: true };
});
afterEach(() => {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index e4fd8649263..73284fbe5e5 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -666,10 +666,11 @@ describe('GfmAutoComplete', () => {
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
- availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>',
+ availabilityStatus:
+ '<span class="badge badge-warning badge-pill gl-badge sm gl-ml-2">Busy</span>',
}),
).toBe(
- '<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>',
+ '<li>IMG my-group <small><span class="badge badge-warning badge-pill gl-badge sm gl-ml-2">Busy</span></small> <i class="icon"/></li>',
);
});
diff --git a/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js b/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
index 685b5144a95..289702a4263 100644
--- a/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
+++ b/spec/frontend/gitlab_pages/new/pages/pages_pipeline_wizard_spec.js
@@ -7,7 +7,7 @@ import PagesPipelineWizard, { i18n } from '~/gitlab_pages/components/pages_pipel
import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
import pagesTemplate from '~/pipeline_wizard/templates/pages.yml';
import pagesMarkOnboardingComplete from '~/gitlab_pages/queries/mark_onboarding_complete.graphql';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
Vue.use(VueApollo);
@@ -50,10 +50,6 @@ describe('PagesPipelineWizard', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows the pipeline wizard', () => {
expect(findPipelineWizardWrapper().exists()).toBe(true);
});
@@ -96,7 +92,7 @@ describe('PagesPipelineWizard', () => {
await waitForPromises();
- expect(redirectTo).toHaveBeenCalledWith(props.redirectToWhenDone);
+ expect(redirectTo).toHaveBeenCalledWith(props.redirectToWhenDone); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
index 949bcf71ff5..e87f7e950cd 100644
--- a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
+++ b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js
@@ -25,7 +25,6 @@ describe('GitlabVersionCheckBadge', () => {
afterEach(() => {
unmockTracking();
- wrapper.destroy();
});
const findGlBadgeClickWrapper = () => wrapper.findByTestId('badge-click-wrapper');
diff --git a/spec/frontend/gl_field_errors_spec.js b/spec/frontend/gl_field_errors_spec.js
index 1f6929baa75..91d0674dfcb 100644
--- a/spec/frontend/gl_field_errors_spec.js
+++ b/spec/frontend/gl_field_errors_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlGlFieldErrors from 'test_fixtures_static/gl_field_errors.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GlFieldErrors from '~/gl_field_errors';
describe('GL Style Field Errors', () => {
@@ -10,7 +11,7 @@ describe('GL Style Field Errors', () => {
});
beforeEach(() => {
- loadHTMLFixture('static/gl_field_errors.html');
+ setHTMLFixture(htmlGlFieldErrors);
const $form = $('form.gl-show-field-errors');
testContext.$form = $form;
diff --git a/spec/frontend/google_cloud/aiml/panel_spec.js b/spec/frontend/google_cloud/aiml/panel_spec.js
new file mode 100644
index 00000000000..374e125c509
--- /dev/null
+++ b/spec/frontend/google_cloud/aiml/panel_spec.js
@@ -0,0 +1,43 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Panel from '~/google_cloud/aiml/panel.vue';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
+import ServiceTable from '~/google_cloud/aiml/service_table.vue';
+
+describe('google_cloud/databases/panel', () => {
+ let wrapper;
+
+ const props = {
+ configurationUrl: 'configuration-url',
+ deploymentsUrl: 'deployments-url',
+ databasesUrl: 'databases-url',
+ aimlUrl: 'aiml-url',
+ visionAiUrl: 'vision-ai-url',
+ translationAiUrl: 'translation-ai-url',
+ languageAiUrl: 'language-ai-url',
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(Panel, { propsData: props });
+ });
+
+ it('contains incubation banner', () => {
+ const target = wrapper.findComponent(IncubationBanner);
+ expect(target.exists()).toBe(true);
+ });
+
+ it('contains google cloud menu with `aiml` active', () => {
+ const target = wrapper.findComponent(GoogleCloudMenu);
+ expect(target.exists()).toBe(true);
+ expect(target.props('active')).toBe('aiml');
+ expect(target.props('configurationUrl')).toBe(props.configurationUrl);
+ expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
+ expect(target.props('databasesUrl')).toBe(props.databasesUrl);
+ expect(target.props('aimlUrl')).toBe(props.aimlUrl);
+ });
+
+ it('contains service table', () => {
+ const target = wrapper.findComponent(ServiceTable);
+ expect(target.exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/google_cloud/aiml/service_table_spec.js b/spec/frontend/google_cloud/aiml/service_table_spec.js
new file mode 100644
index 00000000000..b14db064a7f
--- /dev/null
+++ b/spec/frontend/google_cloud/aiml/service_table_spec.js
@@ -0,0 +1,34 @@
+import { GlTable } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ServiceTable from '~/google_cloud/aiml/service_table.vue';
+
+describe('google_cloud/aiml/service_table', () => {
+ let wrapper;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+
+ beforeEach(() => {
+ const propsData = {
+ visionAiUrl: '#url-vision-ai',
+ languageAiUrl: '#url-language-ai',
+ translationAiUrl: '#url-translate-ai',
+ };
+ wrapper = mountExtended(ServiceTable, { propsData });
+ });
+
+ it('should contain a table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it.each`
+ name | testId | url
+ ${'key-vision-ai'} | ${'button-vision-ai'} | ${'#url-vision-ai'}
+ ${'key-natural-language-ai'} | ${'button-natural-language-ai'} | ${'#url-language-ai'}
+ ${'key-translation-ai'} | ${'button-translation-ai'} | ${'#url-translate-ai'}
+ `('renders $name button with correct url', ({ testId, url }) => {
+ const button = wrapper.findByTestId(testId);
+
+ expect(button.exists()).toBe(true);
+ expect(button.attributes('href')).toBe(url);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
index 4809ea37045..f1ee96ff870 100644
--- a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
+++ b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
@@ -9,16 +9,13 @@ describe('google_cloud/components/google_cloud_menu', () => {
configurationUrl: 'configuration-url',
deploymentsUrl: 'deployments-url',
databasesUrl: 'databases-url',
+ aimlUrl: 'aiml-url',
};
beforeEach(() => {
wrapper = mountExtended(GoogleCloudMenu, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains active configuration link', () => {
const link = wrapper.findByTestId('configurationLink');
expect(link.text()).toBe(GoogleCloudMenu.i18n.configuration.title);
@@ -37,4 +34,10 @@ describe('google_cloud/components/google_cloud_menu', () => {
expect(link.text()).toBe(GoogleCloudMenu.i18n.databases.title);
expect(link.attributes('href')).toBe(props.databasesUrl);
});
+
+ it('contains ai/ml link', () => {
+ const link = wrapper.findByTestId('aimlLink');
+ expect(link.text()).toBe(GoogleCloudMenu.i18n.aiml.title);
+ expect(link.attributes('href')).toBe(props.aimlUrl);
+ });
});
diff --git a/spec/frontend/google_cloud/components/incubation_banner_spec.js b/spec/frontend/google_cloud/components/incubation_banner_spec.js
index 09a4d92dca2..92bc39bdff9 100644
--- a/spec/frontend/google_cloud/components/incubation_banner_spec.js
+++ b/spec/frontend/google_cloud/components/incubation_banner_spec.js
@@ -15,10 +15,6 @@ describe('google_cloud/components/incubation_banner', () => {
wrapper = mount(IncubationBanner);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains alert', () => {
expect(findAlert().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/components/revoke_oauth_spec.js b/spec/frontend/google_cloud/components/revoke_oauth_spec.js
index faaec07fc35..2b39bb9ca74 100644
--- a/spec/frontend/google_cloud/components/revoke_oauth_spec.js
+++ b/spec/frontend/google_cloud/components/revoke_oauth_spec.js
@@ -20,10 +20,6 @@ describe('google_cloud/components/revoke_oauth', () => {
wrapper = shallowMount(RevokeOauth, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains title', () => {
const title = findTitle();
expect(title.text()).toContain('Revoke authorizations');
diff --git a/spec/frontend/google_cloud/configuration/panel_spec.js b/spec/frontend/google_cloud/configuration/panel_spec.js
index 79eb4cb4918..dd85b4c90a7 100644
--- a/spec/frontend/google_cloud/configuration/panel_spec.js
+++ b/spec/frontend/google_cloud/configuration/panel_spec.js
@@ -25,10 +25,6 @@ describe('google_cloud/configuration/panel', () => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
diff --git a/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js
index 48e4b0ca1ad..6e2d3147a54 100644
--- a/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js
+++ b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js
@@ -25,10 +25,6 @@ describe('google_cloud/databases/cloudsql/create_instance_form', () => {
wrapper = shallowMountExtended(InstanceForm, { propsData, stubs: { GlFormCheckbox } });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js
index a5736d0a524..a2ee75f9fbf 100644
--- a/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js
+++ b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js
@@ -8,10 +8,6 @@ describe('google_cloud/databases/cloudsql/instance_table', () => {
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTable = () => wrapper.findComponent(GlTable);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there are no instances', () => {
beforeEach(() => {
const propsData = {
diff --git a/spec/frontend/google_cloud/databases/panel_spec.js b/spec/frontend/google_cloud/databases/panel_spec.js
index e6a0d74f348..779258bbdbb 100644
--- a/spec/frontend/google_cloud/databases/panel_spec.js
+++ b/spec/frontend/google_cloud/databases/panel_spec.js
@@ -23,10 +23,6 @@ describe('google_cloud/databases/panel', () => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
diff --git a/spec/frontend/google_cloud/databases/service_table_spec.js b/spec/frontend/google_cloud/databases/service_table_spec.js
index 4a622e544e1..4594e1758ad 100644
--- a/spec/frontend/google_cloud/databases/service_table_spec.js
+++ b/spec/frontend/google_cloud/databases/service_table_spec.js
@@ -19,10 +19,6 @@ describe('google_cloud/databases/service_table', () => {
wrapper = mountExtended(ServiceTable, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should contain a table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/deployments/panel_spec.js b/spec/frontend/google_cloud/deployments/panel_spec.js
index 729db1707a7..0748d8f9377 100644
--- a/spec/frontend/google_cloud/deployments/panel_spec.js
+++ b/spec/frontend/google_cloud/deployments/panel_spec.js
@@ -19,10 +19,6 @@ describe('google_cloud/deployments/panel', () => {
wrapper = shallowMountExtended(Panel, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains incubation banner', () => {
const target = wrapper.findComponent(IncubationBanner);
expect(target.exists()).toBe(true);
diff --git a/spec/frontend/google_cloud/deployments/service_table_spec.js b/spec/frontend/google_cloud/deployments/service_table_spec.js
index 8faad64e313..49220a6007e 100644
--- a/spec/frontend/google_cloud/deployments/service_table_spec.js
+++ b/spec/frontend/google_cloud/deployments/service_table_spec.js
@@ -18,10 +18,6 @@ describe('google_cloud/deployments/service_table', () => {
wrapper = mount(DeploymentsServiceTable, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should contain a table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/gcp_regions/form_spec.js b/spec/frontend/google_cloud/gcp_regions/form_spec.js
index 1030e9c8a18..be37ff092f0 100644
--- a/spec/frontend/google_cloud/gcp_regions/form_spec.js
+++ b/spec/frontend/google_cloud/gcp_regions/form_spec.js
@@ -16,10 +16,6 @@ describe('google_cloud/gcp_regions/form', () => {
wrapper = shallowMount(GcpRegionsForm, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/gcp_regions/list_spec.js b/spec/frontend/google_cloud/gcp_regions/list_spec.js
index 6d8c389e5a1..74a54b93183 100644
--- a/spec/frontend/google_cloud/gcp_regions/list_spec.js
+++ b/spec/frontend/google_cloud/gcp_regions/list_spec.js
@@ -18,10 +18,6 @@ describe('google_cloud/gcp_regions/list', () => {
wrapper = mount(GcpRegionsList, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows the empty state component', () => {
expect(findEmptyState().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/service_accounts/form_spec.js b/spec/frontend/google_cloud/service_accounts/form_spec.js
index 8be481774fa..c86c8876b15 100644
--- a/spec/frontend/google_cloud/service_accounts/form_spec.js
+++ b/spec/frontend/google_cloud/service_accounts/form_spec.js
@@ -17,10 +17,6 @@ describe('google_cloud/service_accounts/form', () => {
wrapper = shallowMount(ServiceAccountsForm, { propsData, stubs: { GlFormCheckbox } });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains header', () => {
expect(findHeader().exists()).toBe(true);
});
diff --git a/spec/frontend/google_cloud/service_accounts/list_spec.js b/spec/frontend/google_cloud/service_accounts/list_spec.js
index c2bd2005b5d..ae5776081d7 100644
--- a/spec/frontend/google_cloud/service_accounts/list_spec.js
+++ b/spec/frontend/google_cloud/service_accounts/list_spec.js
@@ -18,10 +18,6 @@ describe('google_cloud/service_accounts/list', () => {
wrapper = mount(ServiceAccountsList, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows the empty state component', () => {
expect(findEmptyState().exists()).toBe(true);
});
diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js
index ec9e1ef8e5f..dd8e886e6bc 100644
--- a/spec/frontend/google_tag_manager/index_spec.js
+++ b/spec/frontend/google_tag_manager/index_spec.js
@@ -6,7 +6,6 @@ import {
trackProjectImport,
trackNewRegistrations,
trackSaasTrialSubmit,
- trackSaasTrialSkip,
trackSaasTrialGroup,
trackSaasTrialGetStarted,
trackTrialAcceptTerms,
@@ -143,9 +142,6 @@ describe('~/google_tag_manager/index', () => {
describe.each([
createOmniAuthTestCase(trackFreeTrialAccountSubmissions, 'freeThirtyDayTrial'),
createOmniAuthTestCase(trackNewRegistrations, 'standardSignUp'),
- createTestCase(trackSaasTrialSkip, {
- links: [{ cls: 'js-skip-trial', expectation: { event: 'saasTrialSkip' } }],
- }),
createTestCase(trackSaasTrialGroup, {
forms: [{ cls: 'js-saas-trial-group', expectation: { event: 'saasTrialGroup' } }],
}),
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
index 021a3aa41ed..540fc597aa9 100644
--- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
import { createStore } from '~/grafana_integration/store';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('grafana integration component', () => {
let wrapper;
@@ -28,11 +28,8 @@ describe('grafana integration component', () => {
});
afterEach(() => {
- if (wrapper.destroy) {
- wrapper.destroy();
- createAlert.mockReset();
- refreshCurrentPage.mockReset();
- }
+ createAlert.mockReset();
+ refreshCurrentPage.mockReset();
});
describe('default state', () => {
@@ -103,7 +100,7 @@ describe('grafana integration component', () => {
expect(refreshCurrentPage).toHaveBeenCalled();
});
- it('creates flash banner on error', async () => {
+ it('creates alert banner on error', async () => {
const message = 'mockErrorMessage';
axios.patch.mockRejectedValue({ response: { data: { message } } });
diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js
index cd334ef0d97..f03856e5f75 100644
--- a/spec/frontend/graphql_shared/utils_spec.js
+++ b/spec/frontend/graphql_shared/utils_spec.js
@@ -1,11 +1,16 @@
+import Visibility from 'visibilityjs';
+
import {
isGid,
getIdFromGraphQLId,
+ getTypeFromGraphQLId,
convertToGraphQLId,
convertToGraphQLIds,
convertFromGraphQLIds,
convertNodeIdsFromGraphQLIds,
getNodesOrDefault,
+ toggleQueryPollingByVisibility,
+ etagQueryHeaders,
} from '~/graphql_shared/utils';
const mockType = 'Group';
@@ -22,52 +27,30 @@ describe('isGid', () => {
});
});
-describe('getIdFromGraphQLId', () => {
- [
- {
- input: '',
- output: null,
- },
- {
- input: null,
- output: null,
- },
- {
- input: 2,
- output: 2,
- },
- {
- input: 'gid://',
- output: null,
- },
- {
- input: 'gid://gitlab/',
- output: null,
- },
- {
- input: 'gid://gitlab/Environments',
- output: null,
- },
- {
- input: 'gid://gitlab/Environments/',
- output: null,
- },
- {
- input: 'gid://gitlab/Environments/0',
- output: 0,
- },
- {
- input: 'gid://gitlab/Environments/123',
- output: 123,
- },
- {
- input: 'gid://gitlab/DesignManagement::Version/2',
- output: 2,
- },
- ].forEach(({ input, output }) => {
- it(`getIdFromGraphQLId returns ${output} when passed ${input}`, () => {
- expect(getIdFromGraphQLId(input)).toBe(output);
- });
+describe.each`
+ input | id | type
+ ${''} | ${null} | ${null}
+ ${null} | ${null} | ${null}
+ ${0} | ${0} | ${null}
+ ${'0'} | ${0} | ${null}
+ ${2} | ${2} | ${null}
+ ${'2'} | ${2} | ${null}
+ ${'gid://'} | ${null} | ${null}
+ ${'gid://gitlab'} | ${null} | ${null}
+ ${'gid://gitlab/'} | ${null} | ${null}
+ ${'gid://gitlab/Environments'} | ${null} | ${'Environments'}
+ ${'gid://gitlab/Environments/'} | ${null} | ${'Environments'}
+ ${'gid://gitlab/Environments/0'} | ${0} | ${'Environments'}
+ ${'gid://gitlab/Environments/123'} | ${123} | ${'Environments'}
+ ${'gid://gitlab/Environments/123/test'} | ${123} | ${'Environments'}
+ ${'gid://gitlab/DesignManagement::Version/123'} | ${123} | ${'DesignManagement::Version'}
+`('parses GraphQL ID `$input`', ({ input, id, type }) => {
+ it(`getIdFromGraphQLId returns ${id}`, () => {
+ expect(getIdFromGraphQLId(input)).toBe(id);
+ });
+
+ it(`getTypeFromGraphQLId returns ${type}`, () => {
+ expect(getTypeFromGraphQLId(input)).toBe(type);
});
});
@@ -160,3 +143,52 @@ describe('getNodesOrDefault', () => {
expect(result).toEqual(expected);
});
});
+
+describe('toggleQueryPollingByVisibility', () => {
+ let query;
+ let changeFn;
+ let interval;
+ let hidden;
+
+ beforeEach(() => {
+ hidden = jest.spyOn(Visibility, 'hidden').mockReturnValue(true);
+ jest.spyOn(Visibility, 'change').mockImplementation((fn) => {
+ changeFn = fn;
+ });
+
+ query = { startPolling: jest.fn(), stopPolling: jest.fn() };
+ interval = 5000;
+
+ toggleQueryPollingByVisibility(query, 5000);
+ });
+
+ it('starts polling not hidden', () => {
+ hidden.mockReturnValue(false);
+
+ changeFn();
+ expect(query.startPolling).toHaveBeenCalledWith(interval);
+ });
+
+ it('stops polling when hidden', () => {
+ query.stopPolling.mockReset();
+ hidden.mockReturnValue(true);
+
+ changeFn();
+ expect(query.stopPolling).toHaveBeenCalled();
+ });
+});
+
+describe('etagQueryHeaders', () => {
+ it('returns headers necessary for etag caching', () => {
+ expect(etagQueryHeaders('myFeature', 'myResource')).toEqual({
+ fetchOptions: {
+ method: 'GET',
+ },
+ headers: {
+ 'X-GITLAB-GRAPHQL-FEATURE-CORRELATION': 'myFeature',
+ 'X-GITLAB-GRAPHQL-RESOURCE-ETAG': 'myResource',
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
+ });
+ });
+});
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 85475c749b0..5daa21fd618 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -2,12 +2,17 @@ import { GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { updateGroup } from '~/api/groups_api';
+import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue';
+import { I18N_CONFIRM_MESSAGE } from '~/group_settings/constants';
+
jest.mock('~/api/groups_api');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
const GROUP_ID = '99';
+const GROUP_NAME = 'My group';
const RUNNER_ENABLED_VALUE = 'enabled';
const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable';
const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_and_overridable';
@@ -19,6 +24,8 @@ describe('group_settings/components/shared_runners_form', () => {
wrapper = shallowMountExtended(SharedRunnersForm, {
provide: {
groupId: GROUP_ID,
+ groupName: GROUP_NAME,
+ groupIsEmpty: false,
sharedRunnersSetting: RUNNER_ENABLED_VALUE,
parentSharedRunnersSetting: null,
runnerEnabledValue: RUNNER_ENABLED_VALUE,
@@ -41,13 +48,12 @@ describe('group_settings/components/shared_runners_form', () => {
};
beforeEach(() => {
+ confirmAction.mockResolvedValue(true);
updateGroup.mockResolvedValue({});
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
+ confirmAction.mockReset();
updateGroup.mockReset();
});
@@ -113,8 +119,9 @@ describe('group_settings/components/shared_runners_form', () => {
it('does not update settings while loading', async () => {
findSharedRunnersToggle().vm.$emit('change', true);
+ await nextTick();
findSharedRunnersToggle().vm.$emit('change', false);
- await waitForPromises();
+ await nextTick();
expect(updateGroup).toHaveBeenCalledTimes(1);
});
@@ -137,6 +144,8 @@ describe('group_settings/components/shared_runners_form', () => {
findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
+ expect(confirmAction).not.toHaveBeenCalled();
+
expect(getSharedRunnersSetting()).toEqual(RUNNER_ENABLED_VALUE);
expect(findOverrideToggle().props('disabled')).toBe(true);
});
@@ -145,17 +154,59 @@ describe('group_settings/components/shared_runners_form', () => {
findSharedRunnersToggle().vm.$emit('change', false);
await waitForPromises();
+ expect(confirmAction).toHaveBeenCalledTimes(1);
+ expect(confirmAction).toHaveBeenCalledWith(
+ I18N_CONFIRM_MESSAGE,
+ expect.objectContaining({
+ title: expect.stringContaining(GROUP_NAME),
+ }),
+ );
+
expect(getSharedRunnersSetting()).toEqual(RUNNER_DISABLED_VALUE);
expect(findOverrideToggle().props('disabled')).toBe(false);
});
+
+ describe('when user cancels operation', () => {
+ beforeEach(() => {
+ confirmAction.mockResolvedValue(false);
+ });
+
+ it('sends no payload when turned off', async () => {
+ findSharedRunnersToggle().vm.$emit('change', false);
+ await waitForPromises();
+
+ expect(confirmAction).toHaveBeenCalledTimes(1);
+ expect(confirmAction).toHaveBeenCalledWith(
+ I18N_CONFIRM_MESSAGE,
+ expect.objectContaining({
+ title: expect.stringContaining(GROUP_NAME),
+ }),
+ );
+
+ expect(updateGroup).not.toHaveBeenCalled();
+ expect(findOverrideToggle().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('when group is empty', () => {
+ beforeEach(() => {
+ createComponent({ groupIsEmpty: true });
+ });
+
+ it('confirmation is not shown when turned off', async () => {
+ findSharedRunnersToggle().vm.$emit('change', false);
+ await waitForPromises();
+
+ expect(confirmAction).not.toHaveBeenCalled();
+ expect(getSharedRunnersSetting()).toEqual(RUNNER_DISABLED_VALUE);
+ });
+ });
});
describe('"Override the group setting" toggle', () => {
- beforeEach(() => {
+ it('enabling the override toggle sends correct payload', async () => {
createComponent({ sharedRunnersSetting: RUNNER_ALLOW_OVERRIDE_VALUE });
- });
- it('enabling the override toggle sends correct payload', async () => {
findOverrideToggle().vm.$emit('change', true);
await waitForPromises();
@@ -163,6 +214,8 @@ describe('group_settings/components/shared_runners_form', () => {
});
it('disabling the override toggle sends correct payload', async () => {
+ createComponent({ sharedRunnersSetting: RUNNER_ALLOW_OVERRIDE_VALUE });
+
findOverrideToggle().vm.$emit('change', false);
await waitForPromises();
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 4e6ddd89a55..7b42e50fee5 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -3,7 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
@@ -34,7 +34,7 @@ import {
const $toast = {
show: jest.fn(),
};
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('AppComponent', () => {
let wrapper;
@@ -65,11 +65,6 @@ describe('AppComponent', () => {
vm = wrapper.vm;
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
beforeEach(async () => {
mock = new AxiosMockAdapter(axios);
mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockGroups);
@@ -117,7 +112,7 @@ describe('AppComponent', () => {
});
});
- it('should show flash error when request fails', () => {
+ it('should show an alert when request fails', () => {
mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
@@ -325,7 +320,7 @@ describe('AppComponent', () => {
});
});
- it('should show error flash message if request failed to leave group', () => {
+ it('should show error alert if request failed to leave group', () => {
const message = 'An error occurred. Please try again.';
jest
.spyOn(vm.service, 'leaveGroup')
@@ -342,7 +337,7 @@ describe('AppComponent', () => {
});
});
- it('should show appropriate error flash message if request forbids to leave group', () => {
+ it('shows appropriate error alert if request forbids to leave group', () => {
const message = 'Failed to leave the group. Please make sure you are not the only owner.';
jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: HTTP_STATUS_FORBIDDEN });
jest.spyOn(vm.store, 'removeGroup');
diff --git a/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
index be61ffa92b4..bb3c0bc1526 100644
--- a/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/archived_projects_empty_state_spec.js
@@ -6,7 +6,7 @@ import ArchivedProjectsEmptyState from '~/groups/components/empty_states/archive
let wrapper;
const defaultProvide = {
- newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+ emptyProjectsIllustration: '/assets/llustrations/empty-state/empty-projects-md.svg',
};
const createComponent = () => {
@@ -21,7 +21,7 @@ describe('ArchivedProjectsEmptyState', () => {
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
title: ArchivedProjectsEmptyState.i18n.title,
- svgPath: defaultProvide.newProjectIllustration,
+ svgPath: defaultProvide.emptyProjectsIllustration,
});
});
});
diff --git a/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
index c4ace1be1f3..8ba1c480d5e 100644
--- a/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/shared_projects_empty_state_spec.js
@@ -6,7 +6,7 @@ import SharedProjectsEmptyState from '~/groups/components/empty_states/shared_pr
let wrapper;
const defaultProvide = {
- newProjectIllustration: '/assets/illustrations/project-create-new-sm.svg',
+ emptyProjectsIllustration: '/assets/illustrations/empty-state/empty-projects-md.svg',
};
const createComponent = () => {
@@ -21,7 +21,7 @@ describe('SharedProjectsEmptyState', () => {
expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({
title: SharedProjectsEmptyState.i18n.title,
- svgPath: defaultProvide.newProjectIllustration,
+ svgPath: defaultProvide.emptyProjectsIllustration,
});
});
});
diff --git a/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
index 75edc602fbf..5ae4d0be7d6 100644
--- a/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
+++ b/spec/frontend/groups/components/empty_states/subgroups_and_projects_empty_state_spec.js
@@ -10,6 +10,7 @@ const defaultProvide = {
newProjectPath: '/projects/new?namespace_id=231',
newSubgroupIllustration: '/assets/illustrations/group-new.svg',
newSubgroupPath: '/groups/new?parent_id=231',
+ emptyProjectsIllustration: '/assets/illustrations/empty-state/empty-projects-md.svg',
emptySubgroupIllustration: '/assets/illustrations/empty-state/empty-subgroup-md.svg',
canCreateSubgroups: true,
canCreateProjects: true,
@@ -24,10 +25,6 @@ const createComponent = ({ provide = {} } = {}) => {
});
};
-afterEach(() => {
- wrapper.destroy();
-});
-
const findNewSubgroupLink = () =>
wrapper.findByRole('link', {
name: new RegExp(SubgroupsAndProjectsEmptyState.i18n.withLinks.subgroup.title),
diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js
index f223333360d..da31fb02f69 100644
--- a/spec/frontend/groups/components/group_folder_spec.js
+++ b/spec/frontend/groups/components/group_folder_spec.js
@@ -20,10 +20,6 @@ describe('GroupFolder component', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not render more children stats link when children count of group is under limit', () => {
wrapper = createComponent();
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 4570aa33a6c..663dd341a58 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -37,10 +37,6 @@ describe('GroupItemComponent', () => {
return waitForPromises();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const withMicrodata = (group) => ({
...group,
microdata: getGroupItemMicrodata(group),
diff --git a/spec/frontend/groups/components/group_name_and_path_spec.js b/spec/frontend/groups/components/group_name_and_path_spec.js
index 9965b608f27..0a18e657c94 100644
--- a/spec/frontend/groups/components/group_name_and_path_spec.js
+++ b/spec/frontend/groups/components/group_name_and_path_spec.js
@@ -7,11 +7,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import GroupNameAndPath from '~/groups/components/group_name_and_path.vue';
import { getGroupPathAvailability } from '~/rest_api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { helpPagePath } from '~/helpers/help_page_helper';
import searchGroupsWhereUserCanCreateSubgroups from '~/groups/queries/search_groups_where_user_can_create_subgroups.query.graphql';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/rest_api', () => ({
getGroupPathAvailability: jest.fn(),
}));
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index cae29a8f15a..c04eaa501ba 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -32,15 +32,11 @@ describe('GroupsComponent', () => {
const findPaginationLinks = () => wrapper.findComponent(PaginationLinks);
- beforeEach(async () => {
+ beforeEach(() => {
Vue.component('GroupFolder', GroupFolderComponent);
Vue.component('GroupItem', GroupItemComponent);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('change', () => {
it('should emit `fetchPage` event when page is changed via pagination', () => {
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index 4a385cb00ee..c4bc35dcd57 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -42,8 +42,6 @@ describe('InviteMembersBanner', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
unmockTracking();
});
@@ -59,7 +57,6 @@ describe('InviteMembersBanner', () => {
});
const trackCategory = undefined;
- const buttonClickEvent = 'invite_members_banner_button_clicked';
it('sends the displayEvent when the banner is displayed', () => {
const displayEvent = 'invite_members_banner_displayed';
@@ -80,12 +77,6 @@ describe('InviteMembersBanner', () => {
source: 'invite_members_banner',
});
});
-
- it('sends the buttonClickEvent with correct trackCategory and trackLabel', () => {
- expect(trackingSpy).toHaveBeenCalledWith(trackCategory, buttonClickEvent, {
- label: provide.trackLabel,
- });
- });
});
it('sends the dismissEvent when the banner is dismissed', () => {
diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js
index 3ceb038dd3c..fac6fb77709 100644
--- a/spec/frontend/groups/components/item_actions_spec.js
+++ b/spec/frontend/groups/components/item_actions_spec.js
@@ -18,11 +18,6 @@ describe('ItemActions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findEditGroupBtn = () => wrapper.findByTestId(`edit-group-${mockParentGroupItem.id}-btn`);
const findLeaveGroupBtn = () => wrapper.findByTestId(`leave-group-${mockParentGroupItem.id}-btn`);
const findRemoveGroupBtn = () =>
diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js
index 2333f04bb2e..ff273fcf6da 100644
--- a/spec/frontend/groups/components/item_caret_spec.js
+++ b/spec/frontend/groups/components/item_caret_spec.js
@@ -15,13 +15,6 @@ describe('ItemCaret', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findAllGlIcons = () => wrapper.findAllComponents(GlIcon);
const findGlIcon = () => wrapper.findComponent(GlIcon);
diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js
index 0c2912adc66..b98e60bed63 100644
--- a/spec/frontend/groups/components/item_stats_spec.js
+++ b/spec/frontend/groups/components/item_stats_spec.js
@@ -17,13 +17,6 @@ describe('ItemStats', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findItemStatsValue = () => wrapper.findComponent(ItemStatsValue);
describe('template', () => {
diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js
index b9db83c7dd7..e110004dbac 100644
--- a/spec/frontend/groups/components/item_stats_value_spec.js
+++ b/spec/frontend/groups/components/item_stats_value_spec.js
@@ -18,13 +18,6 @@ describe('ItemStatsValue', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findGlIcon = () => wrapper.findComponent(GlIcon);
const findStatValue = () => wrapper.find('[data-testid="itemStatValue"]');
diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js
index aa00e82150b..c269dc98a45 100644
--- a/spec/frontend/groups/components/item_type_icon_spec.js
+++ b/spec/frontend/groups/components/item_type_icon_spec.js
@@ -16,13 +16,6 @@ describe('ItemTypeIcon', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findGlIcon = () => wrapper.findComponent(GlIcon);
describe('template', () => {
diff --git a/spec/frontend/groups/components/new_top_level_group_alert_spec.js b/spec/frontend/groups/components/new_top_level_group_alert_spec.js
index db9a5c7b16b..060663747e4 100644
--- a/spec/frontend/groups/components/new_top_level_group_alert_spec.js
+++ b/spec/frontend/groups/components/new_top_level_group_alert_spec.js
@@ -30,10 +30,6 @@ describe('NewTopLevelGroupAlert', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the component is created', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index d1ae2c4be17..101dd06d578 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -39,6 +39,7 @@ describe('OverviewTabs', () => {
newProjectPath: 'projects/new',
newSubgroupIllustration: '',
newProjectIllustration: '',
+ emptyProjectsIllustration: '',
emptySubgroupIllustration: '',
canCreateSubgroups: false,
canCreateProjects: false,
@@ -76,7 +77,6 @@ describe('OverviewTabs', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
diff --git a/spec/frontend/groups/components/transfer_group_form_spec.js b/spec/frontend/groups/components/transfer_group_form_spec.js
index 0065820f78f..4d4de1ae3d5 100644
--- a/spec/frontend/groups/components/transfer_group_form_spec.js
+++ b/spec/frontend/groups/components/transfer_group_form_spec.js
@@ -48,10 +48,6 @@ describe('Transfer group form', () => {
const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findHiddenInput = () => wrapper.find('[name="new_parent_group_id"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -73,7 +69,7 @@ describe('Transfer group form', () => {
expect(findHiddenInput().attributes('value')).toBeUndefined();
});
- it('does not render the alert message', () => {
+ it('does not render the alert', () => {
expect(findAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/groups/settings/components/group_settings_readme_spec.js b/spec/frontend/groups/settings/components/group_settings_readme_spec.js
new file mode 100644
index 00000000000..8d4da73934f
--- /dev/null
+++ b/spec/frontend/groups/settings/components/group_settings_readme_spec.js
@@ -0,0 +1,112 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GroupSettingsReadme from '~/groups/settings/components/group_settings_readme.vue';
+import { GITLAB_README_PROJECT } from '~/groups/settings/constants';
+import {
+ MOCK_GROUP_PATH,
+ MOCK_GROUP_ID,
+ MOCK_PATH_TO_GROUP_README,
+ MOCK_PATH_TO_README_PROJECT,
+} from '../mock_data';
+
+describe('GroupSettingsReadme', () => {
+ let wrapper;
+
+ const defaultProps = {
+ groupPath: MOCK_GROUP_PATH,
+ groupId: MOCK_GROUP_ID,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(GroupSettingsReadme, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlModal,
+ GlSprintf,
+ },
+ });
+ };
+
+ const findHasReadmeButtonLink = () => wrapper.findByText('README');
+ const findAddReadmeButton = () => wrapper.findByTestId('group-settings-add-readme-button');
+ const findModalBody = () => wrapper.findByTestId('group-settings-modal-readme-body');
+ const findModalCreateReadmeButton = () =>
+ wrapper.findByTestId('group-settings-modal-create-readme-button');
+
+ describe('Group has existing README', () => {
+ beforeEach(() => {
+ createComponent({
+ groupReadmePath: MOCK_PATH_TO_GROUP_README,
+ readmeProjectPath: MOCK_PATH_TO_README_PROJECT,
+ });
+ });
+
+ describe('template', () => {
+ it('renders README Button Link with correct path and text', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(true);
+ expect(findHasReadmeButtonLink().attributes('href')).toBe(MOCK_PATH_TO_GROUP_README);
+ });
+
+ it('does not render Add README Button', () => {
+ expect(findAddReadmeButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Group has README project without README file', () => {
+ beforeEach(() => {
+ createComponent({ readmeProjectPath: MOCK_PATH_TO_README_PROJECT });
+ });
+
+ describe('template', () => {
+ it('does not render README', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(false);
+ });
+
+ it('does render Add Readme Button with correct text', () => {
+ expect(findAddReadmeButton().exists()).toBe(true);
+ expect(findAddReadmeButton().text()).toBe('Add README');
+ });
+
+ it('generates a hidden modal with correct body text', () => {
+ expect(findModalBody().text()).toMatchInterpolatedText(
+ `This will create a README.md for project ${MOCK_PATH_TO_README_PROJECT}.`,
+ );
+ });
+
+ it('generates a hidden modal with correct button text', () => {
+ expect(findModalCreateReadmeButton().text()).toBe('Add README');
+ });
+ });
+ });
+
+ describe('Group does not have README project', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('template', () => {
+ it('does not render README', () => {
+ expect(findHasReadmeButtonLink().exists()).toBe(false);
+ });
+
+ it('does render Add Readme Button with correct text', () => {
+ expect(findAddReadmeButton().exists()).toBe(true);
+ expect(findAddReadmeButton().text()).toBe('Add README');
+ });
+
+ it('generates a hidden modal with correct body text', () => {
+ expect(findModalBody().text()).toMatchInterpolatedText(
+ `This will create a project ${MOCK_GROUP_PATH}/${GITLAB_README_PROJECT} and add a README.md.`,
+ );
+ });
+
+ it('generates a hidden modal with correct button text', () => {
+ expect(findModalCreateReadmeButton().text()).toBe('Create and add README');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/groups/settings/mock_data.js b/spec/frontend/groups/settings/mock_data.js
new file mode 100644
index 00000000000..4551ee3318b
--- /dev/null
+++ b/spec/frontend/groups/settings/mock_data.js
@@ -0,0 +1,6 @@
+export const MOCK_GROUP_PATH = 'test-group';
+export const MOCK_GROUP_ID = '999';
+
+export const MOCK_PATH_TO_GROUP_README = '/group/project/-/blob/main/README.md';
+
+export const MOCK_PATH_TO_README_PROJECT = 'group/project';
diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js
index 77c0966ba1e..86913bb4c09 100644
--- a/spec/frontend/groups_projects/components/transfer_locations_spec.js
+++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js
@@ -109,10 +109,6 @@ describe('TransferLocations', () => {
const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when `GlDropdown` is opened', () => {
it('shows loading icon', async () => {
getTransferLocations.mockReturnValueOnce(new Promise(() => {}));
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index d6263c663d2..ad56b2dde24 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -20,6 +20,7 @@ import {
IS_NOT_FOCUSED,
IS_FOCUSED,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
+ DROPDOWN_CLOSE_TIMEOUT,
} from '~/header_search/constants';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -43,6 +44,9 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('HeaderSearchApp', () => {
let wrapper;
+ jest.useFakeTimers();
+ jest.spyOn(global, 'setTimeout');
+
const actionSpies = {
setSearch: jest.fn(),
fetchAutocompleteOptions: jest.fn(),
@@ -80,10 +84,6 @@ describe('HeaderSearchApp', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findHeaderSearchForm = () => wrapper.findByTestId('header-search-form');
const findHeaderSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findScopeToken = () => wrapper.findComponent(GlToken);
@@ -135,7 +135,7 @@ describe('HeaderSearchApp', () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent();
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'focusin' : '');
});
it(`should${showSearchDropdown ? '' : ' not'} render`, () => {
@@ -157,7 +157,7 @@ describe('HeaderSearchApp', () => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search }, {});
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
});
it(`should${showDefault ? '' : ' not'} render the Default Dropdown Items`, () => {
@@ -178,6 +178,7 @@ describe('HeaderSearchApp', () => {
it(`should close the dropdown when press escape key`, async () => {
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
+ jest.runAllTimers();
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(false);
expect(wrapper.emitted().expandSearchBar.length).toBe(1);
@@ -187,16 +188,16 @@ describe('HeaderSearchApp', () => {
describe.each`
username | showDropdown | expectedDesc
- ${null} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown}
- ${null} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByNoDropdown}
- ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown}
- ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.searchInputDescribeByWithDropdown}
+ ${null} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
+ ${null} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN}
+ ${MOCK_USERNAME} | ${false} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
+ ${MOCK_USERNAME} | ${true} | ${HeaderSearchApp.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN}
`('Search Input Description', ({ username, showDropdown, expectedDesc }) => {
describe(`current_username is ${username} and showDropdown is ${showDropdown}`, () => {
beforeEach(() => {
window.gon.current_username = username;
createComponent();
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'focusin' : '');
});
it(`sets description to ${expectedDesc}`, () => {
@@ -212,7 +213,7 @@ describe('HeaderSearchApp', () => {
${MOCK_USERNAME} | ${true} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
${MOCK_USERNAME} | ${true} | ${''} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
- ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.searchResultsLoading}
+ ${MOCK_USERNAME} | ${true} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${HeaderSearchApp.i18n.SEARCH_RESULTS_LOADING}
`(
'Search Results Description',
({ username, showDropdown, search, loading, searchOptions, expectedDesc }) => {
@@ -228,7 +229,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit(showDropdown ? 'click' : '');
+ findHeaderSearchInput().vm.$emit(showDropdown ? 'focusin' : '');
});
it(`sets description to ${expectedDesc}`, () => {
@@ -257,7 +258,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
});
it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
@@ -291,7 +292,7 @@ describe('HeaderSearchApp', () => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
if (isFocused) {
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
}
});
@@ -332,7 +333,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusin');
});
it(`icon for data set type "${searchOptions[0]?.html_id}" ${
@@ -353,12 +354,12 @@ describe('HeaderSearchApp', () => {
});
describe('events', () => {
- beforeEach(() => {
- createComponent();
- window.gon.current_username = MOCK_USERNAME;
- });
-
describe('Header Search Input', () => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent();
+ });
+
describe('when dropdown is closed', () => {
let trackingSpy;
@@ -366,9 +367,9 @@ describe('HeaderSearchApp', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
- it('onFocus opens dropdown and triggers snowplow event', async () => {
+ it('onFocusin opens dropdown and triggers snowplow event', async () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
- findHeaderSearchInput().vm.$emit('focus');
+ findHeaderSearchInput().vm.$emit('focusin');
await nextTick();
@@ -379,25 +380,19 @@ describe('HeaderSearchApp', () => {
});
});
- it('onClick opens dropdown and triggers snowplow event', async () => {
+ it('onFocusout closes dropdown and triggers snowplow event', async () => {
expect(findHeaderSearchDropdown().exists()).toBe(false);
- findHeaderSearchInput().vm.$emit('click');
+ findHeaderSearchInput().vm.$emit('focusout');
+ jest.runAllTimers();
await nextTick();
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
+ expect(findHeaderSearchDropdown().exists()).toBe(false);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'blur_input', {
label: 'global_search',
property: 'navigation_top',
});
});
-
- it('onClick followed by onFocus only triggers a single snowplow event', async () => {
- findHeaderSearchInput().vm.$emit('click');
- findHeaderSearchInput().vm.$emit('focus');
-
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- });
});
describe('onInput', () => {
@@ -439,18 +434,18 @@ describe('HeaderSearchApp', () => {
});
});
- describe('Dropdown Keyboard Navigation', () => {
+ describe('onFocusout dropdown', () => {
beforeEach(() => {
- findHeaderSearchInput().vm.$emit('click');
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent({ search: 'tes' }, {});
+ findHeaderSearchInput().vm.$emit('focusin');
});
- it('closes dropdown when @tab is emitted', async () => {
- expect(findHeaderSearchDropdown().exists()).toBe(true);
- findDropdownKeyboardNavigation().vm.$emit('tab');
-
- await nextTick();
+ it('closes with timeout so click event gets emited', () => {
+ findHeaderSearchInput().vm.$emit('focusout');
- expect(findHeaderSearchDropdown().exists()).toBe(false);
+ expect(setTimeout).toHaveBeenCalledTimes(1);
+ expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), DROPDOWN_CLOSE_TIMEOUT);
});
});
});
@@ -463,9 +458,9 @@ describe('HeaderSearchApp', () => {
${2} | ${'test1'}
`('currentFocusedOption', ({ MOCK_INDEX, search }) => {
beforeEach(() => {
- createComponent({ search });
window.gon.current_username = MOCK_USERNAME;
- findHeaderSearchInput().vm.$emit('click');
+ createComponent({ search });
+ findHeaderSearchInput().vm.$emit('focusin');
});
it(`when currentFocusIndex changes to ${MOCK_INDEX} updates the data to searchOptions[${MOCK_INDEX}]`, () => {
@@ -504,12 +499,13 @@ describe('HeaderSearchApp', () => {
const MOCK_INDEX = 1;
beforeEach(() => {
- createComponent();
window.gon.current_username = MOCK_USERNAME;
- findHeaderSearchInput().vm.$emit('click');
+ createComponent();
+ findHeaderSearchInput().vm.$emit('focusin');
});
- it('onKey-enter clicks the selected dropdown item rather than submitting a search', () => {
+ it('onKey-enter clicks the selected dropdown item rather than submitting a search', async () => {
+ await nextTick();
findDropdownKeyboardNavigation().vm.$emit('change', MOCK_INDEX);
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
diff --git a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
index 7952661e2d2..e77a9231b7a 100644
--- a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
@@ -3,15 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import HeaderSearchAutocompleteItems from '~/header_search/components/header_search_autocomplete_items.vue';
+import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '~/header_search/constants';
import {
- GROUPS_CATEGORY,
- LARGE_AVATAR_PX,
PROJECTS_CATEGORY,
- SMALL_AVATAR_PX,
+ GROUPS_CATEGORY,
ISSUES_CATEGORY,
MERGE_REQUEST_CATEGORY,
RECENT_EPICS_CATEGORY,
-} from '~/header_search/constants';
+} from '~/vue_shared/global_search/constants';
import {
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
@@ -46,10 +45,6 @@ describe('HeaderSearchAutocompleteItems', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider);
const findFirstDropdownItem = () => findDropdownItems().at(0);
diff --git a/spec/frontend/header_search/components/header_search_default_items_spec.js b/spec/frontend/header_search/components/header_search_default_items_spec.js
index abcacc487df..3768862d83e 100644
--- a/spec/frontend/header_search/components/header_search_default_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_default_items_spec.js
@@ -29,10 +29,6 @@ describe('HeaderSearchDefaultItems', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownItem = () => findDropdownItems().at(0);
diff --git a/spec/frontend/header_search/components/header_search_scoped_items_spec.js b/spec/frontend/header_search/components/header_search_scoped_items_spec.js
index 2db9f71d702..51d67198f04 100644
--- a/spec/frontend/header_search/components/header_search_scoped_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_scoped_items_spec.js
@@ -5,7 +5,8 @@ import Vuex from 'vuex';
import { trimText } from 'helpers/text_helper';
import HeaderSearchScopedItems from '~/header_search/components/header_search_scoped_items.vue';
import { truncate } from '~/lib/utils/text_utility';
-import { MSG_IN_ALL_GITLAB, SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants';
+import { SCOPE_TOKEN_MAX_LENGTH } from '~/header_search/constants';
+import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants';
import {
MOCK_SEARCH,
MOCK_SCOPED_SEARCH_OPTIONS,
@@ -38,10 +39,6 @@ describe('HeaderSearchScopedItems', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownItem = () => findDropdownItems().at(0);
const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => trimText(w.text()));
diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js
index 40c1843d461..9ccc6919b81 100644
--- a/spec/frontend/header_search/init_spec.js
+++ b/spec/frontend/header_search/init_spec.js
@@ -33,7 +33,6 @@ describe('Header Search EventListener', () => {
jest.mock('~/header_search', () => ({ initHeaderSearchApp: jest.fn() }));
await eventHandler.apply(
{
- newHeaderSearchFeatureFlag: true,
searchInputBox: document.querySelector('#search'),
},
[cleanEventListeners],
@@ -47,7 +46,6 @@ describe('Header Search EventListener', () => {
jest.mock('~/header_search', () => ({ initHeaderSearchApp: mockVueApp }));
await eventHandler.apply(
{
- newHeaderSearchFeatureFlag: true,
searchInputBox: document.querySelector('#search'),
},
() => {},
@@ -55,20 +53,4 @@ describe('Header Search EventListener', () => {
expect(mockVueApp).toHaveBeenCalled();
});
-
- it('attaches old vue dropdown when feature flag is disabled', async () => {
- const mockLegacyApp = jest.fn(() => ({
- onSearchInputFocus: jest.fn(),
- }));
- jest.mock('~/search_autocomplete', () => mockLegacyApp);
- await eventHandler.apply(
- {
- newHeaderSearchFeatureFlag: false,
- searchInputBox: document.querySelector('#search'),
- },
- () => {},
- );
-
- expect(mockLegacyApp).toHaveBeenCalled();
- });
});
diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js
index 3a8624ad9dd..2218c81efc3 100644
--- a/spec/frontend/header_search/mock_data.js
+++ b/spec/frontend/header_search/mock_data.js
@@ -1,16 +1,14 @@
+import { ICON_PROJECT, ICON_GROUP, ICON_SUBGROUP } from '~/header_search/constants';
import {
+ PROJECTS_CATEGORY,
+ GROUPS_CATEGORY,
MSG_ISSUES_ASSIGNED_TO_ME,
MSG_ISSUES_IVE_CREATED,
MSG_MR_ASSIGNED_TO_ME,
MSG_MR_IM_REVIEWER,
MSG_MR_IVE_CREATED,
MSG_IN_ALL_GITLAB,
- PROJECTS_CATEGORY,
- ICON_PROJECT,
- GROUPS_CATEGORY,
- ICON_GROUP,
- ICON_SUBGROUP,
-} from '~/header_search/constants';
+} from '~/vue_shared/global_search/constants';
export const MOCK_USERNAME = 'anyone';
diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js
index bd93b0edadf..95a619ebeca 100644
--- a/spec/frontend/header_search/store/actions_spec.js
+++ b/spec/frontend/header_search/store/actions_spec.js
@@ -16,7 +16,7 @@ import {
MOCK_ISSUE_PATH,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Header Search Store Actions', () => {
let state;
diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js
index a1d9481b5cc..7a7a00178f1 100644
--- a/spec/frontend/header_search/store/getters_spec.js
+++ b/spec/frontend/header_search/store/getters_spec.js
@@ -241,6 +241,13 @@ describe('Header Search Store Getters', () => {
MOCK_DEFAULT_SEARCH_OPTIONS,
);
});
+
+ it('returns the correct array if issues path is false', () => {
+ mockGetters.scopedIssuesPath = undefined;
+ expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_DEFAULT_SEARCH_OPTIONS.slice(2, MOCK_DEFAULT_SEARCH_OPTIONS.length),
+ );
+ });
});
describe('scopedSearchOptions', () => {
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 4e2fb70a2cb..4907dc09a3c 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -1,11 +1,11 @@
+import htmlOpenIssue from 'test_fixtures/issues/open-issue.html';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import initTodoToggle, { initNavUserDropdownTracking } from '~/header';
-import { loadHTMLFixture, setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('Header', () => {
describe('Todos notification', () => {
const todosPendingCount = '.js-todos-count';
- const fixtureTemplate = 'issues/open-issue.html';
function isTodosCountHidden() {
return document.querySelector(todosPendingCount).classList.contains('hidden');
@@ -23,7 +23,7 @@ describe('Header', () => {
beforeEach(() => {
initTodoToggle();
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(htmlOpenIssue);
});
afterEach(() => {
diff --git a/spec/frontend/helpers/init_simple_app_helper_spec.js b/spec/frontend/helpers/init_simple_app_helper_spec.js
index 8dd3745e0ac..7938e3851d0 100644
--- a/spec/frontend/helpers/init_simple_app_helper_spec.js
+++ b/spec/frontend/helpers/init_simple_app_helper_spec.js
@@ -38,19 +38,19 @@ describe('helpers/init_simple_app_helper/initSimpleApp', () => {
resetHTMLFixture();
});
- it('mounts the component if the selector exists', async () => {
+ it('mounts the component if the selector exists', () => {
initMock('<div id="mount-here"></div>');
expect(findMock().exists()).toBe(true);
});
- it('does not mount the component if selector does not exist', async () => {
+ it('does not mount the component if selector does not exist', () => {
initMock('<div id="do-not-mount-here"></div>');
expect(didCreateApp()).toBe(false);
});
- it('passes the prop to the component if the prop exists', async () => {
+ it('passes the prop to the component if the prop exists', () => {
initMock(`<div id="mount-here" data-view-model={"someKey":"thing","count":123}></div>`);
expect(findMock().props()).toEqual({
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
index 05161437c22..28c742386cc 100644
--- a/spec/frontend/helpers/startup_css_helper_spec.js
+++ b/spec/frontend/helpers/startup_css_helper_spec.js
@@ -21,17 +21,10 @@ describe('waitForCSSLoaded', () => {
});
describe('when gon features is not provided', () => {
- let originalGon;
-
beforeEach(() => {
- originalGon = window.gon;
window.gon = null;
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('should invoke the action right away', async () => {
const events = waitForCSSLoaded(mockedCallback);
await events;
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
index a97e883a8bf..95582aca8fd 100644
--- a/spec/frontend/ide/components/activity_bar_spec.js
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -1,14 +1,20 @@
+import { nextTick } from 'vue';
import { GlBadge } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ActivityBar from '~/ide/components/activity_bar.vue';
import { leftSidebarViews } from '~/ide/constants';
import { createStore } from '~/ide/stores';
+const { edit, ...VIEW_OBJECTS_WITHOUT_EDIT } = leftSidebarViews;
+const MODES_WITHOUT_EDIT = Object.keys(VIEW_OBJECTS_WITHOUT_EDIT);
+const MODES = Object.keys(leftSidebarViews);
+
describe('IDE ActivityBar component', () => {
let wrapper;
let store;
const findChangesBadge = () => wrapper.findComponent(GlBadge);
+ const findModeButton = (mode) => wrapper.findByTestId(`${mode}-mode-button`);
const mountComponent = (state) => {
store = createStore();
@@ -19,49 +25,43 @@ describe('IDE ActivityBar component', () => {
...state,
});
- wrapper = shallowMount(ActivityBar, { store });
+ wrapper = shallowMountExtended(ActivityBar, { store });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('updateActivityBarView', () => {
- beforeEach(() => {
- mountComponent();
- jest.spyOn(wrapper.vm, 'updateActivityBarView').mockImplementation(() => {});
- });
+ describe('active item', () => {
+ // Test that mode button does not have 'active' class before click,
+ // and does have 'active' class after click
+ const testSettingActiveItem = async (mode) => {
+ const button = findModeButton(mode);
- it('calls updateActivityBarView with edit value on click', () => {
- wrapper.find('.js-ide-edit-mode').trigger('click');
+ expect(button.classes('active')).toBe(false);
- expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
- });
+ button.trigger('click');
+ await nextTick();
- it('calls updateActivityBarView with commit value on click', () => {
- wrapper.find('.js-ide-commit-mode').trigger('click');
+ expect(button.classes('active')).toBe(true);
+ };
- expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
- });
+ it.each(MODES)('is initially set to %s mode', (mode) => {
+ mountComponent({ currentActivityView: leftSidebarViews[mode].name });
- it('calls updateActivityBarView with review value on click', () => {
- wrapper.find('.js-ide-review-mode').trigger('click');
+ const button = findModeButton(mode);
- expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
+ expect(button.classes('active')).toBe(true);
});
- });
- describe('active item', () => {
- it('sets edit item active', () => {
+ it.each(MODES_WITHOUT_EDIT)('is correctly set after clicking %s mode button', (mode) => {
mountComponent();
- expect(wrapper.find('.js-ide-edit-mode').classes()).toContain('active');
+ testSettingActiveItem(mode);
});
- it('sets commit item active', () => {
- mountComponent({ currentActivityView: leftSidebarViews.commit.name });
+ it('is correctly set after clicking edit mode button', () => {
+ // The default currentActivityView is leftSidebarViews.edit.name,
+ // so for the 'edit' mode, we pass a different currentActivityView.
+ mountComponent({ currentActivityView: leftSidebarViews.review.name });
- expect(wrapper.find('.js-ide-commit-mode').classes()).toContain('active');
+ testSettingActiveItem('edit');
});
});
@@ -69,7 +69,6 @@ describe('IDE ActivityBar component', () => {
it('is rendered when files are staged', () => {
mountComponent({ stagedFiles: [{ path: '/path/to/file' }] });
- expect(findChangesBadge().exists()).toBe(true);
expect(findChangesBadge().text()).toBe('1');
});
diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js
index 3dbd1210916..4cae146cbd2 100644
--- a/spec/frontend/ide/components/branches/item_spec.js
+++ b/spec/frontend/ide/components/branches/item_spec.js
@@ -34,10 +34,6 @@ describe('IDE branch item', () => {
router = createRouter(store);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('if not active', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js
index bbde45d700f..eeab26f7559 100644
--- a/spec/frontend/ide/components/branches/search_list_spec.js
+++ b/spec/frontend/ide/components/branches/search_list_spec.js
@@ -35,11 +35,6 @@ describe('IDE branches search list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('calls fetch on mounted', () => {
createComponent();
expect(fetchBranchesMock).toHaveBeenCalled();
diff --git a/spec/frontend/ide/components/cannot_push_code_alert_spec.js b/spec/frontend/ide/components/cannot_push_code_alert_spec.js
index ff659ecdf3f..c72d8c5fccd 100644
--- a/spec/frontend/ide/components/cannot_push_code_alert_spec.js
+++ b/spec/frontend/ide/components/cannot_push_code_alert_spec.js
@@ -10,10 +10,6 @@ const TEST_BUTTON_TEXT = 'Fork text';
describe('ide/components/cannot_push_code_alert', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const createComponent = (props = {}) => {
wrapper = shallowMount(CannotPushCodeAlert, {
propsData: {
@@ -49,7 +45,7 @@ describe('ide/components/cannot_push_code_alert', () => {
createComponent();
});
- it('shows alert with message', () => {
+ it('shows an alert with message', () => {
expect(findAlert().props()).toMatchObject({ dismissible: false });
expect(findAlert().text()).toBe(TEST_MESSAGE);
});
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index dc103fec5d0..019469cbf87 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -46,10 +46,6 @@ describe('IDE commit sidebar actions', () => {
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findText = () => wrapper.text();
const findRadios = () => wrapper.findAll('input[type="radio"]');
diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
index f6d5833edee..ce43e648b43 100644
--- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
@@ -1,7 +1,9 @@
-import { mount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue';
+import { stubComponent } from 'helpers/stub_component';
import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
@@ -12,9 +14,10 @@ const TEST_FILE_PATH = 'test/file/path';
describe('IDE commit editor header', () => {
let wrapper;
let store;
+ const showMock = jest.fn();
const createComponent = (fileProps = {}) => {
- wrapper = mount(EditorHeader, {
+ wrapper = shallowMount(EditorHeader, {
store,
propsData: {
activeFile: {
@@ -23,22 +26,17 @@ describe('IDE commit editor header', () => {
...fileProps,
},
},
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: { show: showMock },
+ }),
+ },
});
};
const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' });
const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' });
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockImplementation();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
fileProps | shouldExist
${{ staged: false, changed: false }} | ${false}
@@ -52,20 +50,19 @@ describe('IDE commit editor header', () => {
});
describe('discard button', () => {
- beforeEach(() => {
+ it('opens a dialog confirming discard', () => {
createComponent();
+ findDiscardButton().vm.$emit('click');
- const modal = findDiscardModal();
- jest.spyOn(modal.vm, 'show');
-
- findDiscardButton().trigger('click');
- });
-
- it('opens a dialog confirming discard', () => {
- expect(findDiscardModal().vm.show).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalled();
});
it('calls discardFileChanges if dialog result is confirmed', () => {
+ store = createStore();
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ createComponent();
+
expect(store.dispatch).not.toHaveBeenCalled();
findDiscardModal().vm.$emit('primary');
diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
index 7c48c0e6f95..4a6aafe42ae 100644
--- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
@@ -11,10 +11,6 @@ describe('IDE commit panel EmptyState component', () => {
wrapper = shallowMount(EmptyState, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders no changes text when last commit message is empty', () => {
expect(wrapper.find('h4').text()).toBe('No changes');
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index a8ee81afa0b..04dd81d9fda 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -21,15 +21,20 @@ import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants';
describe('IDE commit form', () => {
let wrapper;
let store;
+ const showModalSpy = jest.fn();
const createComponent = () => {
wrapper = shallowMount(CommitForm, {
store,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
- GlModal: stubComponent(GlModal),
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showModalSpy,
+ },
+ }),
},
});
};
@@ -57,6 +62,7 @@ describe('IDE commit form', () => {
tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title,
});
const findForm = () => wrapper.find('form');
+ const findModal = () => wrapper.findComponent(GlModal);
const submitForm = () => findForm().trigger('submit');
const findCommitMessageInput = () => wrapper.findComponent(CommitMessageField);
const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val);
@@ -73,10 +79,6 @@ describe('IDE commit form', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
// Notes:
// - When there are no changes, there is no commit button so there's nothing to test :)
describe.each`
@@ -87,7 +89,7 @@ describe('IDE commit form', () => {
${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${MSG_CANNOT_PUSH_CODE}
${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToCommitView} | ${findCommitButtonData} | ${true} | ${MSG_CANNOT_PUSH_CODE}
`('$desc', ({ stagedFiles, userPermissions, viewFn, buttonFn, disabled, tooltip }) => {
- beforeEach(async () => {
+ beforeEach(() => {
store.state.stagedFiles = stagedFiles;
store.state.projects.abcproject.userPermissions = userPermissions;
@@ -302,22 +304,19 @@ describe('IDE commit form', () => {
${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
${createUnexpectedCommitError} | ${{ actionPrimary: null }}
`('opens error modal if commitError with $error', async ({ createError, props }) => {
- const modal = wrapper.findComponent(GlModal);
- modal.vm.show = jest.fn();
-
const error = createError();
store.state.commit.commitError = error;
await nextTick();
- expect(modal.vm.show).toHaveBeenCalled();
- expect(modal.props()).toMatchObject({
+ expect(showModalSpy).toHaveBeenCalled();
+ expect(findModal().props()).toMatchObject({
actionCancel: { text: 'Cancel' },
...props,
});
// Because of the legacy 'mountComponent' approach here, the only way to
// test the text of the modal is by viewing the content of the modal added to the document.
- expect(modal.html()).toContain(error.messageHTML);
+ expect(findModal().html()).toContain(error.messageHTML);
});
});
@@ -343,7 +342,7 @@ describe('IDE commit form', () => {
await nextTick();
- wrapper.findComponent(GlModal).vm.$emit('ok');
+ findModal().vm.$emit('ok');
await waitForPromises();
diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
index c9571d39acb..c2a33c0d71e 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js
@@ -36,10 +36,6 @@ describe('Multi-file editor commit sidebar list item', () => {
findPathEl = wrapper.find('.multi-file-commit-list-path');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findPathText = () => trimText(findPathEl.text());
it('renders file path', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js
index 4406d14d990..c0b0cb0b732 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js
@@ -19,12 +19,8 @@ describe('Multi-file editor commit sidebar list', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with a list of files', () => {
- beforeEach(async () => {
+ beforeEach(() => {
const f = file('file name');
f.changed = true;
wrapper = mountComponent({ fileList: [f] });
diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
index c2ef29c1059..3403a7b8ad9 100644
--- a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js
@@ -15,10 +15,6 @@ describe('IDE commit message field', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findMessage = () => wrapper.find('textarea');
const findHighlights = () => wrapper.findAll('.highlights span');
const findMarks = () => wrapper.findAll('mark');
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index 2a455c9d7c1..ce26519abc9 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -33,15 +33,11 @@ describe('NewMergeRequestOption component', () => {
},
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the `shouldHideNewMrOption` getter returns false', () => {
beforeEach(() => {
createComponent();
@@ -70,7 +66,7 @@ describe('NewMergeRequestOption component', () => {
});
it('disables the new MR checkbox', () => {
- expect(findCheckbox().attributes('disabled')).toBe('true');
+ expect(findCheckbox().attributes('disabled')).toBeDefined();
});
it('adds `is-disabled` class to the fieldset', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
index a3fa03a4aa5..cdf14056523 100644
--- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
@@ -19,15 +19,11 @@ describe('IDE commit sidebar radio group', () => {
propsData: config.props,
slots: config.slots,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without input', () => {
const props = {
value: '1',
diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
index 63d51953915..d1a81dd1639 100644
--- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
@@ -12,10 +12,6 @@ describe('IDE commit panel successful commit state', () => {
wrapper = shallowMount(SuccessMessage, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders last commit message when it exists', () => {
expect(wrapper.text()).toContain('testing commit message');
});
diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js
index 204d39de741..5f6579654bc 100644
--- a/spec/frontend/ide/components/error_message_spec.js
+++ b/spec/frontend/ide/components/error_message_spec.js
@@ -32,11 +32,6 @@ describe('IDE error message component', () => {
setErrorMessageMock.mockReset();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findDismissButton = () => wrapper.find('button[aria-label=Dismiss]');
const findActionButton = () => wrapper.find('button.gl-alert-action');
diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js
index 281c549a1b4..f5a6e7222f9 100644
--- a/spec/frontend/ide/components/file_row_extra_spec.js
+++ b/spec/frontend/ide/components/file_row_extra_spec.js
@@ -37,8 +37,6 @@ describe('IDE extra file row component', () => {
};
afterEach(() => {
- wrapper.destroy();
-
stagedFilesCount = 0;
unstagedFilesCount = 0;
changesCount = 0;
diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js
index 60f37260393..b8c850fdd13 100644
--- a/spec/frontend/ide/components/file_templates/bar_spec.js
+++ b/spec/frontend/ide/components/file_templates/bar_spec.js
@@ -21,10 +21,6 @@ describe('IDE file templates bar component', () => {
wrapper = mount(Bar, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template type dropdown', () => {
it('renders dropdown component', () => {
expect(wrapper.find('.dropdown').text()).toContain('Choose a type');
diff --git a/spec/frontend/ide/components/file_templates/dropdown_spec.js b/spec/frontend/ide/components/file_templates/dropdown_spec.js
index ee90d87357c..72fdd05eb2c 100644
--- a/spec/frontend/ide/components/file_templates/dropdown_spec.js
+++ b/spec/frontend/ide/components/file_templates/dropdown_spec.js
@@ -49,11 +49,6 @@ describe('IDE file templates dropdown component', () => {
({ element } = wrapper);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('calls clickItem on click', async () => {
const itemData = { name: 'test.yml ' };
createComponent({ props: { data: [itemData] } });
diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js
index aa66224fa19..331877ff112 100644
--- a/spec/frontend/ide/components/ide_file_row_spec.js
+++ b/spec/frontend/ide/components/ide_file_row_spec.js
@@ -34,11 +34,6 @@ describe('Ide File Row component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findFileRowExtra = () => wrapper.findComponent(FileRowExtra);
const findFileRow = () => wrapper.findComponent(FileRow);
const hasDropdownOpen = () => findFileRowExtra().props('dropdownOpen');
diff --git a/spec/frontend/ide/components/ide_project_header_spec.js b/spec/frontend/ide/components/ide_project_header_spec.js
index d0636352a3f..7613f407e45 100644
--- a/spec/frontend/ide/components/ide_project_header_spec.js
+++ b/spec/frontend/ide/components/ide_project_header_spec.js
@@ -20,10 +20,6 @@ describe('IDE project header', () => {
wrapper = shallowMount(IDEProjectHeader, { propsData: { project: mockProject } });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index 0759f957374..7ae8cfac935 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -30,10 +30,6 @@ describe('IDE review mode', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders list of files', () => {
expect(wrapper.text()).toContain('fileName');
});
@@ -67,7 +63,7 @@ describe('IDE review mode', () => {
await wrapper.vm.reactivate();
});
- it('updates viewer to "mrdiff"', async () => {
+ it('updates viewer to "mrdiff"', () => {
expect(store.state.viewer).toBe('mrdiff');
});
});
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index 4784d6c516f..c258c5312d8 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -29,11 +29,6 @@ describe('IdeSidebar', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders a sidebar', () => {
wrapper = createComponent();
diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
index 80e8aba4072..4ee24f63f76 100644
--- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js
+++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
@@ -25,10 +25,6 @@ describe('ide/components/ide_sidebar_nav', () => {
let wrapper;
const createComponent = (props = {}) => {
- if (wrapper) {
- throw new Error('wrapper already exists');
- }
-
wrapper = shallowMount(IdeSidebarNav, {
propsData: {
tabs: TEST_TABS,
@@ -37,16 +33,11 @@ describe('ide/components/ide_sidebar_nav', () => {
...props,
},
directives: {
- tooltip: createMockDirective(),
+ tooltip: createMockDirective('tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findButtons = () => wrapper.findAll('li button');
const findButtonsData = () =>
findButtons().wrappers.map((button) => {
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index a575f428a69..eb8f2a5e4ac 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -45,6 +45,13 @@ describe('WebIDE', () => {
const callOnBeforeUnload = (e = {}) => window.onbeforeunload(e);
+ beforeAll(() => {
+ // HACK: Workaround readonly property in Jest
+ Object.defineProperty(window, 'onbeforeunload', {
+ writable: true,
+ });
+ });
+
beforeEach(() => {
stubPerformanceWebAPI();
@@ -52,8 +59,6 @@ describe('WebIDE', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
window.onbeforeunload = null;
});
@@ -66,7 +71,7 @@ describe('WebIDE', () => {
});
});
- it('renders "New file" button in empty repo', async () => {
+ it('renders "New file" button in empty repo', () => {
expect(wrapper.find('[title="New file"]').exists()).toBe(true);
});
});
@@ -171,7 +176,7 @@ describe('WebIDE', () => {
});
});
- it('when user cannot push code, shows alert', () => {
+ it('when user cannot push code, shows an alert', () => {
store.state.links = {
forkInfo: {
ide_path: TEST_FORK_IDE_PATH,
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index e6e0ebaf1e8..0ee16f98e7e 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -34,10 +34,6 @@ describe('IdeStatusBar component', () => {
wrapper = mount(IdeStatusBar, { store });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
it('triggers a setInterval', () => {
mountComponent();
diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js
index 0b54e8b6afb..344a1fbc4f6 100644
--- a/spec/frontend/ide/components/ide_status_list_spec.js
+++ b/spec/frontend/ide/components/ide_status_list_spec.js
@@ -53,10 +53,7 @@ describe('ide/components/ide_status_list', () => {
});
afterEach(() => {
- wrapper.destroy();
-
store = null;
- wrapper = null;
});
describe('with regular file', () => {
diff --git a/spec/frontend/ide/components/ide_status_mr_spec.js b/spec/frontend/ide/components/ide_status_mr_spec.js
index 0b9111c0e2a..3501ecce061 100644
--- a/spec/frontend/ide/components/ide_status_mr_spec.js
+++ b/spec/frontend/ide/components/ide_status_mr_spec.js
@@ -17,10 +17,6 @@ describe('ide/components/ide_status_mr', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
const findLink = () => wrapper.findComponent(GlLink);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when mounted', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js
index 0f61aa80e53..427daa57324 100644
--- a/spec/frontend/ide/components/ide_tree_list_spec.js
+++ b/spec/frontend/ide/components/ide_tree_list_spec.js
@@ -25,10 +25,6 @@ describe('IdeTreeList component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('normal branch', () => {
const tree = [file('fileName')];
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index f00017a2736..bcfa6809eca 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { keepAlive } from 'helpers/keep_alive_component_helper';
+import { viewerTypes } from '~/ide/constants';
import IdeTree from '~/ide/components/ide_tree.vue';
-import { createStore } from '~/ide/stores';
+import { createStoreOptions } from '~/ide/stores';
import { file } from '../helpers';
import { projectData } from '../mock_data';
@@ -13,46 +13,72 @@ describe('IdeTree', () => {
let store;
let wrapper;
- beforeEach(() => {
- store = createStore();
-
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'main';
- store.state.projects.abcproject = { ...projectData };
- Vue.set(store.state.trees, 'abcproject/main', {
- tree: [file('fileName')],
- loading: false,
+ const actionSpies = {
+ updateViewer: jest.fn(),
+ };
+
+ const testState = {
+ currentProjectId: 'abcproject',
+ currentBranchId: 'main',
+ projects: {
+ abcproject: { ...projectData },
+ },
+ trees: {
+ 'abcproject/main': {
+ tree: [file('fileName')],
+ loading: false,
+ },
+ },
+ };
+
+ const createComponent = (replaceState) => {
+ const defaultStore = createStoreOptions();
+
+ store = new Vuex.Store({
+ ...defaultStore,
+ state: {
+ ...defaultStore.state,
+ ...testState,
+ replaceState,
+ },
+ actions: {
+ ...defaultStore.actions,
+ ...actionSpies,
+ },
});
- wrapper = mount(keepAlive(IdeTree), {
+ wrapper = mount(IdeTree, {
store,
});
+ };
+
+ beforeEach(() => {
+ createComponent();
});
afterEach(() => {
- wrapper.destroy();
+ actionSpies.updateViewer.mockClear();
});
- it('renders list of files', () => {
- expect(wrapper.text()).toContain('fileName');
+ describe('renders properly', () => {
+ it('renders list of files', () => {
+ expect(wrapper.text()).toContain('fileName');
+ });
});
describe('activated', () => {
- let inititializeSpy;
-
- beforeEach(async () => {
- inititializeSpy = jest.spyOn(wrapper.findComponent(IdeTree).vm, 'initialize');
- store.state.viewer = 'diff';
-
- await wrapper.vm.reactivate();
+ beforeEach(() => {
+ createComponent({
+ viewer: viewerTypes.diff,
+ });
});
it('re initializes the component', () => {
- expect(inititializeSpy).toHaveBeenCalled();
+ expect(actionSpies.updateViewer).toHaveBeenCalled();
});
it('updates viewer to "editor" by default', () => {
- expect(store.state.viewer).toBe('editor');
+ expect(actionSpies.updateViewer).toHaveBeenCalledWith(expect.any(Object), viewerTypes.edit);
});
});
});
diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js
index 629c4424314..2bb0f3fccf4 100644
--- a/spec/frontend/ide/components/jobs/detail/description_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/description_spec.js
@@ -14,10 +14,6 @@ describe('IDE job description', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders job details', () => {
expect(wrapper.text()).toContain('#1');
expect(wrapper.text()).toContain('test');
diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
index 5eb66f75978..450c6cb357c 100644
--- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js
@@ -15,10 +15,6 @@ describe('IDE job log scroll button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
direction | icon | title
${'up'} | ${'scroll_up'} | ${'Scroll to top'}
@@ -45,6 +41,6 @@ describe('IDE job log scroll button', () => {
it('disables button when disabled is true', () => {
createComponent({ disabled: true });
- expect(wrapper.find('button').attributes('disabled')).toBe('disabled');
+ expect(wrapper.find('button').attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js
index bf2be3aa595..334501bbca7 100644
--- a/spec/frontend/ide/components/jobs/detail_spec.js
+++ b/spec/frontend/ide/components/jobs/detail_spec.js
@@ -34,10 +34,6 @@ describe('IDE jobs detail view', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('mounted', () => {
const findJobOutput = () => wrapper.find('.bash');
const findBuildLoaderAnimation = () => wrapper.find('.build-loader-animation');
@@ -123,8 +119,8 @@ describe('IDE jobs detail view', () => {
await findBuildJobLog().trigger('scroll');
- expect(findScrollToBottomButton().attributes('disabled')).toBe('disabled');
- expect(findScrollToTopButton().attributes('disabled')).not.toBe('disabled');
+ expect(findScrollToBottomButton().attributes('disabled')).toBeDefined();
+ expect(findScrollToTopButton().attributes('disabled')).toBeUndefined();
});
it('keeps scroll at top when already at top', async () => {
@@ -132,8 +128,8 @@ describe('IDE jobs detail view', () => {
await findBuildJobLog().trigger('scroll');
- expect(findScrollToBottomButton().attributes('disabled')).not.toBe('disabled');
- expect(findScrollToTopButton().attributes('disabled')).toBe('disabled');
+ expect(findScrollToBottomButton().attributes('disabled')).toBeUndefined();
+ expect(findScrollToTopButton().attributes('disabled')).toBeDefined();
});
it('resets scroll when not at top or bottom', async () => {
@@ -141,8 +137,8 @@ describe('IDE jobs detail view', () => {
await findBuildJobLog().trigger('scroll');
- expect(findScrollToBottomButton().attributes('disabled')).not.toBe('disabled');
- expect(findScrollToTopButton().attributes('disabled')).not.toBe('disabled');
+ expect(findScrollToBottomButton().attributes('disabled')).toBeUndefined();
+ expect(findScrollToTopButton().attributes('disabled')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js
index 32e27333e42..ab442a27817 100644
--- a/spec/frontend/ide/components/jobs/item_spec.js
+++ b/spec/frontend/ide/components/jobs/item_spec.js
@@ -12,10 +12,6 @@ describe('IDE jobs item', () => {
wrapper = mount(JobItem, { propsData: { job } });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders job details', () => {
expect(wrapper.text()).toContain(job.name);
expect(wrapper.text()).toContain(`#${job.id}`);
diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js
index b4c7eb51781..0ece42bce51 100644
--- a/spec/frontend/ide/components/jobs/list_spec.js
+++ b/spec/frontend/ide/components/jobs/list_spec.js
@@ -50,11 +50,6 @@ describe('IDE stages list', () => {
Object.values(storeActions).forEach((actionMock) => actionMock.mockClear());
});
- afterAll(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders loading icon when no stages & loading', () => {
createComponent({ loading: true, stages: [] });
diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js
index 52fbff2f497..23ef92f9682 100644
--- a/spec/frontend/ide/components/jobs/stage_spec.js
+++ b/spec/frontend/ide/components/jobs/stage_spec.js
@@ -31,11 +31,6 @@ describe('IDE pipeline stage', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('emits fetch event when mounted', () => {
createComponent();
expect(wrapper.emitted().fetch).toBeDefined();
diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js
index d6cf8127b53..2fbb6919b8b 100644
--- a/spec/frontend/ide/components/merge_requests/item_spec.js
+++ b/spec/frontend/ide/components/merge_requests/item_spec.js
@@ -39,11 +39,6 @@ describe('IDE merge request item', () => {
router = createRouter(store);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js
index ea6e2741a85..3b0e8c632fb 100644
--- a/spec/frontend/ide/components/merge_requests/list_spec.js
+++ b/spec/frontend/ide/components/merge_requests/list_spec.js
@@ -48,11 +48,6 @@ describe('IDE merge requests list', () => {
fetchMergeRequestsMock = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('calls fetch on mounted', () => {
createComponent();
expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), {
diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js
index 8eebcdd9e08..3aae2c83e80 100644
--- a/spec/frontend/ide/components/nav_dropdown_button_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js
@@ -9,10 +9,6 @@ describe('NavDropdownButton component', () => {
const TEST_MR_ID = '12345';
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const createComponent = ({ props = {}, state = {} } = {}) => {
const store = createStore();
store.replaceState(state);
diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js
index 33e638843f5..794aaba6d01 100644
--- a/spec/frontend/ide/components/nav_dropdown_spec.js
+++ b/spec/frontend/ide/components/nav_dropdown_spec.js
@@ -30,10 +30,6 @@ describe('IDE NavDropdown', () => {
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const createComponent = () => {
wrapper = mount(NavDropdown, {
store,
diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js
index a9cfdfd20c1..bfd5cdf7263 100644
--- a/spec/frontend/ide/components/new_dropdown/button_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/button_spec.js
@@ -14,10 +14,6 @@ describe('IDE new entry dropdown button component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders button with label', () => {
createComponent();
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
index 747c099db33..a2371abe955 100644
--- a/spec/frontend/ide/components/new_dropdown/index_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -1,37 +1,50 @@
-import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import NewDropdown from '~/ide/components/new_dropdown/index.vue';
import Button from '~/ide/components/new_dropdown/button.vue';
-import { createStore } from '~/ide/stores';
+import Modal from '~/ide/components/new_dropdown/modal.vue';
+import { stubComponent } from 'helpers/stub_component';
+
+Vue.use(Vuex);
describe('new dropdown component', () => {
let wrapper;
+ const openMock = jest.fn();
+ const deleteEntryMock = jest.fn();
const findAllButtons = () => wrapper.findAllComponents(Button);
- const mountComponent = () => {
- const store = createStore();
- store.state.currentProjectId = 'abcproject';
- store.state.path = '';
- store.state.trees['abcproject/mybranch'] = { tree: [] };
+ const mountComponent = (props = {}) => {
+ const fakeStore = () => {
+ return new Vuex.Store({
+ actions: {
+ deleteEntry: deleteEntryMock,
+ },
+ });
+ };
- wrapper = mount(NewDropdown, {
- store,
+ wrapper = mountExtended(NewDropdown, {
+ store: fakeStore(),
propsData: {
branch: 'main',
path: '',
mouseOver: false,
type: 'tree',
+ ...props,
+ },
+ stubs: {
+ NewModal: stubComponent(Modal, {
+ methods: {
+ open: openMock,
+ },
+ }),
},
});
};
beforeEach(() => {
mountComponent();
- jest.spyOn(wrapper.vm.$refs.newModal, 'open').mockImplementation(() => {});
- });
-
- afterEach(() => {
- wrapper.destroy();
});
it('renders new file, upload and new directory links', () => {
@@ -42,37 +55,34 @@ describe('new dropdown component', () => {
describe('createNewItem', () => {
it('opens modal for a blob when new file is clicked', () => {
- findAllButtons().at(0).trigger('click');
+ findAllButtons().at(0).vm.$emit('click');
- expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('blob', '');
+ expect(openMock).toHaveBeenCalledWith('blob', '');
});
it('opens modal for a tree when new directory is clicked', () => {
- findAllButtons().at(2).trigger('click');
+ findAllButtons().at(2).vm.$emit('click');
- expect(wrapper.vm.$refs.newModal.open).toHaveBeenCalledWith('tree', '');
+ expect(openMock).toHaveBeenCalledWith('tree', '');
});
});
describe('isOpen', () => {
it('scrolls dropdown into view', async () => {
- jest.spyOn(wrapper.vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
+ const dropdownMenu = wrapper.findByTestId('dropdown-menu');
+ const scrollIntoViewSpy = jest.spyOn(dropdownMenu.element, 'scrollIntoView');
await wrapper.setProps({ isOpen: true });
- expect(wrapper.vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
- block: 'nearest',
- });
+ expect(scrollIntoViewSpy).toHaveBeenCalledWith({ block: 'nearest' });
});
});
describe('delete entry', () => {
it('calls delete action', () => {
- jest.spyOn(wrapper.vm, 'deleteEntry').mockImplementation(() => {});
-
findAllButtons().at(4).trigger('click');
- expect(wrapper.vm.deleteEntry).toHaveBeenCalledWith('');
+ expect(deleteEntryMock).toHaveBeenCalledWith(expect.anything(), '');
});
});
});
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index c6f9fd0c4ea..36c3d323e63 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -1,13 +1,13 @@
import { GlButton, GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Modal from '~/ide/components/new_dropdown/modal.vue';
import { createStore } from '~/ide/stores';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createEntriesFromPaths } from '../../helpers';
-jest.mock('~/flash');
+jest.mock('~/alert');
const NEW_NAME = 'babar';
@@ -79,7 +79,6 @@ describe('new file modal component', () => {
afterEach(() => {
store = null;
- wrapper.destroy();
document.body.innerHTML = '';
});
@@ -94,11 +93,11 @@ describe('new file modal component', () => {
it('renders modal', () => {
expect(findGlModal().props()).toMatchObject({
actionCancel: {
- attributes: [{ variant: 'default' }],
+ attributes: { variant: 'default' },
text: 'Cancel',
},
actionPrimary: {
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
text: 'Create file',
},
actionSecondary: null,
@@ -170,7 +169,7 @@ describe('new file modal component', () => {
expect(findGlModal().props()).toMatchObject({
title: modalTitle,
actionPrimary: {
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
text: btnTitle,
},
});
@@ -298,7 +297,7 @@ describe('new file modal component', () => {
expect(findGlModal().props()).toMatchObject({
title,
actionPrimary: {
- attributes: [{ variant: 'confirm' }],
+ attributes: { variant: 'confirm' },
text: title,
},
});
@@ -340,7 +339,7 @@ describe('new file modal component', () => {
});
});
- it('does not trigger flash', () => {
+ it('does not trigger alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -359,7 +358,7 @@ describe('new file modal component', () => {
});
});
- it('does not trigger flash', () => {
+ it('does not trigger alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -379,7 +378,7 @@ describe('new file modal component', () => {
triggerSubmitModal();
});
- it('creates flash', () => {
+ it('creates alert', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'The name "src" is already taken in this directory.',
fadeTransition: false,
@@ -404,7 +403,7 @@ describe('new file modal component', () => {
triggerSubmitModal();
});
- it('does not create flash', () => {
+ it('does not create alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
index fc643589d51..883b365c99a 100644
--- a/spec/frontend/ide/components/new_dropdown/upload_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -1,36 +1,31 @@
import { mount } from '@vue/test-utils';
import Upload from '~/ide/components/new_dropdown/upload.vue';
+import waitForPromises from 'helpers/wait_for_promises';
describe('new dropdown upload', () => {
let wrapper;
- beforeEach(() => {
+ function createComponent() {
wrapper = mount(Upload, {
propsData: {
path: '',
},
});
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('openFile', () => {
- it('calls for each file', () => {
- const files = ['test', 'test2', 'test3'];
-
- jest.spyOn(wrapper.vm, 'readFile').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.$refs.fileUpload, 'files', 'get').mockReturnValue(files);
+ }
- wrapper.vm.openFile();
+ const uploadFile = (file) => {
+ const input = wrapper.find('input[type="file"]');
+ Object.defineProperty(input.element, 'files', { value: [file] });
+ input.trigger('change', file);
+ };
- expect(wrapper.vm.readFile.mock.calls.length).toBe(3);
+ const waitForFileToLoad = async () => {
+ await waitForPromises();
+ return waitForPromises();
+ };
- files.forEach((file, i) => {
- expect(wrapper.vm.readFile.mock.calls[i]).toEqual([file]);
- });
- });
+ beforeEach(() => {
+ createComponent();
});
describe('readFile', () => {
@@ -43,20 +38,13 @@ describe('new dropdown upload', () => {
type: 'images/png',
};
- wrapper.vm.readFile(file);
+ uploadFile(file);
expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
});
});
describe('createFile', () => {
- const textTarget = {
- result: 'base64,cGxhaW4gdGV4dA==',
- };
- const binaryTarget = {
- result: 'base64,8PDw8A==', // ðððð
- };
-
const textFile = new File(['plain text'], 'textFile', { type: 'test/mime-text' });
const binaryFile = new File(['😺'], 'binaryFile', { type: 'test/mime-binary' });
@@ -65,15 +53,13 @@ describe('new dropdown upload', () => {
});
it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', async () => {
- const waitForCreate = new Promise((resolve) => {
- wrapper.vm.$on('create', resolve);
- });
+ uploadFile(textFile);
- wrapper.vm.createFile(textTarget, textFile);
+ // Text file has an additional load, so need to wait twice
+ await waitForFileToLoad();
+ await waitForFileToLoad();
expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(textFile);
-
- await waitForCreate;
expect(wrapper.emitted('create')[0]).toStrictEqual([
{
name: textFile.name,
@@ -85,8 +71,10 @@ describe('new dropdown upload', () => {
]);
});
- it('creates a blob URL for the content if binary', () => {
- wrapper.vm.createFile(binaryTarget, binaryFile);
+ it('creates a blob URL for the content if binary', async () => {
+ uploadFile(binaryFile);
+
+ await waitForFileToLoad();
expect(FileReader.prototype.readAsText).not.toHaveBeenCalled();
@@ -94,7 +82,7 @@ describe('new dropdown upload', () => {
{
name: binaryFile.name,
type: 'blob',
- content: 'ðððð',
+ content: '😺', // '😺'
rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b',
mimeType: 'test/mime-binary',
},
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index e92f843ae6e..42eb5b3fc7a 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -35,11 +35,6 @@ describe('ide/components/panes/collapsible_sidebar.vue', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with a tab', () => {
let fakeView;
let extensionTabs;
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index 1d81c3ea89d..832983edf21 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -28,11 +28,6 @@ describe('ide/components/panes/right.vue', () => {
store = createStore();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/pipelines/empty_state_spec.js b/spec/frontend/ide/components/pipelines/empty_state_spec.js
index 31081e8f9d5..71de9aecb52 100644
--- a/spec/frontend/ide/components/pipelines/empty_state_spec.js
+++ b/spec/frontend/ide/components/pipelines/empty_state_spec.js
@@ -22,10 +22,6 @@ describe('~/ide/components/pipelines/empty_state.vue', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index d82b97561f0..e913fa84d56 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -65,11 +65,6 @@ describe('IDE pipelines list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('fetches latest pipeline', () => {
createComponent();
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index d3312358402..ead609421b7 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -63,11 +63,6 @@ describe('RepoCommitSection', () => {
jest.spyOn(router, 'push').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('empty state', () => {
beforeEach(() => {
store.state.noChangesStateSvgPath = TEST_NO_CHANGES_SVG;
@@ -162,21 +157,4 @@ describe('RepoCommitSection', () => {
expect(wrapper.findComponent(EmptyState).exists()).toBe(false);
});
});
-
- describe('activated', () => {
- let inititializeSpy;
-
- beforeEach(async () => {
- createComponent();
-
- inititializeSpy = jest.spyOn(wrapper.findComponent(RepoCommitSection).vm, 'initialize');
- store.state.viewer = 'diff';
-
- await wrapper.vm.reactivate();
- });
-
- it('re initializes the component', () => {
- expect(inititializeSpy).toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index c9f033bffbb..6747ec97050 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -162,8 +162,6 @@ describe('RepoEditor', () => {
// create a new model each time, otherwise tests conflict with each other
// because of same model being used in multiple tests
monacoEditor.getModels().forEach((model) => model.dispose());
- wrapper.destroy();
- wrapper = null;
});
describe('default', () => {
@@ -295,7 +293,7 @@ describe('RepoEditor', () => {
});
describe('when file changes to non-markdown file', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper.setProps({ file: dummyFile.empty });
});
@@ -603,7 +601,7 @@ describe('RepoEditor', () => {
const f = createRemoteFile('newFile');
Vue.set(vm.$store.state.entries, f.path, f);
- jest.spyOn(service, 'getRawFileData').mockImplementation(async () => {
+ jest.spyOn(service, 'getRawFileData').mockImplementation(() => {
expect(vm.file.loading).toBe(true);
// switching from edit to diff mode usually triggers editor initialization
@@ -611,7 +609,7 @@ describe('RepoEditor', () => {
jest.runOnlyPendingTimers();
- return 'rawFileData123\n';
+ return Promise.resolve('rawFileData123\n');
});
wrapper.setProps({
@@ -632,18 +630,18 @@ describe('RepoEditor', () => {
jest
.spyOn(service, 'getRawFileData')
- .mockImplementation(async () => {
+ .mockImplementation(() => {
// opening fileB while the content of fileA is still being fetched
wrapper.setProps({
file: fileB,
});
- return aContent;
+ return Promise.resolve(aContent);
})
- .mockImplementationOnce(async () => {
+ .mockImplementationOnce(() => {
// we delay returning fileB content
// to make sure the editor doesn't initialize prematurely
jest.advanceTimersByTime(30);
- return bContent;
+ return Promise.resolve(bContent);
});
wrapper.setProps({
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index b26edc5a85b..d4f29b16a88 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -1,11 +1,10 @@
import { GlTab } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { stubComponent } from 'helpers/stub_component';
import RepoTab from '~/ide/components/repo_tab.vue';
-import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { file } from '../helpers';
Vue.use(Vuex);
@@ -17,41 +16,40 @@ const GlTabStub = stubComponent(GlTab, {
describe('RepoTab', () => {
let wrapper;
let store;
- let router;
+ const pushMock = jest.fn();
const findTab = () => wrapper.findComponent(GlTabStub);
+ const findCloseButton = () => wrapper.findByTestId('close-button');
function createComponent(propsData) {
- wrapper = mount(RepoTab, {
+ wrapper = mountExtended(RepoTab, {
store,
propsData,
stubs: {
GlTab: GlTabStub,
},
+ mocks: {
+ $router: {
+ push: pushMock,
+ },
+ },
});
}
beforeEach(() => {
store = createStore();
- router = createRouter(store);
- jest.spyOn(router, 'push').mockImplementation(() => {});
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
});
it('renders a close link and a name link', () => {
+ const tab = file();
createComponent({
- tab: file(),
+ tab,
});
- wrapper.vm.$store.state.openFiles.push(wrapper.vm.tab);
- const close = wrapper.find('.multi-file-tab-close');
+ store.state.openFiles.push(tab);
const name = wrapper.find(`[title]`);
- expect(close.html()).toContain('#close');
- expect(name.text().trim()).toEqual(wrapper.vm.tab.name);
+ expect(findCloseButton().html()).toContain('#close');
+ expect(name.text()).toBe(tab.name);
});
it('does not call openPendingTab when tab is active', async () => {
@@ -63,35 +61,33 @@ describe('RepoTab', () => {
},
});
- jest.spyOn(wrapper.vm, 'openPendingTab').mockImplementation(() => {});
+ jest.spyOn(store, 'dispatch');
await findTab().vm.$emit('click');
- expect(wrapper.vm.openPendingTab).not.toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalledWith('openPendingTab');
});
- it('fires clickFile when the link is clicked', () => {
- createComponent({
- tab: file(),
- });
-
- jest.spyOn(wrapper.vm, 'clickFile').mockImplementation(() => {});
+ it('fires clickFile when the link is clicked', async () => {
+ const { getters } = store;
+ const tab = file();
+ createComponent({ tab });
- findTab().vm.$emit('click');
+ await findTab().vm.$emit('click', tab);
- expect(wrapper.vm.clickFile).toHaveBeenCalledWith(wrapper.vm.tab);
+ expect(pushMock).toHaveBeenCalledWith(getters.getUrlForPath(tab.path));
});
- it('calls closeFile when clicking close button', () => {
- createComponent({
- tab: file(),
- });
+ it('calls closeFile when clicking close button', async () => {
+ const tab = file();
+ createComponent({ tab });
+ store.state.entries[tab.path] = tab;
- jest.spyOn(wrapper.vm, 'closeFile').mockImplementation(() => {});
+ jest.spyOn(store, 'dispatch');
- wrapper.find('.multi-file-tab-close').trigger('click');
+ await findCloseButton().trigger('click');
- expect(wrapper.vm.closeFile).toHaveBeenCalledWith(wrapper.vm.tab);
+ expect(store.dispatch).toHaveBeenCalledWith('closeFile', tab);
});
it('changes icon on hover', async () => {
@@ -119,7 +115,7 @@ describe('RepoTab', () => {
createComponent({ tab });
- expect(wrapper.find('button').attributes('aria-label')).toBe(closeLabel);
+ expect(findCloseButton().attributes('aria-label')).toBe(closeLabel);
});
describe('locked file', () => {
@@ -157,15 +153,15 @@ describe('RepoTab', () => {
createComponent({
tab,
});
- wrapper.vm.$store.state.openFiles.push(tab);
- wrapper.vm.$store.state.changedFiles.push(tab);
- wrapper.vm.$store.state.entries[tab.path] = tab;
- wrapper.vm.$store.dispatch('setFileActive', tab.path);
+ store.state.openFiles.push(tab);
+ store.state.changedFiles.push(tab);
+ store.state.entries[tab.path] = tab;
+ store.dispatch('setFileActive', tab.path);
- await wrapper.find('.multi-file-tab-close').trigger('click');
+ await findCloseButton().trigger('click');
expect(tab.opened).toBe(false);
- expect(wrapper.vm.$store.state.changedFiles).toHaveLength(1);
+ expect(store.state.changedFiles).toHaveLength(1);
});
it('closes tab when clicking close btn', async () => {
@@ -174,11 +170,11 @@ describe('RepoTab', () => {
createComponent({
tab,
});
- wrapper.vm.$store.state.openFiles.push(tab);
- wrapper.vm.$store.state.entries[tab.path] = tab;
- wrapper.vm.$store.dispatch('setFileActive', tab.path);
+ store.state.openFiles.push(tab);
+ store.state.entries[tab.path] = tab;
+ store.dispatch('setFileActive', tab.path);
- await wrapper.find('.multi-file-tab-close').trigger('click');
+ await findCloseButton().trigger('click');
expect(tab.opened).toBe(false);
});
diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js
index 1cfc1f12745..06ad162d398 100644
--- a/spec/frontend/ide/components/repo_tabs_spec.js
+++ b/spec/frontend/ide/components/repo_tabs_spec.js
@@ -25,10 +25,6 @@ describe('RepoTabs', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a list of tabs', async () => {
store.state.openFiles[0].active = true;
diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js
index fe2a128c9c8..240e675a38e 100644
--- a/spec/frontend/ide/components/resizable_panel_spec.js
+++ b/spec/frontend/ide/components/resizable_panel_spec.js
@@ -19,11 +19,6 @@ describe('~/ide/components/resizable_panel', () => {
jest.spyOn(store, 'dispatch').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createComponent = (props = {}) => {
wrapper = shallowMount(ResizablePanel, {
propsData: {
diff --git a/spec/frontend/ide/components/shared/commit_message_field_spec.js b/spec/frontend/ide/components/shared/commit_message_field_spec.js
index 94da06f4cb2..ccf544b27b7 100644
--- a/spec/frontend/ide/components/shared/commit_message_field_spec.js
+++ b/spec/frontend/ide/components/shared/commit_message_field_spec.js
@@ -23,10 +23,6 @@ describe('CommitMessageField', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTextArea = () => wrapper.find('textarea');
const findHighlights = () => wrapper.findByTestId('highlights');
const findHighlightsText = () => wrapper.findByTestId('highlights-text');
@@ -54,7 +50,7 @@ describe('CommitMessageField', () => {
await nextTick();
});
- it('is added on textarea focus', async () => {
+ it('is added on textarea focus', () => {
expect(wrapper.attributes('class')).toEqual(
expect.stringContaining('gl-outline-none! gl-focus-ring-border-1-gray-900!'),
);
diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js
index b70c9659e46..4bd5a6527e2 100644
--- a/spec/frontend/ide/components/shared/tokened_input_spec.js
+++ b/spec/frontend/ide/components/shared/tokened_input_spec.js
@@ -28,10 +28,6 @@ describe('IDE shared/TokenedInput', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders tokens', () => {
createComponent();
const renderedTokens = getTokenElements(wrapper).wrappers.map((w) => w.text());
diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js
index 15fb0fe9013..3a691c151d5 100644
--- a/spec/frontend/ide/components/terminal/empty_state_spec.js
+++ b/spec/frontend/ide/components/terminal/empty_state_spec.js
@@ -16,10 +16,6 @@ describe('IDE TerminalEmptyState', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not show illustration, if no path specified', () => {
factory();
diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js
index 0d22f7f73fe..0500c116d23 100644
--- a/spec/frontend/ide/components/terminal/terminal_spec.js
+++ b/spec/frontend/ide/components/terminal/terminal_spec.js
@@ -59,10 +59,6 @@ describe('IDE Terminal', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading text', () => {
[STARTING, PENDING].forEach((status) => {
it(`shows when starting (${status})`, () => {
diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js
index 57c8da9f5b7..b8ffaa89047 100644
--- a/spec/frontend/ide/components/terminal/view_spec.js
+++ b/spec/frontend/ide/components/terminal/view_spec.js
@@ -59,10 +59,6 @@ describe('IDE TerminalView', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders empty state', async () => {
await factory();
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
index 5b1502cc190..e420e28c7b6 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js
@@ -22,10 +22,6 @@ describe('ide/components/terminal_sync/terminal_sync_status_safe', () => {
beforeEach(createComponent);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with terminal sync module in store', () => {
beforeEach(() => {
store.registerModule('terminalSync', {
diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
index 147235abc8e..4541c3b5ec8 100644
--- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
+++ b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js
@@ -48,10 +48,6 @@ describe('ide/components/terminal_sync/terminal_sync_status', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when doing nothing', () => {
it('shows nothing', () => {
createComponent();
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index bfc87f17092..f8af8459025 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -20,12 +20,12 @@ const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
const TEST_PROJECT_PATH = 'group1/project1';
const TEST_BRANCH_NAME = '12345-foo-patch';
-const TEST_GITLAB_URL = 'https://test-gitlab/';
const TEST_USER_PREFERENCES_PATH = '/user/preferences';
const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path';
const TEST_FILE_PATH = 'foo/README.md';
const TEST_MR_ID = '7';
const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab';
+const TEST_SIGN_IN_PATH = 'sign-in';
const TEST_FORK_INFO = { fork_path: '/forky' };
const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path';
const TEST_START_REMOTE_PARAMS = {
@@ -56,6 +56,7 @@ describe('ide/init_gitlab_web_ide', () => {
el.dataset.editorFontSrcUrl = TEST_EDITOR_FONT_SRC_URL;
el.dataset.editorFontFormat = TEST_EDITOR_FONT_FORMAT;
el.dataset.editorFontFamily = TEST_EDITOR_FONT_FAMILY;
+ el.dataset.signInPath = TEST_SIGN_IN_PATH;
document.body.append(el);
};
@@ -69,7 +70,6 @@ describe('ide/init_gitlab_web_ide', () => {
beforeEach(() => {
process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH;
- window.gon.gitlab_url = TEST_GITLAB_URL;
confirmAction.mockImplementation(
() =>
@@ -100,7 +100,7 @@ describe('ide/init_gitlab_web_ide', () => {
mrId: TEST_MR_ID,
mrTargetProject: '',
forkInfo: null,
- gitlabUrl: TEST_GITLAB_URL,
+ gitlabUrl: TEST_HOST,
nonce: TEST_NONCE,
httpHeaders: {
'mock-csrf-header': 'mock-csrf-token',
@@ -109,6 +109,7 @@ describe('ide/init_gitlab_web_ide', () => {
links: {
userPreferences: TEST_USER_PREFERENCES_PATH,
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
+ signIn: TEST_SIGN_IN_PATH,
},
editorFont: {
srcUrl: TEST_EDITOR_FONT_SRC_URL,
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
index ed67a0948e4..3c42f54a1f7 100644
--- a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
+++ b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
@@ -2,14 +2,12 @@ import { getBaseConfig } from '~/ide/lib/gitlab_web_ide/get_base_config';
import { TEST_HOST } from 'helpers/test_constants';
const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/gitlab-web-ide/public/path';
-const TEST_GITLAB_URL = 'https://gdk.test/';
const TEST_RELATIVE_URL_ROOT = '/gl_rel_root';
describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
beforeEach(() => {
// why: add trailing "/" to test that it gets removed
process.env.GITLAB_WEB_IDE_PUBLIC_PATH = `${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}/`;
- window.gon.gitlab_url = TEST_GITLAB_URL;
window.gon.relative_url_root = '';
});
@@ -18,7 +16,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
expect(actual).toEqual({
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
- gitlabUrl: TEST_GITLAB_URL,
+ gitlabUrl: TEST_HOST,
});
});
@@ -27,8 +25,9 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
const actual = getBaseConfig();
- expect(actual).toMatchObject({
+ expect(actual).toEqual({
baseUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
+ gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}`,
});
});
});
diff --git a/spec/frontend/ide/lib/languages/codeowners_spec.js b/spec/frontend/ide/lib/languages/codeowners_spec.js
new file mode 100644
index 00000000000..aa0ab123c4b
--- /dev/null
+++ b/spec/frontend/ide/lib/languages/codeowners_spec.js
@@ -0,0 +1,85 @@
+import { editor } from 'monaco-editor';
+import codeowners from '~/ide/lib/languages/codeowners';
+import { registerLanguages } from '~/ide/utils';
+
+describe('tokenization for CODEOWNERS files', () => {
+ beforeEach(() => {
+ registerLanguages(codeowners);
+ });
+
+ it.each([
+ ['## Foo bar comment', [[{ language: 'codeowners', offset: 0, type: 'comment.codeowners' }]]],
+ [
+ '/foo/bar @gsamsa',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'regexp.codeowners' },
+ { language: 'codeowners', offset: 8, type: 'source.codeowners' },
+ { language: 'codeowners', offset: 9, type: 'variable.value.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '^[Section name]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'constant.numeric.codeowners' },
+ { language: 'codeowners', offset: 1, type: 'namespace.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '[Section name][3]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 14, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '[Section name][30]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 14, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '^[Section name][3]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'constant.numeric.codeowners' },
+ { language: 'codeowners', offset: 1, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 15, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '^[Section-name-test][3]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'constant.numeric.codeowners' },
+ { language: 'codeowners', offset: 1, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 20, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ [
+ '[Section-name_test]',
+ [[{ language: 'codeowners', offset: 0, type: 'namespace.codeowners' }]],
+ ],
+ [
+ '[2 Be or not 2 be][3]',
+ [
+ [
+ { language: 'codeowners', offset: 0, type: 'namespace.codeowners' },
+ { language: 'codeowners', offset: 18, type: 'constant.numeric.codeowners' },
+ ],
+ ],
+ ],
+ ])('%s', (string, tokens) => {
+ expect(editor.tokenize(string, 'codeowners')).toEqual(tokens);
+ });
+});
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 623dee387e5..cd099e60070 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -252,12 +252,10 @@ describe('IDE services', () => {
describe('pingUsage', () => {
let mock;
- let relativeUrlRoot;
const TEST_RELATIVE_URL_ROOT = 'blah-blah';
beforeEach(() => {
jest.spyOn(axios, 'post');
- relativeUrlRoot = gon.relative_url_root;
gon.relative_url_root = TEST_RELATIVE_URL_ROOT;
mock = new MockAdapter(axios);
@@ -265,7 +263,6 @@ describe('IDE services', () => {
afterEach(() => {
mock.restore();
- gon.relative_url_root = relativeUrlRoot;
});
it('posts to usage endpoint', () => {
diff --git a/spec/frontend/ide/services/terminals_spec.js b/spec/frontend/ide/services/terminals_spec.js
index 5f752197e13..5b6b60a250c 100644
--- a/spec/frontend/ide/services/terminals_spec.js
+++ b/spec/frontend/ide/services/terminals_spec.js
@@ -9,7 +9,6 @@ const TEST_BRANCH = 'ref';
describe('~/ide/services/terminals', () => {
let axiosSpy;
let mock;
- const prevRelativeUrlRoot = gon.relative_url_root;
beforeEach(() => {
axiosSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, {}]);
@@ -19,7 +18,6 @@ describe('~/ide/services/terminals', () => {
});
afterEach(() => {
- gon.relative_url_root = prevRelativeUrlRoot;
mock.restore();
});
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 90ca8526698..7f4e1cf761d 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -16,7 +16,6 @@ const RELATIVE_URL_ROOT = '/gitlab';
describe('IDE store file actions', () => {
let mock;
- let originalGon;
let store;
let router;
@@ -24,9 +23,7 @@ describe('IDE store file actions', () => {
stubPerformanceWebAPI();
mock = new MockAdapter(axios);
- originalGon = window.gon;
window.gon = {
- ...window.gon,
relative_url_root: RELATIVE_URL_ROOT,
};
@@ -44,7 +41,6 @@ describe('IDE store file actions', () => {
afterEach(() => {
mock.restore();
- window.gon = originalGon;
});
describe('closeFile', () => {
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index fbae84631ee..a41ffdb0a31 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -3,7 +3,7 @@ import { range } from 'lodash';
import { stubPerformanceWebAPI } from 'helpers/performance';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
@@ -30,7 +30,7 @@ const createMergeRequestChangesCount = (n) =>
const testGetUrlForPath = (path) => `${TEST_HOST}/test/${path}`;
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('IDE store merge request actions', () => {
let store;
@@ -135,7 +135,7 @@ describe('IDE store merge request actions', () => {
mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError();
});
- it('flashes message, if error', () => {
+ it('shows an alert, if error', () => {
return store
.dispatch('getMergeRequestsForBranch', {
projectId: TEST_PROJECT,
@@ -519,7 +519,7 @@ describe('IDE store merge request actions', () => {
);
});
- it('flashes message, if error', () => {
+ it('shows an alert, if error', () => {
store.dispatch.mockRejectedValue();
return openMergeRequest(store, mr).catch(() => {
diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js
index 5a5ead4c544..b13228c20f5 100644
--- a/spec/frontend/ide/stores/actions/project_spec.js
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import service from '~/ide/services';
import { createStore } from '~/ide/stores';
import {
@@ -19,7 +19,7 @@ import {
import { logError } from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/logger');
const TEST_PROJECT_ID = 'abc/def';
@@ -104,7 +104,7 @@ describe('IDE store project actions', () => {
desc | projectPath | responseSuccess | expectedMutations
${'does not fetch permissions if project does not exist'} | ${undefined} | ${true} | ${[]}
${'fetches permission when project is specified'} | ${TEST_PROJECT_ID} | ${true} | ${[...permissionsMutations]}
- ${'flashes an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]}
+ ${'alerts an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]}
`('$desc', async ({ projectPath, expectedMutations, responseSuccess } = {}) => {
store.state.currentProjectId = projectPath;
if (responseSuccess) {
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index 1c90c0f943a..f6925e78b6a 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -4,7 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import eventHub from '~/ide/eventhub';
import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
init,
stageAllChanges,
@@ -31,7 +31,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
}));
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Multi-file store actions', () => {
let store;
@@ -210,7 +210,7 @@ describe('Multi-file store actions', () => {
expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test');
});
- it('creates flash message if file already exists', async () => {
+ it('creates alert if file already exists', async () => {
const f = file('test', '1', 'blob');
store.state.trees['abcproject/mybranch'].tree = [f];
store.state.entries[f.path] = f;
@@ -440,7 +440,7 @@ describe('Multi-file store actions', () => {
});
describe('setErrorMessage', () => {
- it('commis error messsage', () => {
+ it('commis error message', () => {
return testAction(
setErrorMessage,
'error',
@@ -927,7 +927,7 @@ describe('Multi-file store actions', () => {
expect(document.querySelector('.flash-alert')).toBeNull();
});
- it('does not pass the error further and flashes an alert if error is not 404', async () => {
+ it('does not pass the error further and creates an alert if error is not 404', async () => {
mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_IM_A_TEAPOT);
await expect(getBranchData(...callParams)).rejects.toEqual(
diff --git a/spec/frontend/ide/stores/extend_spec.js b/spec/frontend/ide/stores/extend_spec.js
index ffb00f9ef5b..88909999c82 100644
--- a/spec/frontend/ide/stores/extend_spec.js
+++ b/spec/frontend/ide/stores/extend_spec.js
@@ -6,12 +6,10 @@ jest.mock('~/ide/stores/plugins/terminal', () => jest.fn());
jest.mock('~/ide/stores/plugins/terminal_sync', () => jest.fn());
describe('ide/stores/extend', () => {
- let prevGon;
let store;
let el;
beforeEach(() => {
- prevGon = global.gon;
store = {};
el = {};
@@ -23,13 +21,12 @@ describe('ide/stores/extend', () => {
});
afterEach(() => {
- global.gon = prevGon;
terminalPlugin.mockClear();
terminalSyncPlugin.mockClear();
});
const withGonFeatures = (features) => {
- global.gon = { ...global.gon, features };
+ global.gon.features = features;
};
describe('terminalPlugin', () => {
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index d4166a3bd6d..0fe6a16c676 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -24,11 +24,8 @@ const TEST_FORK_PATH = '/test/fork/path';
describe('IDE store getters', () => {
let localState;
let localStore;
- let origGon;
beforeEach(() => {
- origGon = window.gon;
-
// Feature flag is defaulted to on in prod
window.gon = { features: { rejectUnsignedCommitsByGitlab: true } };
@@ -36,10 +33,6 @@ describe('IDE store getters', () => {
localState = localStore.state;
});
- afterEach(() => {
- window.gon = origGon;
- });
-
describe('activeFile', () => {
it('returns the current active file', () => {
localState.openFiles.push(file());
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index 4068a9d0919..3eaff92d321 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -53,7 +53,6 @@ describe('IDE commit module actions', () => {
});
afterEach(() => {
- delete gon.api_version;
mock.restore();
});
@@ -81,19 +80,12 @@ describe('IDE commit module actions', () => {
});
describe('updateBranchName', () => {
- let originalGon;
-
beforeEach(() => {
- originalGon = window.gon;
- window.gon = { current_username: 'johndoe' };
+ window.gon.current_username = 'johndoe';
store.state.currentBranchId = 'main';
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('updates store with new branch name', async () => {
await store.dispatch('commit/updateBranchName', 'branch-name');
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
index 0287e5269ee..3f7ded5e718 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@@ -13,7 +13,7 @@ import {
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_PROJECT_PATH = 'lorem/root';
const TEST_BRANCH_ID = 'main';
@@ -91,7 +91,7 @@ describe('IDE store terminal session controls actions', () => {
});
describe('receiveStartSessionError', () => {
- it('flashes message', () => {
+ it('shows an alert', () => {
actions.receiveStartSessionError({ dispatch });
expect(createAlert).toHaveBeenCalledWith({
@@ -165,7 +165,7 @@ describe('IDE store terminal session controls actions', () => {
});
describe('receiveStopSessionError', () => {
- it('flashes message', () => {
+ it('shows an alert', () => {
actions.receiveStopSessionError({ dispatch });
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
index 9616733f052..30ae7d203a9 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
@@ -8,7 +8,7 @@ import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_SESSION = {
id: 7,
@@ -113,7 +113,7 @@ describe('IDE store terminal session controls actions', () => {
});
describe('receiveSessionStatusError', () => {
- it('flashes message', () => {
+ it('shows an alert', () => {
actions.receiveSessionStatusError({ dispatch });
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/import/details/components/import_details_app_spec.js b/spec/frontend/import/details/components/import_details_app_spec.js
new file mode 100644
index 00000000000..6e748a57a1d
--- /dev/null
+++ b/spec/frontend/import/details/components/import_details_app_spec.js
@@ -0,0 +1,18 @@
+import { shallowMount } from '@vue/test-utils';
+import ImportDetailsApp from '~/import/details/components/import_details_app.vue';
+
+describe('Import details app', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(ImportDetailsApp);
+ };
+
+ describe('template', () => {
+ it('renders heading', () => {
+ createComponent();
+
+ expect(wrapper.find('h1').text()).toBe(ImportDetailsApp.i18n.pageTitle);
+ });
+ });
+});
diff --git a/spec/frontend/import/details/components/import_details_table_spec.js b/spec/frontend/import/details/components/import_details_table_spec.js
new file mode 100644
index 00000000000..aee8573eb02
--- /dev/null
+++ b/spec/frontend/import/details/components/import_details_table_spec.js
@@ -0,0 +1,113 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
+import { createAlert } from '~/alert';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
+import ImportDetailsTable from '~/import/details/components/import_details_table.vue';
+import { mockImportFailures, mockHeaders } from '../mock_data';
+
+jest.mock('~/alert');
+
+describe('Import details table', () => {
+ let wrapper;
+ let mock;
+
+ const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => {
+ wrapper = mountFn(ImportDetailsTable, {
+ provide,
+ });
+ };
+
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findGlTable = () => wrapper.findComponent(GlTable);
+ const findGlTableRows = () => findGlTable().find('tbody').findAll('tr');
+ const findGlEmptyState = () => findGlTable().findComponent(GlEmptyState);
+ const findPaginationBar = () => wrapper.findComponent(PaginationBar);
+
+ describe('template', () => {
+ describe('when no items are available', () => {
+ it('renders table with empty state', () => {
+ createComponent({ mountFn: mount });
+
+ expect(findGlEmptyState().text()).toBe(ImportDetailsTable.i18n.emptyText);
+ });
+
+ it('does not render pagination', () => {
+ createComponent();
+
+ expect(findPaginationBar().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('fetching failures from API', () => {
+ const mockImportFailuresPath = '/failures';
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('when request is successful', () => {
+ beforeEach(() => {
+ mock.onGet(mockImportFailuresPath).reply(HTTP_STATUS_OK, mockImportFailures, mockHeaders);
+
+ createComponent({
+ mountFn: mount,
+ provide: {
+ failuresPath: mockImportFailuresPath,
+ },
+ });
+ });
+
+ it('renders loading icon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render loading icon after fetch', async () => {
+ await waitForPromises();
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+
+ it('sets items and pagination info', async () => {
+ await waitForPromises();
+
+ expect(findGlTableRows().length).toBe(mockImportFailures.length);
+ expect(findPaginationBar().props('pageInfo')).toMatchObject({
+ page: mockHeaders['x-page'],
+ perPage: mockHeaders['x-per-page'],
+ total: mockHeaders['x-total'],
+ totalPages: mockHeaders['x-total-pages'],
+ });
+ });
+ });
+
+ describe('when request fails', () => {
+ beforeEach(() => {
+ mock.onGet(mockImportFailuresPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ createComponent({
+ provide: {
+ failuresPath: mockImportFailuresPath,
+ },
+ });
+ });
+
+ it('displays an error', async () => {
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: ImportDetailsTable.i18n.fetchErrorMessage,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import/details/mock_data.js b/spec/frontend/import/details/mock_data.js
new file mode 100644
index 00000000000..67148173404
--- /dev/null
+++ b/spec/frontend/import/details/mock_data.js
@@ -0,0 +1,53 @@
+export const mockImportFailures = [
+ {
+ type: 'pull_request',
+ title: 'Add one cool feature',
+ provider_url: 'https://github.com/USER/REPO/pull/2',
+ details: {
+ exception_class: 'ActiveRecord::RecordInvalid',
+ exception_message: 'Record invalid',
+ source: 'Gitlab::GithubImport::Importer::PullRequestImporter',
+ github_identifiers: {
+ iid: 2,
+ issuable_type: 'MergeRequest',
+ object_type: 'pull_request',
+ },
+ },
+ },
+ {
+ type: 'pull_request',
+ title: 'Add another awesome feature',
+ provider_url: 'https://github.com/USER/REPO/pull/3',
+ details: {
+ exception_class: 'ActiveRecord::RecordInvalid',
+ exception_message: 'Record invalid',
+ source: 'Gitlab::GithubImport::Importer::PullRequestImporter',
+ github_identifiers: {
+ iid: 3,
+ issuable_type: 'MergeRequest',
+ object_type: 'pull_request',
+ },
+ },
+ },
+ {
+ type: 'lfs_object',
+ title: '3a9257fae9e86faee27d7208cb55e086f18e6f29f48c430bfbc26d42eb',
+ provider_url: null,
+ details: {
+ exception_class: 'NameError',
+ exception_message: 'some message',
+ source: 'Gitlab::GithubImport::Importer::LfsObjectImporter',
+ github_identifiers: {
+ oid: '3a9257fae9e86faee27d7208cb55e086f18e6f29f48c430bfbc26d42eb',
+ size: 2473979,
+ },
+ },
+ },
+];
+
+export const mockHeaders = {
+ 'x-page': 1,
+ 'x-per-page': 20,
+ 'x-total': 3,
+ 'x-total-pages': 1,
+};
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
index 31e097cfa7b..14f39a35387 100644
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/group_dropdown_spec.js
@@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import GroupDropdown from '~/import_entities/components/group_dropdown.vue';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
Vue.use(VueApollo);
@@ -49,7 +49,7 @@ describe('Import entities group dropdown component', () => {
const createComponent = (propsData) => {
const apolloProvider = createMockApollo([
- [searchNamespacesWhereUserCanCreateProjectsQuery, () => SEARCH_NAMESPACES_MOCK],
+ [searchNamespacesWhereUserCanImportProjectsQuery, () => SEARCH_NAMESPACES_MOCK],
]);
namespacesTracker = jest.fn();
@@ -64,10 +64,6 @@ describe('Import entities group dropdown component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('passes namespaces from graphql query to default slot', async () => {
createComponent();
jest.advanceTimersByTime(DEBOUNCE_DELAY);
diff --git a/spec/frontend/import_entities/components/import_status_spec.js b/spec/frontend/import_entities/components/import_status_spec.js
index 56c4ed827d7..4c6fee35389 100644
--- a/spec/frontend/import_entities/components/import_status_spec.js
+++ b/spec/frontend/import_entities/components/import_status_spec.js
@@ -1,4 +1,4 @@
-import { GlAccordionItem, GlBadge, GlIcon } from '@gitlab/ui';
+import { GlAccordionItem, GlBadge, GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ImportStatus from '~/import_entities/components/import_status.vue';
import { STATUSES } from '~/import_entities/constants';
@@ -6,15 +6,16 @@ import { STATUSES } from '~/import_entities/constants';
describe('Import entities status component', () => {
let wrapper;
- const createComponent = (propsData) => {
+ const mockStatItems = { label: 100, note: 200 };
+
+ const createComponent = (propsData, { provide } = {}) => {
wrapper = shallowMount(ImportStatus, {
propsData,
+ provide,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
+ const findGlLink = () => wrapper.findComponent(GlLink);
describe('success status', () => {
const getStatusText = () => wrapper.findComponent(GlBadge).text();
@@ -28,13 +29,11 @@ describe('Import entities status component', () => {
});
it('displays finished status as complete when all stats items were processed', () => {
- const statItems = { label: 100, note: 200 };
-
createComponent({
status: STATUSES.FINISHED,
stats: {
- fetched: { ...statItems },
- imported: { ...statItems },
+ fetched: { ...mockStatItems },
+ imported: { ...mockStatItems },
},
});
@@ -43,17 +42,15 @@ describe('Import entities status component', () => {
});
it('displays finished status as partial when all stats items were processed', () => {
- const statItems = { label: 100, note: 200 };
-
createComponent({
status: STATUSES.FINISHED,
stats: {
- fetched: { ...statItems },
- imported: { ...statItems, label: 50 },
+ fetched: { ...mockStatItems },
+ imported: { ...mockStatItems, label: 50 },
},
});
- expect(getStatusText()).toBe('Partial import');
+ expect(getStatusText()).toBe('Partially completed');
expect(getStatusIcon()).toBe('status-alert');
});
});
@@ -155,4 +152,57 @@ describe('Import entities status component', () => {
expect(getStatusIcon()).toBe('status-success');
});
});
+
+ describe('show details link', () => {
+ const mockDetailsPath = 'details_path';
+ const mockProjectId = 29;
+ const mockCompleteStats = {
+ fetched: { ...mockStatItems },
+ imported: { ...mockStatItems },
+ };
+ const mockIncompleteStats = {
+ fetched: { ...mockStatItems },
+ imported: { ...mockStatItems, label: 50 },
+ };
+
+ describe.each`
+ detailsPath | importDetailsPage | partialImport | expectLink
+ ${undefined} | ${false} | ${false} | ${false}
+ ${undefined} | ${false} | ${true} | ${false}
+ ${undefined} | ${true} | ${false} | ${false}
+ ${undefined} | ${true} | ${true} | ${false}
+ ${mockDetailsPath} | ${false} | ${false} | ${false}
+ ${mockDetailsPath} | ${false} | ${true} | ${false}
+ ${mockDetailsPath} | ${true} | ${false} | ${false}
+ ${mockDetailsPath} | ${true} | ${true} | ${true}
+ `(
+ 'when detailsPath is $detailsPath, feature flag importDetailsPage is $importDetailsPage, partial import is $partialImport',
+ ({ detailsPath, importDetailsPage, partialImport, expectLink }) => {
+ beforeEach(() => {
+ createComponent(
+ {
+ projectId: mockProjectId,
+ status: STATUSES.FINISHED,
+ stats: partialImport ? mockIncompleteStats : mockCompleteStats,
+ },
+ {
+ provide: {
+ detailsPath,
+ glFeatures: { importDetailsPage },
+ },
+ },
+ );
+ });
+
+ it(`${expectLink ? 'renders' : 'does not render'} import details link`, () => {
+ expect(findGlLink().exists()).toBe(expectLink);
+ if (expectLink) {
+ expect(findGlLink().attributes('href')).toBe(
+ `${mockDetailsPath}?project_id=${mockProjectId}`,
+ );
+ }
+ });
+ },
+ );
+ });
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
index 163a60bae36..4c13ec555c2 100644
--- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDropdown, GlIcon, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
@@ -8,7 +8,6 @@ describe('import actions cell', () => {
const createComponent = (props) => {
wrapper = shallowMount(ImportActionsCell, {
propsData: {
- isProjectsImportEnabled: false,
isFinished: false,
isAvailableForImport: false,
isInvalid: false,
@@ -17,19 +16,15 @@ describe('import actions cell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when group is available for import', () => {
beforeEach(() => {
createComponent({ isAvailableForImport: true });
});
- it('renders import button', () => {
- const button = wrapper.findComponent(GlButton);
- expect(button.exists()).toBe(true);
- expect(button.text()).toBe('Import');
+ it('renders import dropdown', () => {
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.props('text')).toBe('Import with projects');
});
it('does not render icon with a hint', () => {
@@ -42,10 +37,10 @@ describe('import actions cell', () => {
createComponent({ isAvailableForImport: false, isFinished: true });
});
- it('renders re-import button', () => {
- const button = wrapper.findComponent(GlButton);
- expect(button.exists()).toBe(true);
- expect(button.text()).toBe('Re-import');
+ it('renders re-import dropdown', () => {
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.props('text')).toBe('Re-import with projects');
});
it('renders icon with a hint', () => {
@@ -57,25 +52,25 @@ describe('import actions cell', () => {
});
});
- it('does not render import button when group is not available for import', () => {
+ it('does not render import dropdown when group is not available for import', () => {
createComponent({ isAvailableForImport: false });
- const button = wrapper.findComponent(GlButton);
- expect(button.exists()).toBe(false);
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.exists()).toBe(false);
});
- it('renders import button as disabled when group is invalid', () => {
+ it('renders import dropdown as disabled when group is invalid', () => {
createComponent({ isInvalid: true, isAvailableForImport: true });
- const button = wrapper.findComponent(GlButton);
- expect(button.props().disabled).toBe(true);
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.props().disabled).toBe(true);
});
it('emits import-group event when import button is clicked', () => {
createComponent({ isAvailableForImport: true });
- const button = wrapper.findComponent(GlButton);
- button.vm.$emit('click');
+ const dropdown = wrapper.findComponent(GlDropdown);
+ dropdown.vm.$emit('click');
expect(wrapper.emitted('import-group')).toHaveLength(1);
});
@@ -85,10 +80,10 @@ describe('import actions cell', () => {
${false} | ${'Import'}
${true} | ${'Re-import'}
`(
- 'when import projects is enabled, group is available for import and finish status is $status',
+ 'group is available for import and finish status is $isFinished',
({ isFinished, expectedAction }) => {
beforeEach(() => {
- createComponent({ isProjectsImportEnabled: true, isAvailableForImport: true, isFinished });
+ createComponent({ isAvailableForImport: true, isFinished });
});
it('render import dropdown', () => {
@@ -99,14 +94,14 @@ describe('import actions cell', () => {
);
});
- it('request migrate projects by default', async () => {
+ it('request migrate projects by default', () => {
const dropdown = wrapper.findComponent(GlDropdown);
dropdown.vm.$emit('click');
expect(wrapper.emitted('import-group')[0]).toStrictEqual([{ migrateProjects: true }]);
});
- it('request not to migrate projects via dropdown option', async () => {
+ it('request not to migrate projects via dropdown option', () => {
const dropdown = wrapper.findComponent(GlDropdown);
dropdown.findComponent(GlDropdownItem).vm.$emit('click');
diff --git a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
index f2735d86493..9ead483d02f 100644
--- a/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_source_cell_spec.js
@@ -22,10 +22,6 @@ describe('import source cell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when group status is NONE', () => {
beforeEach(() => {
group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
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 c7bda5a60ec..dae5671777c 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
@@ -6,7 +6,7 @@ import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_OK, HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
@@ -15,7 +15,7 @@ import ImportTable from '~/import_entities/import_groups/components/import_table
import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
import {
AVAILABLE_NAMESPACES,
@@ -23,7 +23,7 @@ import {
generateFakeEntry,
} from '../graphql/fixtures';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/import_entities/import_groups/services/status_poller');
Vue.use(VueApollo);
@@ -49,12 +49,12 @@ describe('import table', () => {
},
};
- const findImportSelectedButton = () =>
- wrapper.findAll('button').wrappers.find((w) => w.text() === 'Import selected');
const findImportSelectedDropdown = () =>
- wrapper.findAll('.gl-dropdown').wrappers.find((w) => w.text().includes('Import with projects'));
- const findImportButtons = () =>
- wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import');
+ wrapper.find('[data-testid="import-selected-groups-dropdown"]');
+ const findRowImportDropdownAtIndex = (idx) =>
+ wrapper.findAll('tbody td button').wrappers.filter((w) => w.text() === 'Import with projects')[
+ idx
+ ];
const findPaginationDropdown = () => wrapper.find('[data-testid="page-size"]');
const findTargetNamespaceDropdown = (rowWrapper) =>
rowWrapper.find('[data-testid="target-namespace-selector"]');
@@ -70,16 +70,11 @@ describe('import table', () => {
const findRowCheckbox = (idx) => wrapper.findAll('tbody td input[type=checkbox]').at(idx);
const selectRow = (idx) => findRowCheckbox(idx).setChecked(true);
- const createComponent = ({
- bulkImportSourceGroups,
- importGroups,
- defaultTargetNamespace,
- glFeatures = {},
- }) => {
+ const createComponent = ({ bulkImportSourceGroups, importGroups, defaultTargetNamespace }) => {
apolloProvider = createMockApollo(
[
[
- searchNamespacesWhereUserCanCreateProjectsQuery,
+ searchNamespacesWhereUserCanImportProjectsQuery,
() => Promise.resolve(availableNamespacesFixture),
],
],
@@ -102,10 +97,7 @@ describe('import table', () => {
defaultTargetNamespace,
},
directives: {
- GlTooltip: createMockDirective(),
- },
- provide: {
- glFeatures,
+ GlTooltip: createMockDirective('gl-tooltip'),
},
apolloProvider,
});
@@ -120,10 +112,6 @@ describe('import table', () => {
axiosMock.onGet(/.*\/exists$/, () => []).reply(HTTP_STATUS_OK, { exists: false });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('renders loading icon while performing request', async () => {
createComponent({
@@ -134,7 +122,7 @@ describe('import table', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
- it('does not renders loading icon when request is completed', async () => {
+ it('does not render loading icon when request is completed', async () => {
createComponent({
bulkImportSourceGroups: () => [],
});
@@ -245,12 +233,13 @@ describe('import table', () => {
await waitForPromises();
- await findImportButtons()[0].trigger('click');
+ await findRowImportDropdownAtIndex(0).trigger('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
variables: {
importRequests: [
{
+ migrateProjects: true,
newName: FAKE_GROUP.lastImportTarget.newName,
sourceGroupId: FAKE_GROUP.id,
targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
@@ -273,7 +262,7 @@ describe('import table', () => {
});
await waitForPromises();
- await findImportButtons()[0].trigger('click');
+ await findRowImportDropdownAtIndex(0).trigger('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith(
@@ -298,7 +287,7 @@ describe('import table', () => {
});
await waitForPromises();
- await findImportButtons()[0].trigger('click');
+ await findRowImportDropdownAtIndex(0).trigger('click');
await waitForPromises();
expect(createAlert).not.toHaveBeenCalled();
@@ -476,7 +465,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(findImportSelectedButton().props().disabled).toBe(true);
+ expect(findImportSelectedDropdown().props().disabled).toBe(true);
});
it('import selected button is enabled when groups were selected for import', async () => {
@@ -491,7 +480,7 @@ describe('import table', () => {
await selectRow(0);
- expect(findImportSelectedButton().props().disabled).toBe(false);
+ expect(findImportSelectedDropdown().props().disabled).toBe(false);
});
it('does not allow selecting already started groups', async () => {
@@ -509,7 +498,7 @@ describe('import table', () => {
await selectRow(0);
await nextTick();
- expect(findImportSelectedButton().props().disabled).toBe(true);
+ expect(findImportSelectedDropdown().props().disabled).toBe(true);
});
it('does not allow selecting groups with validation errors', async () => {
@@ -534,10 +523,10 @@ describe('import table', () => {
await selectRow(0);
await nextTick();
- expect(findImportSelectedButton().props().disabled).toBe(true);
+ expect(findImportSelectedDropdown().props().disabled).toBe(true);
});
- it('invokes importGroups mutation when import selected button is clicked', async () => {
+ it('invokes importGroups mutation when import selected dropdown is clicked', async () => {
const NEW_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.NONE }),
@@ -558,7 +547,7 @@ describe('import table', () => {
await selectRow(1);
await nextTick();
- await findImportSelectedButton().trigger('click');
+ await findImportSelectedDropdown().find('button').trigger('click');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: importGroupsMutation,
@@ -679,7 +668,7 @@ describe('import table', () => {
});
});
- describe('when import projects is enabled', () => {
+ describe('importing projects', () => {
const NEW_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.NONE }),
@@ -693,15 +682,12 @@ describe('import table', () => {
pageInfo: FAKE_PAGE_INFO,
versionValidation: FAKE_VERSION_VALIDATION,
}),
- glFeatures: {
- bulkImportProjects: true,
- },
});
jest.spyOn(apolloProvider.defaultClient, 'mutate');
return waitForPromises();
});
- it('renders import all dropdown', async () => {
+ it('renders import all dropdown', () => {
expect(findImportSelectedDropdown().exists()).toBe(true);
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index d5286e71c44..46884a42707 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -8,7 +8,7 @@ import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
import {
generateFakeEntry,
@@ -42,7 +42,7 @@ describe('import target cell', () => {
const createComponent = (props) => {
apolloProvider = createMockApollo([
[
- searchNamespacesWhereUserCanCreateProjectsQuery,
+ searchNamespacesWhereUserCanImportProjectsQuery,
() => Promise.resolve(availableNamespacesFixture),
],
]);
@@ -57,11 +57,6 @@ describe('import target cell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('events', () => {
beforeEach(async () => {
group = generateFakeTableEntry({ id: 1, status: STATUSES.NONE });
@@ -146,7 +141,7 @@ describe('import target cell', () => {
});
it('renders namespace dropdown as disabled', () => {
- expect(findNamespaceDropdown().attributes('disabled')).toBe('true');
+ expect(findNamespaceDropdown().attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index ce111a0c10c..540c42a2854 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -16,7 +16,7 @@ import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { statusEndpointFixture } from './fixtures';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/import_entities/import_groups/graphql/services/local_storage_cache', () => ({
LocalStorageCache: jest.fn().mockImplementation(function mock() {
this.get = jest.fn();
@@ -48,7 +48,7 @@ describe('Bulk import resolvers', () => {
};
let results;
- beforeEach(async () => {
+ beforeEach(() => {
axiosMockAdapter = new MockAdapter(axios);
client = createClient();
diff --git a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
index 4a1b85d24e3..e1ed739a708 100644
--- a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import Visibility from 'visibilityjs';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { STATUSES } from '~/import_entities/constants';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import axios from '~/lib/utils/axios_utils';
@@ -8,7 +8,7 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
jest.mock('visibilityjs');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/poll');
const FAKE_POLL_PATH = '/fake/poll/path';
@@ -81,7 +81,7 @@ describe('Bulk import status poller', () => {
expect(pollInstance.makeRequest).toHaveBeenCalled();
});
- it('when error occurs shows flash with error', () => {
+ it('when error occurs shows an alert with error', () => {
const [[pollConfig]] = Poll.mock.calls;
pollConfig.errorCallback();
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js
index 68716600592..29af6dc946f 100644
--- a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js
@@ -5,8 +5,8 @@ import AdvancedSettingsPanel from '~/import_entities/import_projects/components/
describe('Import Advanced Settings', () => {
let wrapper;
const OPTIONAL_STAGES = [
- { name: 'stage1', label: 'Stage 1' },
- { name: 'stage2', label: 'Stage 2', details: 'Extra details' },
+ { name: 'stage1', label: 'Stage 1', selected: false },
+ { name: 'stage2', label: 'Stage 2', details: 'Extra details', selected: false },
];
const createComponent = () => {
@@ -25,10 +25,6 @@ describe('Import Advanced Settings', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders GLFormCheckbox for each optional stage', () => {
expect(wrapper.findAllComponents(GlFormCheckbox)).toHaveLength(OPTIONAL_STAGES.length);
});
diff --git a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
index 9eae4ed974e..246c6499a97 100644
--- a/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/bitbucket_status_table_spec.js
@@ -14,18 +14,11 @@ const ImportProjectsTableStub = {
describe('BitbucketStatusTable', () => {
let wrapper;
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
- function createComponent(propsData, importProjectsTableStub = true, slots) {
+ function createComponent(propsData, slots) {
wrapper = shallowMount(BitbucketStatusTable, {
propsData,
stubs: {
- ImportProjectsTable: importProjectsTableStub,
+ ImportProjectsTable: ImportProjectsTableStub,
},
slots,
});
@@ -37,20 +30,23 @@ describe('BitbucketStatusTable', () => {
});
it('passes alert in incompatible-repos-warning slot', () => {
- createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
+ createComponent({ providerTitle: 'Test' });
expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
});
it('passes actions slot to import project table component', () => {
const actionsSlotContent = 'DEMO';
- createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, {
- actions: actionsSlotContent,
- });
+ createComponent(
+ { providerTitle: 'Test' },
+ {
+ actions: actionsSlotContent,
+ },
+ );
expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent);
});
it('dismisses alert when requested', async () => {
- createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub);
+ createComponent({ providerTitle: 'Test' });
wrapper.findComponent(GlAlert).vm.$emit('dismiss');
await nextTick();
diff --git a/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
new file mode 100644
index 00000000000..b6f96cd6a23
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/components/github_organizations_box_spec.js
@@ -0,0 +1,97 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { mount } from '@vue/test-utils';
+import { captureException } from '@sentry/browser';
+import { nextTick } from 'vue';
+
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { createAlert } from '~/alert';
+
+import GithubOrganizationsBox from '~/import_entities/import_projects/components/github_organizations_box.vue';
+
+jest.mock('@sentry/browser');
+jest.mock('~/alert');
+
+const MOCK_RESPONSE = {
+ provider_groups: [{ name: 'alpha-1' }, { name: 'alpha-2' }, { name: 'beta-1' }],
+};
+
+describe('GithubOrganizationsBox component', () => {
+ let wrapper;
+ let mockAxios;
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const mockGithubGroupPath = '/mock/groups.json';
+
+ const createComponent = (props) => {
+ wrapper = mount(GithubOrganizationsBox, {
+ propsData: {
+ value: 'some-org',
+ ...props,
+ },
+ provide: () => ({
+ statusImportGithubGroupPath: mockGithubGroupPath,
+ }),
+ });
+ };
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_OK, MOCK_RESPONSE);
+ });
+
+ afterEach(() => {
+ mockAxios.restore();
+ });
+
+ it('has underlying listbox as loading while loading organizations', () => {
+ createComponent();
+ expect(findListbox().props('loading')).toBe(true);
+ });
+
+ it('clears underlying listbox when loading is complete', async () => {
+ createComponent();
+ await axios.waitForAll();
+ expect(findListbox().props('loading')).toBe(false);
+ });
+
+ it('sets toggle-text to all organizations when selection is not provided', () => {
+ createComponent({ value: '' });
+ expect(findListbox().props('toggleText')).toBe(GithubOrganizationsBox.i18n.allOrganizations);
+ });
+
+ it('sets toggle-text to organization name when it is provided', () => {
+ const ORG_NAME = 'org';
+ createComponent({ value: ORG_NAME });
+
+ expect(findListbox().props('toggleText')).toBe(ORG_NAME);
+ });
+
+ it('emits selected organization from underlying listbox', () => {
+ createComponent();
+
+ findListbox().vm.$emit('select', 'org-id');
+ expect(wrapper.emitted('input').at(-1)).toStrictEqual(['org-id']);
+ });
+
+ it('filters list for underlying listbox', async () => {
+ createComponent();
+ await axios.waitForAll();
+
+ findListbox().vm.$emit('search', 'alpha');
+ await nextTick();
+
+ // 2 matches + 'All organizations'
+ expect(findListbox().props('items')).toHaveLength(3);
+ });
+
+ it('reports error to sentry on load', async () => {
+ mockAxios.onGet(mockGithubGroupPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+ await axios.waitForAll();
+
+ expect(captureException).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js b/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js
new file mode 100644
index 00000000000..7eebff7364c
--- /dev/null
+++ b/spec/frontend/import_entities/import_projects/components/github_status_table_spec.js
@@ -0,0 +1,125 @@
+import { GlTabs, GlSearchBoxByClick } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+
+import { stubComponent } from 'helpers/stub_component';
+import GithubStatusTable from '~/import_entities/import_projects/components/github_status_table.vue';
+import GithubOrganizationsBox from '~/import_entities/import_projects/components/github_organizations_box.vue';
+import ImportProjectsTable from '~/import_entities/import_projects/components/import_projects_table.vue';
+import initialState from '~/import_entities/import_projects/store/state';
+import * as getters from '~/import_entities/import_projects/store/getters';
+
+const ImportProjectsTableStub = stubComponent(ImportProjectsTable, {
+ importAllButtonText: 'IMPORT_ALL_TEXT',
+ methods: {
+ showImportAllModal: jest.fn(),
+ },
+ template:
+ '<div><slot name="filter" v-bind="{ importAllButtonText: $options.importAllButtonText, showImportAllModal }"></slot></div>',
+});
+
+Vue.use(Vuex);
+
+describe('GithubStatusTable', () => {
+ let wrapper;
+
+ const setFilterAction = jest.fn().mockImplementation(({ state }, filter) => {
+ state.filter = { ...state.filter, ...filter };
+ });
+
+ const findFilterField = () => wrapper.findComponent(GlSearchBoxByClick);
+ const selectTab = (idx) => {
+ wrapper.findComponent(GlTabs).vm.$emit('input', idx);
+ return nextTick();
+ };
+
+ function createComponent() {
+ const store = new Vuex.Store({
+ state: { ...initialState() },
+ getters,
+ actions: {
+ setFilter: setFilterAction,
+ },
+ });
+
+ wrapper = mount(GithubStatusTable, {
+ store,
+ propsData: {
+ providerTitle: 'Github',
+ },
+ stubs: {
+ ImportProjectsTable: ImportProjectsTableStub,
+ GithubOrganizationsBox: stubComponent(GithubOrganizationsBox),
+ GlTabs: false,
+ GlTab: false,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders import table component', () => {
+ expect(wrapper.findComponent(ImportProjectsTable).exists()).toBe(true);
+ });
+
+ it('sets relation_type filter to owned repositories by default', () => {
+ expect(setFilterAction).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ relation_type: 'owned' }),
+ );
+ });
+
+ it('updates relation_type and resets organization filter when tab is switched', async () => {
+ const NEW_ACTIVE_TAB_IDX = 1;
+ await selectTab(NEW_ACTIVE_TAB_IDX);
+
+ expect(setFilterAction).toHaveBeenCalledTimes(2);
+ expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
+ ...GithubStatusTable.relationTypes[NEW_ACTIVE_TAB_IDX].backendFilter,
+ organization_login: '',
+ filter: '',
+ });
+ });
+
+ it('renders name filter disabled when tab with organization filter is selected and organization is not set', async () => {
+ const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex(
+ (entry) => entry.showOrganizationFilter,
+ );
+ await selectTab(NEW_ACTIVE_TAB_IDX);
+ expect(findFilterField().props('disabled')).toBe(true);
+ });
+
+ it('enables name filter disabled when organization is set', async () => {
+ const NEW_ACTIVE_TAB_IDX = GithubStatusTable.relationTypes.findIndex(
+ (entry) => entry.showOrganizationFilter,
+ );
+ await selectTab(NEW_ACTIVE_TAB_IDX);
+
+ wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', 'some-org');
+ await nextTick();
+
+ expect(findFilterField().props('disabled')).toBe(false);
+ });
+
+ it('updates filter when search box is changed', async () => {
+ const NEW_FILTER = 'test';
+ findFilterField().vm.$emit('submit', NEW_FILTER);
+ await nextTick();
+
+ expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
+ filter: NEW_FILTER,
+ });
+ });
+
+ it('updates organization_login filter when GithubOrganizationsBox emits input', () => {
+ const NEW_ORG = 'some-org';
+ wrapper.findComponent(GithubOrganizationsBox).vm.$emit('input', NEW_ORG);
+
+ expect(setFilterAction).toHaveBeenCalledWith(expect.anything(), {
+ organization_login: NEW_ORG,
+ });
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
index 51f82dab381..351bbe5ea28 100644
--- a/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/import_projects_table_spec.js
@@ -81,13 +81,6 @@ describe('ImportProjectsTable', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
it('renders a loading icon while repos are loading', () => {
createComponent({ state: { isLoadingRepos: true } });
@@ -292,7 +285,7 @@ describe('ImportProjectsTable', () => {
});
it('should render advanced settings panel when no optional steps are passed', () => {
- const OPTIONAL_STAGES = [{ name: 'step1', label: 'Step 1' }];
+ const OPTIONAL_STAGES = [{ name: 'step1', label: 'Step 1', selected: true }];
createComponent({ state: { providerRepos: [providerRepo] }, optionalStages: OPTIONAL_STAGES });
expect(wrapper.findComponent(AdvancedSettingsPanel).exists()).toBe(true);
@@ -300,7 +293,7 @@ describe('ImportProjectsTable', () => {
OPTIONAL_STAGES,
);
expect(wrapper.findComponent(AdvancedSettingsPanel).props('value')).toStrictEqual({
- step1: false,
+ step1: true,
});
});
});
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index e613b9756af..57e232a4c46 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -40,6 +40,7 @@ describe('ProviderRepoTableRow', () => {
const findImportButton = () => findButton('Import');
const findReimportButton = () => findButton('Re-import');
const findGroupDropdown = () => wrapper.findComponent(ImportGroupDropdown);
+ const findImportStatus = () => wrapper.findComponent(ImportStatus);
const findCancelButton = () => {
const buttons = wrapper
@@ -60,11 +61,6 @@ describe('ProviderRepoTableRow', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when rendering importable project', () => {
const repo = {
importSource: {
@@ -86,7 +82,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders empty import status', () => {
- expect(wrapper.findComponent(ImportStatus).props().status).toBe(STATUSES.NONE);
+ expect(findImportStatus().props().status).toBe(STATUSES.NONE);
});
it('renders a group namespace select', () => {
@@ -203,9 +199,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders proper import status', () => {
- expect(wrapper.findComponent(ImportStatus).props().status).toBe(
- repo.importedProject.importStatus,
- );
+ expect(findImportStatus().props().status).toBe(repo.importedProject.importStatus);
});
it('does not render a namespace select', () => {
@@ -241,8 +235,11 @@ describe('ProviderRepoTableRow', () => {
});
});
- it('passes stats to import status component', () => {
- expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS);
+ it('passes props to import status component', () => {
+ expect(findImportStatus().props()).toMatchObject({
+ projectId: repo.importedProject.id,
+ stats: FAKE_STATS,
+ });
});
});
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index 990587d4af7..3b94db37801 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { STATUSES, PROVIDERS } from '~/import_entities/constants';
import actionsFactory from '~/import_entities/import_projects/store/actions';
import { getImportTarget } from '~/import_entities/import_projects/store/getters';
@@ -27,7 +27,7 @@ import {
HTTP_STATUS_TOO_MANY_REQUESTS,
} from '~/lib/utils/http_status';
-jest.mock('~/flash');
+jest.mock('~/alert');
const MOCK_ENDPOINT = `${TEST_HOST}/endpoint.json`;
const endpoints = {
@@ -220,12 +220,14 @@ describe('import_projects store actions', () => {
describe('when rate limited', () => {
it('commits RECEIVE_REPOS_ERROR and shows rate limited error message', async () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_TOO_MANY_REQUESTS);
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json?filtered_field=filter`)
+ .reply(HTTP_STATUS_TOO_MANY_REQUESTS);
await testAction(
fetchRepos,
null,
- { ...localState, filter: 'filter' },
+ { ...localState, filter: { filtered_field: 'filter' } },
[{ type: REQUEST_REPOS }, { type: RECEIVE_REPOS_ERROR }],
[],
);
@@ -238,12 +240,12 @@ describe('import_projects store actions', () => {
describe('when filtered', () => {
it('fetches repos with filter applied', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_OK, payload);
+ mock.onGet(`${TEST_HOST}/endpoint.json?some_filter=filter`).reply(HTTP_STATUS_OK, payload);
return testAction(
fetchRepos,
null,
- { ...localState, filter: 'filter' },
+ { ...localState, filter: { some_filter: 'filter' } },
[
{ type: REQUEST_REPOS },
{ type: SET_PAGE, payload: 1 },
@@ -374,12 +376,12 @@ describe('import_projects store actions', () => {
describe('when filtered', () => {
beforeEach(() => {
- localState.filter = 'filter';
+ localState.filter = { some_filter: 'filter' };
});
it('fetches realtime changes with filter applied', () => {
mock
- .onGet(`${TEST_HOST}/endpoint.json?filter=filter`)
+ .onGet(`${TEST_HOST}/endpoint.json?some_filter=filter`)
.reply(HTTP_STATUS_OK, updatedProjects);
return testAction(
diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
index 514a168553a..07d247630cc 100644
--- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -25,7 +25,7 @@ describe('import_projects store mutations', () => {
beforeEach(() => {
state = {
- filter: 'some-value',
+ filter: { someField: 'some-value' },
repositories: ['some', ' repositories'],
pageInfo: {
page: 1,
@@ -47,6 +47,17 @@ describe('import_projects store mutations', () => {
expect(state.pageInfo.endCursor).toBe(null);
expect(state.pageInfo.hasNextPage).toBe(true);
});
+
+ it('merges filter updates', () => {
+ const originalFilter = { ...state.filter };
+ const anotherFilter = { anotherField: 'another-value' };
+ mutations[types.SET_FILTER](state, anotherFilter);
+
+ expect(state.filter).toStrictEqual({
+ ...originalFilter,
+ ...anotherFilter,
+ });
+ });
});
describe(`${types.REQUEST_REPOS}`, () => {
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index e8d222dc2e9..a0710ddb06c 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -97,13 +97,6 @@ describe('Incidents List', () => {
);
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
it('shows the loading state', () => {
mountComponent({
loading: true,
@@ -212,7 +205,7 @@ describe('Incidents List', () => {
});
});
- it('contains a link to the incident details page', async () => {
+ it('contains a link to the incident details page', () => {
findTableRows().at(0).trigger('click');
expect(visitUrl).toHaveBeenCalledWith(
joinPaths(`/project/issues/incident`, mockIncidents[0].iid),
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
deleted file mode 100644
index 0f042dfaa4c..00000000000
--- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
+++ /dev/null
@@ -1,59 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`IncidentsSettingTabs should render the component 1`] = `
-<section
- class="settings no-animate"
- data-qa-selector="incidents_settings_content"
- id="incident-management-settings"
->
- <div
- class="settings-header"
- >
- <h4
- class="settings-title js-settings-toggle js-settings-toggle-trigger-only"
- >
-
- Incidents
-
- </h4>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="js-settings-toggle"
- icon=""
- size="medium"
- variant="default"
- >
- Expand
- </gl-button-stub>
-
- <p>
-
- Fine-tune incident settings and set up integrations with external tools to help better manage incidents.
-
- </p>
- </div>
-
- <div
- class="settings-content"
- >
- <gl-tabs-stub
- queryparamname="tab"
- value="0"
- >
- <!---->
-
- <gl-tab-stub
- title="PagerDuty integration"
- titlelinkclass=""
- >
- <pagerdutysettingsform-stub
- class="gl-pt-3"
- data-testid="PagerDutySettingsForm-tab"
- />
- </gl-tab-stub>
- </gl-tabs-stub>
- </div>
-</section>
-`;
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index 1d1b285c1b6..9b11fe2bff0 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -1,12 +1,12 @@
import AxiosMockAdapter from 'axios-mock-adapter';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { ERROR_MSG } from '~/incidents_settings/constants';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('IncidentsSettingsService', () => {
@@ -33,7 +33,7 @@ describe('IncidentsSettingsService', () => {
});
});
- it('should display a flash message on update error', () => {
+ it('should display an alert on update error', () => {
mock.onPatch().reply(HTTP_STATUS_BAD_REQUEST);
return service.updateSettings({}).then(() => {
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
index 394d1f12bcb..850fd9f0fc9 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
@@ -1,12 +1,13 @@
import { GlTab } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import IncidentsSettingTabs from '~/incidents_settings/components/incidents_settings_tabs.vue';
+import { INTEGRATION_TABS_CONFIG } from '~/incidents_settings/constants';
describe('IncidentsSettingTabs', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallowMount(IncidentsSettingTabs, {
+ wrapper = shallowMountExtended(IncidentsSettingTabs, {
provide: {
service: {},
serviceLevelAgreementSettings: {},
@@ -14,16 +15,12 @@ describe('IncidentsSettingTabs', () => {
});
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
const findToggleButton = () => wrapper.findComponent({ ref: 'toggleBtn' });
const findSectionHeader = () => wrapper.findComponent({ ref: 'sectionHeader' });
-
const findIntegrationTabs = () => wrapper.findAllComponents(GlTab);
+ const findIntegrationTabAt = (index) => findIntegrationTabs().at(index);
+ const findTabComponent = (tab) => wrapper.findByTestId(`${tab.component}-tab`);
+
it('renders header text', () => {
expect(findSectionHeader().text()).toBe('Incidents');
});
@@ -34,18 +31,13 @@ describe('IncidentsSettingTabs', () => {
});
});
- it('should render the component', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('should render the tab for each active integration', () => {
- const activeTabs = wrapper.vm.$options.tabs.filter((tab) => tab.active);
- expect(findIntegrationTabs().length).toBe(activeTabs.length);
+ const activeTabs = INTEGRATION_TABS_CONFIG.filter((tab) => tab.active);
+ expect(findIntegrationTabs()).toHaveLength(activeTabs.length);
+
activeTabs.forEach((tab, index) => {
- expect(findIntegrationTabs().at(index).attributes('title')).toBe(tab.title);
- expect(
- findIntegrationTabs().at(index).find(`[data-testid="${tab.component}-tab"]`).exists(),
- ).toBe(true);
+ expect(findIntegrationTabAt(index).attributes('title')).toBe(tab.title);
+ expect(findTabComponent(tab).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
index 521a861829b..77258db437d 100644
--- a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
+++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
@@ -26,10 +26,6 @@ describe('Alert integration settings form', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/integrations/edit/components/active_checkbox_spec.js b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
index 1f7a5f0dbc9..3a78140d0b1 100644
--- a/spec/frontend/integrations/edit/components/active_checkbox_spec.js
+++ b/spec/frontend/integrations/edit/components/active_checkbox_spec.js
@@ -17,10 +17,6 @@ describe('ActiveCheckbox', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findInputInCheckbox = () => findGlFormCheckbox().find('input');
@@ -30,7 +26,7 @@ describe('ActiveCheckbox', () => {
createComponent({}, { isInheriting: true });
expect(findGlFormCheckbox().exists()).toBe(true);
- expect(findInputInCheckbox().attributes('disabled')).toBe('disabled');
+ expect(findInputInCheckbox().attributes('disabled')).toBeDefined();
});
});
@@ -39,7 +35,7 @@ describe('ActiveCheckbox', () => {
createComponent({ activateDisabled: true });
expect(findGlFormCheckbox().exists()).toBe(true);
- expect(findInputInCheckbox().attributes('disabled')).toBe('disabled');
+ expect(findInputInCheckbox().attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
index cbe3402727a..dfb6b7d9a9c 100644
--- a/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
+++ b/spec/frontend/integrations/edit/components/confirmation_modal_spec.js
@@ -14,10 +14,6 @@ describe('ConfirmationModal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlModal = () => wrapper.findComponent(GlModal);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index 7589b04b0fd..e1d9aef752f 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -21,10 +21,6 @@ describe('DynamicField', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findGlFormInput = () => wrapper.findComponent(GlFormInput);
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 383dfb36aa5..5aa3ee35379 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -84,7 +84,6 @@ describe('IntegrationForm', () => {
});
afterEach(() => {
- wrapper.destroy();
mockAxios.restore();
});
@@ -496,7 +495,7 @@ describe('IntegrationForm', () => {
expect(refreshCurrentPage).toHaveBeenCalledTimes(1);
});
- it('resets `isResetting`', async () => {
+ it('resets `isResetting`', () => {
expect(findFormActions().props('isResetting')).toBe(false);
});
});
@@ -507,30 +506,21 @@ describe('IntegrationForm', () => {
const dummyHelp = 'Foo Help';
it.each`
- integration | flagIsOn | helpHtml | sections | shouldShowSections | shouldShowHelp
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
- ${'foo'} | ${false} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${false} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${false} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${''} | ${[]} | ${false} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[]} | ${false} | ${true}
- ${'foo'} | ${true} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
- ${'foo'} | ${true} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
+ integration | helpHtml | sections | shouldShowSections | shouldShowHelp
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${''} | ${[]} | ${false} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${INTEGRATION_FORM_TYPE_SLACK} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${true}
+ ${'foo'} | ${''} | ${[]} | ${false} | ${false}
+ ${'foo'} | ${dummyHelp} | ${[]} | ${false} | ${true}
+ ${'foo'} | ${undefined} | ${[mockSectionConnection]} | ${true} | ${false}
+ ${'foo'} | ${dummyHelp} | ${[mockSectionConnection]} | ${true} | ${false}
`(
- '$sections sections, and "$helpHtml" helpHtml when the FF is "$flagIsOn" for "$integration" integration',
- ({ integration, flagIsOn, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
+ '$sections sections, and "$helpHtml" helpHtml for "$integration" integration',
+ ({ integration, helpHtml, sections, shouldShowSections, shouldShowHelp }) => {
createComponent({
provide: {
helpHtml,
- glFeatures: { integrationSlackAppNotifications: flagIsOn },
},
customStateProps: {
sections,
@@ -553,20 +543,15 @@ describe('IntegrationForm', () => {
${false} | ${true} | ${'When having only the fields without a section'}
`('$description', ({ hasSections, hasFieldsWithoutSections }) => {
it.each`
- prefix | integration | shouldUpgradeSlack | flagIsOn | shouldShowAlert
- ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true} | ${true}
- ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${true} | ${false}
- ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${false} | ${false}
- ${'does not'} | ${'foo'} | ${true} | ${true} | ${false}
- ${'does not'} | ${'foo'} | ${false} | ${true} | ${false}
- ${'does not'} | ${'foo'} | ${true} | ${false} | ${false}
+ prefix | integration | shouldUpgradeSlack | shouldShowAlert
+ ${'does'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${true} | ${true}
+ ${'does not'} | ${INTEGRATION_FORM_TYPE_SLACK} | ${false} | ${false}
+ ${'does not'} | ${'foo'} | ${true} | ${false}
+ ${'does not'} | ${'foo'} | ${false} | ${false}
`(
- '$prefix render the upgrade warning when we are in "$integration" integration with the flag "$flagIsOn" and Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections',
- ({ integration, shouldUpgradeSlack, flagIsOn, shouldShowAlert }) => {
+ '$prefix render the upgrade warning when we are in "$integration" integration with Slack-needs-upgrade is "$shouldUpgradeSlack" and have sections',
+ ({ integration, shouldUpgradeSlack, shouldShowAlert }) => {
createComponent({
- provide: {
- glFeatures: { integrationSlackAppNotifications: flagIsOn },
- },
customStateProps: {
shouldUpgradeSlack,
type: integration,
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index fa91f8de45a..82f70b8ede1 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -31,18 +31,13 @@ describe('JiraIssuesFields', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findEnableCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findEnableCheckboxDisabled = () =>
findEnableCheckbox().find('[type=checkbox]').attributes('disabled');
const findProjectKey = () => wrapper.findComponent(GlFormInput);
const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group');
const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities');
- const setEnableCheckbox = async (isEnabled = true) =>
- findEnableCheckbox().vm.$emit('input', isEnabled);
+ const setEnableCheckbox = (isEnabled = true) => findEnableCheckbox().vm.$emit('input', isEnabled);
const assertProjectKeyState = (expectedStateValue) => {
expect(findProjectKey().attributes('state')).toBe(expectedStateValue);
@@ -182,7 +177,7 @@ describe('JiraIssuesFields', () => {
});
describe('with no project key', () => {
- it('sets Project Key `state` attribute to `undefined`', async () => {
+ it('sets Project Key `state` attribute to `undefined`', () => {
assertProjectKeyState(undefined);
});
});
diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
index 6011b3e6edc..a038b63d28c 100644
--- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
@@ -22,10 +22,6 @@ describe('JiraTriggerFields', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findCommentSettings = () => wrapper.findByTestId('comment-settings');
const findCommentDetail = () => wrapper.findByTestId('comment-detail');
const findCommentSettingsCheckbox = () => findCommentSettings().findComponent(GlFormCheckbox);
@@ -191,7 +187,7 @@ describe('JiraTriggerFields', () => {
);
wrapper.findAll('[type=text], [type=checkbox], [type=radio]').wrappers.forEach((input) => {
- expect(input.attributes('disabled')).toBe('disabled');
+ expect(input.attributes('disabled')).toBeDefined();
});
});
});
diff --git a/spec/frontend/integrations/edit/components/override_dropdown_spec.js b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
index 90facaff1f9..2d1a6b3ace1 100644
--- a/spec/frontend/integrations/edit/components/override_dropdown_spec.js
+++ b/spec/frontend/integrations/edit/components/override_dropdown_spec.js
@@ -26,10 +26,6 @@ describe('OverrideDropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlLink = () => wrapper.findComponent(GlLink);
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
diff --git a/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js b/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js
new file mode 100644
index 00000000000..62f0439a13f
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/apple_app_store_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionAppleAppStore from '~/integrations/edit/components/sections/apple_app_store.vue';
+import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue';
+import { createStore } from '~/integrations/edit/store';
+
+describe('IntegrationSectionAppleAppStore', () => {
+ let wrapper;
+
+ const createComponent = (componentFields) => {
+ const store = createStore({
+ customState: { ...componentFields },
+ });
+ wrapper = shallowMount(IntegrationSectionAppleAppStore, {
+ store,
+ });
+ };
+
+ const componentFields = (fileName = '') => {
+ return {
+ fields: [
+ {
+ name: 'app_store_private_key_file_name',
+ value: fileName,
+ },
+ ],
+ };
+ };
+
+ const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField);
+
+ describe('computed properties', () => {
+ it('renders UploadDropzoneField with default values', () => {
+ createComponent(componentFields());
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'The Apple App Store Connect Private Key (.p8)',
+ helpText: '',
+ });
+ });
+
+ it('renders UploadDropzoneField with custom values for an attached file', () => {
+ createComponent(componentFields('fileName.txt'));
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'Upload a new Apple App Store Connect Private Key (replace fileName.txt)',
+ helpText: 'Leave empty to use your current Private Key.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/sections/configuration_spec.js b/spec/frontend/integrations/edit/components/sections/configuration_spec.js
index e697212ea0b..c8a7d17c041 100644
--- a/spec/frontend/integrations/edit/components/sections/configuration_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/configuration_spec.js
@@ -19,10 +19,6 @@ describe('IntegrationSectionCoonfiguration', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/sections/connection_spec.js b/spec/frontend/integrations/edit/components/sections/connection_spec.js
index 1eb92e80723..a24253d542d 100644
--- a/spec/frontend/integrations/edit/components/sections/connection_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/connection_spec.js
@@ -20,10 +20,6 @@ describe('IntegrationSectionConnection', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
diff --git a/spec/frontend/integrations/edit/components/sections/google_play_spec.js b/spec/frontend/integrations/edit/components/sections/google_play_spec.js
new file mode 100644
index 00000000000..c0d6d17f639
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/sections/google_play_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+
+import IntegrationSectionGooglePlay from '~/integrations/edit/components/sections/google_play.vue';
+import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue';
+import { createStore } from '~/integrations/edit/store';
+
+describe('IntegrationSectionGooglePlay', () => {
+ let wrapper;
+
+ const createComponent = (fileName = '') => {
+ const store = createStore({
+ customState: {
+ fields: [
+ {
+ name: 'service_account_key_file_name',
+ value: fileName,
+ },
+ ],
+ },
+ });
+
+ wrapper = shallowMount(IntegrationSectionGooglePlay, {
+ store,
+ });
+ };
+
+ const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField);
+
+ describe('computed properties', () => {
+ it('renders UploadDropzoneField with default values', () => {
+ createComponent();
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'Service account key (.json)',
+ helpText: '',
+ });
+ });
+
+ it('renders UploadDropzoneField with custom values for an attached file', () => {
+ createComponent('fileName.txt');
+
+ const field = findUploadDropzoneField();
+
+ expect(field.exists()).toBe(true);
+ expect(field.props()).toMatchObject({
+ label: 'Upload a new service account key (replace fileName.txt)',
+ helpText: 'Leave empty to use your current service account key.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js b/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js
index a7c1cc2a03f..8b39fa8f583 100644
--- a/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/jira_issues_spec.js
@@ -18,10 +18,6 @@ describe('IntegrationSectionJiraIssue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findJiraIssuesFields = () => wrapper.findComponent(JiraIssuesFields);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js b/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js
index d4ab9864fab..b3b7f508e25 100644
--- a/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/jira_trigger_spec.js
@@ -18,10 +18,6 @@ describe('IntegrationSectionJiraTrigger', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findJiraTriggerFields = () => wrapper.findComponent(JiraTriggerFields);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/sections/trigger_spec.js b/spec/frontend/integrations/edit/components/sections/trigger_spec.js
index 883f5c7bf79..b9c1efbb0a2 100644
--- a/spec/frontend/integrations/edit/components/sections/trigger_spec.js
+++ b/spec/frontend/integrations/edit/components/sections/trigger_spec.js
@@ -18,10 +18,6 @@ describe('IntegrationSectionTrigger', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAllTriggerFields = () => wrapper.findAllComponents(TriggerField);
describe('template', () => {
diff --git a/spec/frontend/integrations/edit/components/trigger_field_spec.js b/spec/frontend/integrations/edit/components/trigger_field_spec.js
index ed0b3324708..b3d6784959f 100644
--- a/spec/frontend/integrations/edit/components/trigger_field_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_field_spec.js
@@ -23,10 +23,6 @@ describe('TriggerField', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findGlFormInput = () => wrapper.findComponent(GlFormInput);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
@@ -41,7 +37,7 @@ describe('TriggerField', () => {
it('when isInheriting is true, renders disabled GlFormCheckbox', () => {
createComponent({ isInheriting: true });
- expect(findGlFormCheckbox().attributes('disabled')).toBe('true');
+ expect(findGlFormCheckbox().attributes('disabled')).toBeDefined();
});
it('renders correct title', () => {
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index 082eeea30f1..defa02aefd2 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -20,10 +20,6 @@ describe('TriggerFields', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTriggerLabel = () => wrapper.findByTestId('trigger-fields-group').find('label');
const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAllComponents(GlFormGroup);
const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
diff --git a/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js b/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js
new file mode 100644
index 00000000000..36e20db0022
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/upload_dropzone_field_spec.js
@@ -0,0 +1,88 @@
+import { mount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import { nextTick } from 'vue';
+
+import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import { mockField } from '../mock_data';
+
+describe('UploadDropzoneField', () => {
+ let wrapper;
+
+ const contentsInputName = 'service[app_store_private_key]';
+ const fileNameInputName = 'service[app_store_private_key_file_name]';
+
+ const createComponent = (props) => {
+ wrapper = mount(UploadDropzoneField, {
+ propsData: {
+ ...mockField,
+ ...props,
+ name: contentsInputName,
+ label: 'Input Label',
+ fileInputName: fileNameInputName,
+ },
+ });
+ };
+
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
+ const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
+ const findFileContentsHiddenInput = () => wrapper.find(`input[name="${contentsInputName}"]`);
+ const findFileNameHiddenInput = () => wrapper.find(`input[name="${fileNameInputName}"]`);
+
+ describe('template', () => {
+ it('adds the expected file inputFieldName', () => {
+ createComponent();
+
+ expect(findUploadDropzone().props('inputFieldName')).toBe('service[dropzone_file_name]');
+ });
+
+ it('adds a disabled, hidden text input for the file contents', () => {
+ createComponent();
+
+ expect(findFileContentsHiddenInput().attributes('name')).toBe(contentsInputName);
+ expect(findFileContentsHiddenInput().attributes('disabled')).toBeDefined();
+ });
+
+ it('adds a disabled, hidden text input for the file name', () => {
+ createComponent();
+
+ expect(findFileNameHiddenInput().attributes('name')).toBe(fileNameInputName);
+ expect(findFileNameHiddenInput().attributes('disabled')).toBeDefined();
+ });
+ });
+
+ describe('clearError', () => {
+ it('clears uploadError when called', async () => {
+ createComponent();
+
+ expect(findGlAlert().exists()).toBe(false);
+
+ findUploadDropzone().vm.$emit('error');
+ await nextTick();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe(
+ 'Error: You are trying to upload something other than an allowed file.',
+ );
+
+ findGlAlert().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findGlAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('onError', () => {
+ it('assigns uploadError to the supplied custom message', async () => {
+ const message = 'test error message';
+ createComponent({ errorMessage: message });
+
+ findUploadDropzone().vm.$emit('error');
+
+ await nextTick();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe(message);
+ });
+ });
+});
diff --git a/spec/frontend/integrations/index/components/integrations_list_spec.js b/spec/frontend/integrations/index/components/integrations_list_spec.js
index ee54a5fd359..155a3d1c6be 100644
--- a/spec/frontend/integrations/index/components/integrations_list_spec.js
+++ b/spec/frontend/integrations/index/components/integrations_list_spec.js
@@ -13,10 +13,6 @@ describe('IntegrationsList', () => {
wrapper = shallowMountExtended(IntegrationsList, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('provides correct `integrations` prop to the IntegrationsTable instance', () => {
createComponent({ integrations: [...mockInactiveIntegrations, ...mockActiveIntegrations] });
diff --git a/spec/frontend/integrations/index/components/integrations_table_spec.js b/spec/frontend/integrations/index/components/integrations_table_spec.js
index 976c7b74890..54e5b45a5d8 100644
--- a/spec/frontend/integrations/index/components/integrations_table_spec.js
+++ b/spec/frontend/integrations/index/components/integrations_table_spec.js
@@ -1,6 +1,5 @@
import { GlTable, GlIcon, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { INTEGRATION_TYPE_SLACK } from '~/integrations/constants';
import IntegrationsTable from '~/integrations/index/components/integrations_table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -11,24 +10,18 @@ describe('IntegrationsTable', () => {
const findTable = () => wrapper.findComponent(GlTable);
- const createComponent = (propsData = {}, flagIsOn = false) => {
+ const createComponent = (propsData = {}, glFeatures = {}) => {
wrapper = mount(IntegrationsTable, {
propsData: {
integrations: mockActiveIntegrations,
...propsData,
},
provide: {
- glFeatures: {
- integrationSlackAppNotifications: flagIsOn,
- },
+ glFeatures,
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each([true, false])('when `showUpdatedAt` is %p', (showUpdatedAt) => {
beforeEach(() => {
createComponent({ showUpdatedAt });
@@ -57,50 +50,16 @@ describe('IntegrationsTable', () => {
});
});
- describe('integrations filtering', () => {
- const slackActive = {
- ...mockActiveIntegrations[0],
- name: INTEGRATION_TYPE_SLACK,
- title: 'Slack',
- };
- const slackInactive = {
- ...mockInactiveIntegrations[0],
- name: INTEGRATION_TYPE_SLACK,
- title: 'Slack',
- };
-
- describe.each`
- desc | flagIsOn | integrations | expectedIntegrations
- ${'only active'} | ${false} | ${mockActiveIntegrations} | ${mockActiveIntegrations}
- ${'only active'} | ${true} | ${mockActiveIntegrations} | ${mockActiveIntegrations}
- ${'only inactive'} | ${true} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations}
- ${'only inactive'} | ${false} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations}
- ${'active and inactive'} | ${true} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'active and inactive'} | ${false} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'Slack active with active'} | ${false} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]}
- ${'Slack active with active'} | ${true} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]}
- ${'Slack active with inactive'} | ${false} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]}
- ${'Slack active with inactive'} | ${true} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]}
- ${'Slack inactive with active'} | ${false} | ${[slackInactive, ...mockActiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations]}
- ${'Slack inactive with active'} | ${true} | ${[slackInactive, ...mockActiveIntegrations]} | ${mockActiveIntegrations}
- ${'Slack inactive with inactive'} | ${false} | ${[slackInactive, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockInactiveIntegrations]}
- ${'Slack inactive with inactive'} | ${true} | ${[slackInactive, ...mockInactiveIntegrations]} | ${mockInactiveIntegrations}
- ${'Slack active with active and inactive'} | ${true} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'Slack active with active and inactive'} | ${false} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'Slack inactive with active and inactive'} | ${true} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
- ${'Slack inactive with active and inactive'} | ${false} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
- `('when $desc and flag "$flagIsOn"', ({ flagIsOn, integrations, expectedIntegrations }) => {
+ describe.each([true, false])(
+ 'when `remove_monitor_metrics` flag is %p',
+ (removeMonitorMetrics) => {
beforeEach(() => {
- createComponent({ integrations }, flagIsOn);
+ createComponent({ integrations: [mockInactiveIntegrations[3]] }, { removeMonitorMetrics });
});
- it('renders correctly', () => {
- const links = wrapper.findAllComponents(GlLink);
- expect(links).toHaveLength(expectedIntegrations.length);
- expectedIntegrations.forEach((integration, index) => {
- expect(links.at(index).text()).toBe(integration.title);
- });
+ it(`${removeMonitorMetrics ? 'does not render' : 'renders'} prometheus integration`, () => {
+ expect(findTable().findComponent(GlLink).exists()).toBe(!removeMonitorMetrics);
});
- });
- });
+ },
+ );
});
diff --git a/spec/frontend/integrations/index/mock_data.js b/spec/frontend/integrations/index/mock_data.js
index 2231687d255..c07b320c0d3 100644
--- a/spec/frontend/integrations/index/mock_data.js
+++ b/spec/frontend/integrations/index/mock_data.js
@@ -47,4 +47,13 @@ export const mockInactiveIntegrations = [
'/gitlab-qa-sandbox-group/project_with_jenkins_6a55a67c-57c6ed0597c9319a/-/services/bamboo/edit',
name: 'bamboo',
},
+ {
+ active: false,
+ title: 'Prometheus',
+ description: 'A monitoring tool for Kubernetes',
+ updated_at: null,
+ edit_path:
+ '/gitlab-qa-sandbox-group/project_with_jenkins_6a55a67c-57c6ed0597c9319a/-/services/prometheus/edit',
+ name: 'prometheus',
+ },
];
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index fdb728281b5..9e863eaecfd 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -47,7 +47,6 @@ describe('IntegrationOverrides', () => {
afterEach(() => {
mockAxios.restore();
- wrapper.destroy();
});
const findGlTable = () => wrapper.findComponent(GlTable);
diff --git a/spec/frontend/integrations/overrides/components/integration_tabs_spec.js b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
index a728b4d391f..b35a40d69c1 100644
--- a/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_tabs_spec.js
@@ -21,10 +21,6 @@ describe('IntegrationTabs', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlBadge = () => wrapper.findComponent(GlBadge);
const findGlTab = () => wrapper.findComponent(GlTab);
const findSettingsLink = () => wrapper.find('a');
diff --git a/spec/frontend/invite_members/components/confetti_spec.js b/spec/frontend/invite_members/components/confetti_spec.js
index 2f361f1dc1e..382569abfd9 100644
--- a/spec/frontend/invite_members/components/confetti_spec.js
+++ b/spec/frontend/invite_members/components/confetti_spec.js
@@ -6,16 +6,10 @@ jest.mock('canvas-confetti', () => ({
create: jest.fn(),
}));
-let wrapper;
-
const createComponent = () => {
- wrapper = shallowMount(Confetti);
+ shallowMount(Confetti);
};
-afterEach(() => {
- wrapper.destroy();
-});
-
describe('Confetti', () => {
it('initiates confetti', () => {
const basicCannon = jest.spyOn(Confetti.methods, 'basicCannon').mockImplementation(() => {});
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js
index e1563a7bb3a..a1ca9a69926 100644
--- a/spec/frontend/invite_members/components/group_select_spec.js
+++ b/spec/frontend/invite_members/components/group_select_spec.js
@@ -26,14 +26,9 @@ describe('GroupSelect', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="true"]');
+ const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="menu"]');
const findAvatarByLabel = (text) =>
wrapper
.findAllComponents(GlAvatarLabeled)
@@ -66,6 +61,7 @@ describe('GroupSelect', () => {
expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
exclude_internal: true,
active: true,
+ order_by: 'similarity',
});
});
diff --git a/spec/frontend/invite_members/components/import_project_members_modal_spec.js b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
index d839cde163c..73634855850 100644
--- a/spec/frontend/invite_members/components/import_project_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/import_project_members_modal_spec.js
@@ -1,6 +1,8 @@
import { GlFormGroup, GlSprintf, GlModal } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { createWrapper } from '@vue/test-utils';
+import { BV_HIDE_MODAL } from '~/lib/utils/constants';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -54,7 +56,6 @@ beforeEach(() => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
@@ -108,6 +109,15 @@ describe('ImportProjectMembersModal', () => {
});
describe('submitting the import', () => {
+ it('prevents closing', () => {
+ const evt = { preventDefault: jest.fn() };
+ createComponent();
+
+ findGlModal().vm.$emit('primary', evt);
+
+ expect(evt.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
describe('when the import is successful with reloadPageOnSubmit', () => {
beforeEach(() => {
createComponent({
@@ -162,6 +172,12 @@ describe('ImportProjectMembersModal', () => {
);
});
+ it('hides the modal', () => {
+ const rootWrapper = createWrapper(wrapper.vm.$root);
+
+ expect(rootWrapper.emitted(BV_HIDE_MODAL)).toHaveLength(1);
+ });
+
it('does not call displaySuccessfulInvitationAlert on mount', () => {
expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/invite_members/components/import_project_members_trigger_spec.js b/spec/frontend/invite_members/components/import_project_members_trigger_spec.js
index b6375fcfa22..0e8243491a8 100644
--- a/spec/frontend/invite_members/components/import_project_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/import_project_members_trigger_spec.js
@@ -17,10 +17,6 @@ const createComponent = (props = {}) => {
describe('ImportProjectMembersTrigger', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButton = () => wrapper.findComponent(GlButton);
describe('displayText', () => {
diff --git a/spec/frontend/invite_members/components/invite_group_notification_spec.js b/spec/frontend/invite_members/components/invite_group_notification_spec.js
index 3e6ba6da9f4..1da2e7b705d 100644
--- a/spec/frontend/invite_members/components/invite_group_notification_spec.js
+++ b/spec/frontend/invite_members/components/invite_group_notification_spec.js
@@ -2,7 +2,7 @@ import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
import InviteGroupNotification from '~/invite_members/components/invite_group_notification.vue';
-import { GROUP_MODAL_ALERT_BODY } from '~/invite_members/constants';
+import { GROUP_MODAL_TO_GROUP_ALERT_BODY } from '~/invite_members/constants';
describe('InviteGroupNotification', () => {
let wrapper;
@@ -13,7 +13,11 @@ describe('InviteGroupNotification', () => {
const createComponent = () => {
wrapper = shallowMountExtended(InviteGroupNotification, {
provide: { freeUsersLimit: 5 },
- propsData: { name: 'name' },
+ propsData: {
+ name: 'name',
+ notificationLink: '_notification_link_',
+ notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ },
stubs: { GlSprintf },
});
};
@@ -28,15 +32,13 @@ describe('InviteGroupNotification', () => {
});
it('shows the correct message', () => {
- const message = sprintf(GROUP_MODAL_ALERT_BODY, { count: 5 });
+ const message = sprintf(GROUP_MODAL_TO_GROUP_ALERT_BODY, { count: 5 });
expect(findAlert().text()).toMatchInterpolatedText(message);
});
it('has a help link', () => {
- expect(findLink().attributes('href')).toEqual(
- 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group',
- );
+ expect(findLink().attributes('href')).toEqual('_notification_link_');
});
});
});
diff --git a/spec/frontend/invite_members/components/invite_group_trigger_spec.js b/spec/frontend/invite_members/components/invite_group_trigger_spec.js
index 84ddb779a9e..e088dc41a2b 100644
--- a/spec/frontend/invite_members/components/invite_group_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_group_trigger_spec.js
@@ -17,11 +17,6 @@ const createComponent = (props = {}) => {
describe('InviteGroupTrigger', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findButton = () => wrapper.findComponent(GlButton);
describe('displayText', () => {
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index c2a55517405..4f082145562 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -12,6 +12,12 @@ import {
displaySuccessfulInvitationAlert,
reloadOnInvitationSuccess,
} from '~/invite_members/utils/trigger_successful_invite_alert';
+import {
+ GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ GROUP_MODAL_TO_GROUP_ALERT_LINK,
+ GROUP_MODAL_TO_PROJECT_ALERT_BODY,
+ GROUP_MODAL_TO_PROJECT_ALERT_LINK,
+} from '~/invite_members/constants';
import { propsData, sharedGroup } from '../mock_data/group_modal';
jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
@@ -44,11 +50,6 @@ describe('InviteGroupsModal', () => {
createComponent({ isProject: false });
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findModal = () => wrapper.findComponent(GlModal);
const findGroupSelect = () => wrapper.findComponent(GroupSelect);
const findInviteGroupAlert = () => wrapper.findComponent(InviteGroupNotification);
@@ -58,11 +59,13 @@ describe('InviteGroupsModal', () => {
findMembersFormGroup().attributes('invalid-feedback');
const findBase = () => wrapper.findComponent(InviteModalBase);
const triggerGroupSelect = (val) => findGroupSelect().vm.$emit('input', val);
- const emitEventFromModal = (eventName) => () =>
- findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
- const hideModal = emitEventFromModal('hidden');
- const clickInviteButton = emitEventFromModal('primary');
- const clickCancelButton = emitEventFromModal('cancel');
+ const hideModal = () => findModal().vm.$emit('hidden', { preventDefault: jest.fn() });
+
+ const emitClickFromModal = (testId) => () =>
+ wrapper.findByTestId(testId).vm.$emit('click', { preventDefault: jest.fn() });
+
+ const clickInviteButton = emitClickFromModal('invite-modal-submit');
+ const clickCancelButton = emitClickFromModal('invite-modal-cancel');
describe('displaying the correct introText and form group description', () => {
describe('when inviting to a project', () => {
@@ -94,6 +97,26 @@ describe('InviteGroupsModal', () => {
expect(findInviteGroupAlert().exists()).toBe(false);
});
+
+ it('shows the user limit notification alert with correct link and text for group', () => {
+ createComponent({ freeUserCapEnabled: true });
+
+ expect(findInviteGroupAlert().props()).toMatchObject({
+ name: propsData.name,
+ notificationText: GROUP_MODAL_TO_GROUP_ALERT_BODY,
+ notificationLink: GROUP_MODAL_TO_GROUP_ALERT_LINK,
+ });
+ });
+
+ it('shows the user limit notification alert with correct link and text for project', () => {
+ createComponent({ freeUserCapEnabled: true, isProject: true });
+
+ expect(findInviteGroupAlert().props()).toMatchObject({
+ name: propsData.name,
+ notificationText: GROUP_MODAL_TO_PROJECT_ALERT_BODY,
+ notificationLink: GROUP_MODAL_TO_PROJECT_ALERT_LINK,
+ });
+ });
});
describe('submitting the invite form', () => {
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 9687d528321..e080e665a3b 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -6,7 +6,6 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import ExperimentTracking from '~/experimentation/experiment_tracking';
import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue';
import InviteModalBase from '~/invite_members/components/invite_modal_base.vue';
import ModalConfetti from '~/invite_members/components/confetti.vue';
@@ -18,11 +17,11 @@ import {
MEMBERS_MODAL_CELEBRATE_TITLE,
MEMBERS_PLACEHOLDER,
MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT,
- LEARN_GITLAB,
EXPANDED_ERRORS,
EMPTY_INVITES_ALERT_TEXT,
ON_CELEBRATION_TRACK_LABEL,
INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
+ INVALID_FEEDBACK_MESSAGE_DEFAULT,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -40,7 +39,9 @@ import {
import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses';
import {
propsData,
- inviteSource,
+ emailPostData,
+ postData,
+ singleUserPostData,
newProjectPath,
user1,
user2,
@@ -63,16 +64,18 @@ describe('InviteMembersModal', () => {
let mock;
let trackingSpy;
- const expectTracking = (
- action,
- label = undefined,
- category = INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
- ) => expect(trackingSpy).toHaveBeenCalledWith(category, action, { label, category });
+ const expectTracking = (action, label = undefined, property = undefined) =>
+ expect(trackingSpy).toHaveBeenCalledWith(INVITE_MEMBER_MODAL_TRACKING_CATEGORY, action, {
+ label,
+ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
+ property,
+ });
const createComponent = (props = {}, stubs = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
provide: {
newProjectPath,
+ name: propsData.name,
},
propsData: {
usersLimitDataset: {},
@@ -116,8 +119,6 @@ describe('InviteMembersModal', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mock.restore();
});
@@ -134,10 +135,15 @@ describe('InviteMembersModal', () => {
`${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${
Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]
}`;
- const emitEventFromModal = (eventName) => () =>
- findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
- const clickInviteButton = emitEventFromModal('primary');
- const clickCancelButton = emitEventFromModal('cancel');
+ const findActionButton = () => wrapper.findByTestId('invite-modal-submit');
+ const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel');
+
+ const emitClickFromModal = (findButton) => () =>
+ findButton().vm.$emit('click', { preventDefault: jest.fn() });
+
+ const clickInviteButton = emitClickFromModal(findActionButton);
+ const clickCancelButton = emitClickFromModal(findCancelButton);
+
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const membersFormGroupInvalidFeedback = () =>
findMembersFormGroup().attributes('invalid-feedback');
@@ -207,15 +213,6 @@ describe('InviteMembersModal', () => {
expect(findTasksToBeDone().exists()).toBe(false);
});
-
- describe('when opened from the Learn GitLab page', () => {
- it('does render the tasks to be done', async () => {
- await setupComponent({}, []);
- await triggerOpenModal({ source: LEARN_GITLAB });
-
- expect(findTasksToBeDone().exists()).toBe(true);
- });
- });
});
describe('rendering the tasks', () => {
@@ -274,38 +271,18 @@ describe('InviteMembersModal', () => {
});
describe('tracking events', () => {
- it('tracks the view for invite_members_for_task', async () => {
- await setupComponentWithTasks();
-
- expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- INVITE_MEMBERS_FOR_TASK.view,
- );
- });
-
it('tracks the submit for invite_members_for_task', async () => {
await setupComponentWithTasks();
- await triggerMembersTokenSelect([user1]);
- clickInviteButton();
+ await triggerMembersTokenSelect([user1]);
- expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name, {
- label: 'selected_tasks_to_be_done',
- property: 'ci,code',
- });
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- INVITE_MEMBERS_FOR_TASK.submit,
- );
- });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- it('does not track the submit for invite_members_for_task when invites have not been entered', async () => {
- await setupComponentWithTasks();
clickInviteButton();
- expect(ExperimentTracking).not.toHaveBeenCalledWith(
- INVITE_MEMBERS_FOR_TASK.name,
- expect.any,
- );
+ expectTracking(INVITE_MEMBERS_FOR_TASK.submit, 'selected_tasks_to_be_done', 'ci,code');
+
+ unmockTracking();
});
});
});
@@ -368,13 +345,11 @@ describe('InviteMembersModal', () => {
it('tracks actions', async () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- const mockEvent = { preventDefault: jest.fn() };
-
await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL });
expectTracking('render', ON_CELEBRATION_TRACK_LABEL);
- findModal().vm.$emit('cancel', mockEvent);
+ clickCancelButton();
expectTracking('click_cancel', ON_CELEBRATION_TRACK_LABEL);
findModal().vm.$emit('close');
@@ -411,13 +386,11 @@ describe('InviteMembersModal', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- const mockEvent = { preventDefault: jest.fn() };
-
await triggerOpenModal(source);
expectTracking('render', label);
- findModal().vm.$emit('cancel', mockEvent);
+ clickCancelButton();
expectTracking('click_cancel', label);
findModal().vm.$emit('close');
@@ -472,7 +445,7 @@ describe('InviteMembersModal', () => {
const expectedSyntaxError = 'email contains an invalid email address';
describe('when no invites have been entered in the form and then some are entered', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createInviteMembersToGroupWrapper();
});
@@ -492,16 +465,6 @@ describe('InviteMembersModal', () => {
});
describe('when inviting an existing user to group by user ID', () => {
- const postData = {
- user_id: '1,2',
- access_level: propsData.defaultAccessLevel,
- expires_at: undefined,
- invite_source: inviteSource,
- format: 'json',
- tasks_to_be_done: [],
- tasks_project_id: '',
- };
-
describe('when reloadOnSubmit is true', () => {
beforeEach(async () => {
createComponent({ reloadPageOnSubmit: true });
@@ -555,20 +518,6 @@ describe('InviteMembersModal', () => {
expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
});
});
-
- describe('when opened from a Learn GitLab page', () => {
- it('emits the `showSuccessfulInvitationsAlert` event', async () => {
- await triggerOpenModal({ source: LEARN_GITLAB });
-
- jest.spyOn(eventHub, '$emit').mockImplementation();
-
- clickInviteButton();
-
- await waitForPromises();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('showSuccessfulInvitationsAlert');
- });
- });
});
describe('when member is not added successfully', () => {
@@ -675,16 +624,6 @@ describe('InviteMembersModal', () => {
});
describe('when inviting a new user by email address', () => {
- const postData = {
- access_level: propsData.defaultAccessLevel,
- expires_at: undefined,
- email: 'email@example.com',
- invite_source: inviteSource,
- tasks_to_be_done: [],
- tasks_project_id: '',
- format: 'json',
- };
-
describe('when invites are sent successfully', () => {
beforeEach(async () => {
createComponent();
@@ -692,7 +631,7 @@ describe('InviteMembersModal', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: emailPostData });
});
describe('when triggered from regular mounting', () => {
@@ -701,7 +640,7 @@ describe('InviteMembersModal', () => {
});
it('calls Api inviteGroupMembers with the correct params', () => {
- expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, emailPostData);
});
it('displays the successful toastMessage', () => {
@@ -719,96 +658,117 @@ describe('InviteMembersModal', () => {
});
describe('when invites are not sent successfully', () => {
- beforeEach(async () => {
- createInviteMembersToGroupWrapper();
+ describe('when api throws error', () => {
+ beforeEach(async () => {
+ jest.spyOn(axios, 'post').mockImplementation(() => {
+ throw new Error();
+ });
- await triggerMembersTokenSelect([user3]);
+ createInviteMembersToGroupWrapper();
+
+ await triggerMembersTokenSelect([user3]);
+ clickInviteButton();
+ });
+
+ it('displays the default error message', () => {
+ expect(membersFormGroupInvalidFeedback()).toBe(INVALID_FEEDBACK_MESSAGE_DEFAULT);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
+ });
});
- it('displays the api error for invalid email syntax', async () => {
- mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+ describe('when api rejects promise', () => {
+ beforeEach(async () => {
+ createInviteMembersToGroupWrapper();
- clickInviteButton();
+ await triggerMembersTokenSelect([user3]);
+ });
- await waitForPromises();
+ it('displays the api error for invalid email syntax', async () => {
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
- expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('exceptionState')).toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
- });
+ clickInviteButton();
- it('clears the error when the modal is hidden', async () => {
- mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+ await waitForPromises();
- clickInviteButton();
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
+ });
- await waitForPromises();
+ it('clears the error when the modal is hidden', async () => {
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
- expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('exceptionState')).toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+ clickInviteButton();
- findModal().vm.$emit('hidden');
+ await waitForPromises();
- await nextTick();
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
- expect(findMemberErrorAlert().exists()).toBe(false);
- expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('exceptionState')).not.toBe(false);
- });
+ findModal().vm.$emit('hidden');
- it('displays the restricted email error when restricted email is invited', async () => {
- mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+ await nextTick();
- clickInviteButton();
+ expect(findMemberErrorAlert().exists()).toBe(false);
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
+ });
- await waitForPromises();
+ it('displays the restricted email error when restricted email is invited', async () => {
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
- expect(findMemberErrorAlert().exists()).toBe(true);
- expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
- expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('exceptionState')).not.toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
- });
+ clickInviteButton();
- it('displays all errors when there are multiple emails that return a restricted error message', async () => {
- mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ await waitForPromises();
- clickInviteButton();
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
+ expect(findActionButton().props('loading')).toBe(false);
+ });
- await waitForPromises();
+ it('displays all errors when there are multiple emails that return a restricted error message', async () => {
+ mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
- expect(findMemberErrorAlert().exists()).toBe(true);
- expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0],
- );
- expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1],
- );
- expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2],
- );
- expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('exceptionState')).not.toBe(false);
- });
+ clickInviteButton();
- it('displays the invalid syntax error for bad request', async () => {
- mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
+ await waitForPromises();
- clickInviteButton();
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0],
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1],
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2],
+ );
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
+ });
- await waitForPromises();
+ it('displays the invalid syntax error for bad request', async () => {
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
- expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('exceptionState')).toBe(false);
- });
+ clickInviteButton();
- it('does not call displaySuccessfulInvitationAlert on mount', () => {
- expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
- });
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ });
+
+ it('does not call displaySuccessfulInvitationAlert on mount', () => {
+ expect(displaySuccessfulInvitationAlert).not.toHaveBeenCalled();
+ });
- it('does not call reloadOnInvitationSuccess', () => {
- expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ it('does not call reloadOnInvitationSuccess', () => {
+ expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
+ });
});
});
@@ -892,17 +852,6 @@ describe('InviteMembersModal', () => {
});
describe('when inviting members and non-members in same click', () => {
- const postData = {
- access_level: propsData.defaultAccessLevel,
- expires_at: undefined,
- invite_source: inviteSource,
- format: 'json',
- tasks_to_be_done: [],
- tasks_project_id: '',
- user_id: '1',
- email: 'email@example.com',
- };
-
describe('when invites are sent successfully', () => {
beforeEach(async () => {
createComponent();
@@ -910,7 +859,7 @@ describe('InviteMembersModal', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
+ jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: singleUserPostData });
});
describe('when triggered from regular mounting', () => {
@@ -922,7 +871,7 @@ describe('InviteMembersModal', () => {
it('calls Api inviteGroupMembers with the correct params and invite source', () => {
expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, {
- ...postData,
+ ...singleUserPostData,
invite_source: '_invite_source_',
});
});
@@ -951,26 +900,9 @@ describe('InviteMembersModal', () => {
clickInviteButton();
- expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, singleUserPostData);
});
});
});
-
- describe('tracking', () => {
- beforeEach(async () => {
- createComponent();
- await triggerMembersTokenSelect([user3]);
-
- wrapper.vm.$toast = { show: jest.fn() };
- jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({});
- });
-
- it('tracks the view for learn_gitlab source', () => {
- eventHub.$emit('openModal', { source: LEARN_GITLAB });
-
- expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name);
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(LEARN_GITLAB);
- });
- });
});
});
diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
index c522abe63c5..58c40a49b3c 100644
--- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js
@@ -1,12 +1,15 @@
-import { GlButton, GlLink, GlIcon } from '@gitlab/ui';
+import { GlButton, GlLink, GlDropdownItem, GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import eventHub from '~/invite_members/event_hub';
import {
TRIGGER_ELEMENT_BUTTON,
- TRIGGER_ELEMENT_SIDE_NAV,
TRIGGER_DEFAULT_QA_SELECTOR,
+ TRIGGER_ELEMENT_WITH_EMOJI,
+ TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI,
+ TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN,
} from '~/invite_members/constants';
+import { GlEmoji } from '../mock_data/member_modal';
jest.mock('~/experimentation/experiment_tracking');
@@ -19,7 +22,9 @@ let findButton;
const triggerComponent = {
button: GlButton,
anchor: GlLink,
- 'side-nav': GlLink,
+ 'text-emoji': GlLink,
+ 'dropdown-text-emoji': GlDropdownItem,
+ 'dropdown-text': GlButton,
};
const createComponent = (props = {}) => {
@@ -29,6 +34,11 @@ const createComponent = (props = {}) => {
...triggerProps,
...props,
},
+ stubs: {
+ GlEmoji,
+ GlDisclosureDropdownItem,
+ GlButton,
+ },
});
};
@@ -40,8 +50,8 @@ const triggerItems = [
triggerElement: 'anchor',
},
{
- triggerElement: TRIGGER_ELEMENT_SIDE_NAV,
- icon: 'plus',
+ triggerElement: TRIGGER_ELEMENT_WITH_EMOJI,
+ icon: 'shaking_hands',
},
];
@@ -50,10 +60,6 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
findButton = () => wrapper.findComponent(triggerComponent[triggerItem.triggerElement]);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('configurable attributes', () => {
it('includes the correct displayText for the button', () => {
createComponent();
@@ -91,31 +97,45 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => {
});
});
});
+});
- describe('tracking', () => {
- it('does not add tracking attributes', () => {
- createComponent();
-
- expect(findButton().attributes('data-track-action')).toBeUndefined();
- expect(findButton().attributes('data-track-label')).toBeUndefined();
- });
+describe('link with emoji', () => {
+ it('includes the specified icon with correct size when triggerElement is link', () => {
+ const findEmoji = () => wrapper.findComponent(GlEmoji);
- it('adds tracking attributes', () => {
- createComponent({ label: '_label_', event: '_event_' });
+ createComponent({ triggerElement: TRIGGER_ELEMENT_WITH_EMOJI, icon: 'shaking_hands' });
- expect(findButton().attributes('data-track-action')).toBe('_event_');
- expect(findButton().attributes('data-track-label')).toBe('_label_');
- });
+ expect(findEmoji().exists()).toBe(true);
+ expect(findEmoji().attributes('data-name')).toBe('shaking_hands');
});
});
-describe('side-nav with icon', () => {
+describe('dropdown item with emoji', () => {
it('includes the specified icon with correct size when triggerElement is link', () => {
- const findIcon = () => wrapper.findComponent(GlIcon);
+ const findEmoji = () => wrapper.findComponent(GlEmoji);
+
+ createComponent({ triggerElement: TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, icon: 'shaking_hands' });
+
+ expect(findEmoji().exists()).toBe(true);
+ expect(findEmoji().attributes('data-name')).toBe('shaking_hands');
+ });
+});
+
+describe('disclosure dropdown item', () => {
+ const findTrigger = () => wrapper.findComponent(GlDisclosureDropdownItem);
+
+ beforeEach(() => {
+ createComponent({ triggerElement: TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN });
+ });
+
+ it('renders a trigger button', () => {
+ expect(findTrigger().exists()).toBe(true);
+ expect(findTrigger().text()).toBe(displayText);
+ });
- createComponent({ triggerElement: TRIGGER_ELEMENT_SIDE_NAV, icon: 'plus' });
+ it('emits modalOpened which clicked', () => {
+ findTrigger().vm.$emit('action');
- expect(findIcon().exists()).toBe(true);
- expect(findIcon().props('name')).toBe('plus');
+ expect(wrapper.emitted('modal-opened')).toHaveLength(1);
});
});
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index f34f9902514..e70c83a424e 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -54,10 +54,6 @@ describe('InviteModalBase', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findFormSelect = () => wrapper.findComponent(GlFormSelect);
const findFormSelectOptions = () => findFormSelect().findAllComponents('option');
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
@@ -66,8 +62,8 @@ describe('InviteModalBase', () => {
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
const findMembersFormGroup = () => wrapper.findByTestId('members-form-group');
const findDisabledInput = () => wrapper.findByTestId('disabled-input');
- const findCancelButton = () => wrapper.find('.js-modal-action-cancel');
- const findActionButton = () => wrapper.find('.js-modal-action-primary');
+ const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel');
+ const findActionButton = () => wrapper.findByTestId('invite-modal-submit');
describe('rendering the modal', () => {
let trackingSpy;
@@ -88,20 +84,19 @@ describe('InviteModalBase', () => {
});
it('renders the Cancel button text correctly', () => {
- expect(wrapper.findComponent(GlModal).props('actionCancel')).toMatchObject({
- text: CANCEL_BUTTON_TEXT,
- });
+ expect(findCancelButton().text()).toBe(CANCEL_BUTTON_TEXT);
});
it('renders the Invite button correctly', () => {
- expect(wrapper.findComponent(GlModal).props('actionPrimary')).toMatchObject({
- text: INVITE_BUTTON_TEXT,
- attributes: {
- variant: 'confirm',
- disabled: false,
- loading: false,
- 'data-qa-selector': 'invite_button',
- },
+ const actionButton = findActionButton();
+
+ expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT);
+ expect(actionButton.attributes('data-qa-selector')).toBe('invite_button');
+
+ expect(actionButton.props()).toMatchObject({
+ variant: 'confirm',
+ disabled: false,
+ loading: false,
});
});
@@ -235,7 +230,7 @@ describe('InviteModalBase', () => {
},
});
- expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true);
+ expect(findActionButton().props('loading')).toBe(true);
});
it('with invalidFeedbackMessage, set members form group exception state', () => {
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index 0455460918c..c7e9905dee3 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -30,11 +30,6 @@ const createComponent = (props) => {
describe('MembersTokenSelect', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
describe('rendering the token-selector component', () => {
diff --git a/spec/frontend/invite_members/components/project_select_spec.js b/spec/frontend/invite_members/components/project_select_spec.js
index 6fbf95362fa..20db4f20408 100644
--- a/spec/frontend/invite_members/components/project_select_spec.js
+++ b/spec/frontend/invite_members/components/project_select_spec.js
@@ -23,10 +23,6 @@ describe('ProjectSelect', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findAvatarLabeled = (index) => wrapper.findAllComponents(GlAvatarLabeled).at(index);
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 59d58f21bb0..67fb1dcbfbd 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -45,4 +45,35 @@ export const user6 = {
avatar_url: '',
};
+export const postData = {
+ user_id: `${user1.id},${user2.id}`,
+ access_level: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ invite_source: inviteSource,
+ format: 'json',
+ tasks_to_be_done: [],
+ tasks_project_id: '',
+};
+
+export const emailPostData = {
+ access_level: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ email: `${user3.name}`,
+ invite_source: inviteSource,
+ tasks_to_be_done: [],
+ tasks_project_id: '',
+ format: 'json',
+};
+
+export const singleUserPostData = {
+ access_level: propsData.defaultAccessLevel,
+ expires_at: undefined,
+ user_id: `${user1.id}`,
+ email: `${user3.name}`,
+ invite_source: inviteSource,
+ tasks_to_be_done: [],
+ tasks_project_id: '',
+ format: 'json',
+};
+
export const GlEmoji = { template: '<img/>' };
diff --git a/spec/frontend/invite_members/utils/member_utils_spec.js b/spec/frontend/invite_members/utils/member_utils_spec.js
index eb76c9845d4..b6fc70038bb 100644
--- a/spec/frontend/invite_members/utils/member_utils_spec.js
+++ b/spec/frontend/invite_members/utils/member_utils_spec.js
@@ -1,4 +1,12 @@
-import { memberName } from '~/invite_members/utils/member_utils';
+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';
+
+jest.mock('~/lib/utils/url_utility');
describe('Member Name', () => {
it.each([
@@ -10,3 +18,23 @@ describe('Member Name', () => {
expect(memberName(member)).toBe(result);
});
});
+
+describe('Trigger External Alert', () => {
+ it('returns false', () => {
+ 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/invite_members/utils/trigger_successful_invite_alert_spec.js b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
index 38b16dd0c2c..6192713f121 100644
--- a/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
+++ b/spec/frontend/invite_members/utils/trigger_successful_invite_alert_spec.js
@@ -6,14 +6,14 @@ import {
TOAST_MESSAGE_LOCALSTORAGE_KEY,
TOAST_MESSAGE_SUCCESSFUL,
} from '~/invite_members/constants';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-jest.mock('~/flash');
+jest.mock('~/alert');
useLocalStorageSpy();
describe('Display Successful Invitation Alert', () => {
- it('does not show alert if localStorage key not present', () => {
+ it('does not show an alert if localStorage key not present', () => {
localStorage.removeItem(TOAST_MESSAGE_LOCALSTORAGE_KEY);
displaySuccessfulInvitationAlert();
@@ -21,7 +21,7 @@ describe('Display Successful Invitation Alert', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('shows alert when localStorage key is present', () => {
+ it('shows an alert when localStorage key is present', () => {
localStorage.setItem(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
displaySuccessfulInvitationAlert();
diff --git a/spec/frontend/issuable/components/csv_export_modal_spec.js b/spec/frontend/issuable/components/csv_export_modal_spec.js
index f798f87b6b2..ccd53e64c4d 100644
--- a/spec/frontend/issuable/components/csv_export_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_export_modal_spec.js
@@ -17,7 +17,7 @@ describe('CsvExportModal', () => {
...props,
},
provide: {
- issuableType: 'issues',
+ issuableType: 'issue',
...injectedProperties,
},
stubs: {
@@ -29,19 +29,15 @@ describe('CsvExportModal', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findModal = () => wrapper.findComponent(GlModal);
const findIcon = () => wrapper.findComponent(GlIcon);
describe('template', () => {
describe.each`
- issuableType | modalTitle
- ${'issues'} | ${'Export issues'}
- ${'merge-requests'} | ${'Export merge requests'}
- `('with the issuableType "$issuableType"', ({ issuableType, modalTitle }) => {
+ issuableType | modalTitle | dataTrackLabel
+ ${'issue'} | ${'Export issues'} | ${'export_issues_csv'}
+ ${'merge_request'} | ${'Export merge requests'} | ${'export_merge-requests_csv'}
+ `('with the issuableType "$issuableType"', ({ issuableType, modalTitle, dataTrackLabel }) => {
beforeEach(() => {
wrapper = createComponent({ injectedProperties: { issuableType } });
});
@@ -57,9 +53,9 @@ describe('CsvExportModal', () => {
href: 'export/csv/path',
variant: 'confirm',
'data-method': 'post',
- 'data-qa-selector': `export_${issuableType}_button`,
+ 'data-qa-selector': `export_issues_button`,
'data-track-action': 'click_button',
- 'data-track-label': `export_${issuableType}_csv`,
+ 'data-track-label': dataTrackLabel,
},
});
});
diff --git a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
index 118c12d968b..0e2f71fa3ee 100644
--- a/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
+++ b/spec/frontend/issuable/components/csv_import_export_buttons_spec.js
@@ -1,5 +1,4 @@
-import { GlButton, GlDropdown } from '@gitlab/ui';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { createMockDirective } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CsvExportModal from '~/issuable/components/csv_export_modal.vue';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
@@ -16,7 +15,7 @@ describe('CsvImportExportButtons', () => {
glModalDirective = jest.fn();
return mountExtended(CsvImportExportButtons, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
glModal: {
bind(_, { value }) {
glModalDirective(value);
@@ -33,12 +32,7 @@ describe('CsvImportExportButtons', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findExportCsvButton = () => wrapper.findComponent(GlButton);
- const findImportDropdown = () => wrapper.findComponent(GlDropdown);
+ const findExportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Export as CSV' });
const findImportCsvButton = () => wrapper.findByRole('menuitem', { name: 'Import CSV' });
const findImportFromJiraLink = () => wrapper.findByRole('menuitem', { name: 'Import from Jira' });
const findExportCsvModal = () => wrapper.findComponent(CsvExportModal);
@@ -54,13 +48,6 @@ describe('CsvImportExportButtons', () => {
expect(findExportCsvButton().exists()).toBe(true);
});
- it('export button has a tooltip', () => {
- const tooltip = getBinding(findExportCsvButton().element, 'gl-tooltip');
-
- expect(tooltip).toBeDefined();
- expect(tooltip.value).toBe('Export as CSV');
- });
-
it('renders the export modal', () => {
expect(findExportCsvModal().props()).toMatchObject({ exportCsvPath, issuableCount });
});
@@ -68,7 +55,7 @@ describe('CsvImportExportButtons', () => {
it('opens the export modal', () => {
findExportCsvButton().trigger('click');
- expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.exportModalId);
+ expect(glModalDirective).toHaveBeenCalled();
});
});
@@ -87,79 +74,38 @@ describe('CsvImportExportButtons', () => {
});
describe('when the showImportButton=true', () => {
- beforeEach(() => {
+ it('renders the import csv menu item', () => {
wrapper = createComponent({ showImportButton: true });
- });
-
- it('displays the import dropdown', () => {
- expect(findImportDropdown().exists()).toBe(true);
- });
- it('renders the import csv menu item', () => {
expect(findImportCsvButton().exists()).toBe(true);
});
- describe('when showLabel=false', () => {
- beforeEach(() => {
- wrapper = createComponent({ showImportButton: true, showLabel: false });
- });
-
- it('hides button text', () => {
- expect(findImportDropdown().props()).toMatchObject({
- text: 'Import issues',
- textSrOnly: true,
- });
- });
-
- it('import button has a tooltip', () => {
- const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip');
-
- expect(tooltip).toBeDefined();
- expect(tooltip.value).toBe('Import issues');
- });
- });
-
- describe('when showLabel=true', () => {
- beforeEach(() => {
- wrapper = createComponent({ showImportButton: true, showLabel: true });
- });
-
- it('displays a button text', () => {
- expect(findImportDropdown().props()).toMatchObject({
- text: 'Import issues',
- textSrOnly: false,
- });
- });
-
- it('import button has no tooltip', () => {
- const tooltip = getBinding(findImportDropdown().element, 'gl-tooltip');
-
- expect(tooltip.value).toBe(null);
- });
- });
-
it('renders the import modal', () => {
+ wrapper = createComponent({ showImportButton: true });
+
expect(findImportCsvModal().exists()).toBe(true);
});
it('opens the import modal', () => {
+ wrapper = createComponent({ showImportButton: true });
+
findImportCsvButton().trigger('click');
- expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.importModalId);
+ expect(glModalDirective).toHaveBeenCalled();
});
describe('import from jira link', () => {
const projectImportJiraPath = 'gitlab-org/gitlab-test/-/import/jira';
- beforeEach(() => {
- wrapper = createComponent({
- showImportButton: true,
- canEdit: true,
- projectImportJiraPath,
+ describe('when canEdit=true', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ showImportButton: true,
+ canEdit: true,
+ projectImportJiraPath,
+ });
});
- });
- describe('when canEdit=true', () => {
it('renders the import dropdown item', () => {
expect(findImportFromJiraLink().exists()).toBe(true);
});
@@ -186,8 +132,8 @@ describe('CsvImportExportButtons', () => {
wrapper = createComponent({ showImportButton: false });
});
- it('does not display the import dropdown', () => {
- expect(findImportDropdown().exists()).toBe(false);
+ it('does not render the import csv menu item', () => {
+ expect(findImportCsvButton().exists()).toBe(false);
});
it('does not render the import modal', () => {
diff --git a/spec/frontend/issuable/components/csv_import_modal_spec.js b/spec/frontend/issuable/components/csv_import_modal_spec.js
index 6e954c91f46..9069d2b3ab3 100644
--- a/spec/frontend/issuable/components/csv_import_modal_spec.js
+++ b/spec/frontend/issuable/components/csv_import_modal_spec.js
@@ -32,10 +32,6 @@ describe('CsvImportModal', () => {
formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findModal = () => wrapper.findComponent(GlModal);
const findForm = () => wrapper.find('form');
const findFileInput = () => wrapper.findByLabelText('Upload CSV file');
diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js
index b04a6c0b8fd..4cc5775b54e 100644
--- a/spec/frontend/issuable/components/issuable_by_email_spec.js
+++ b/spec/frontend/issuable/components/issuable_by_email_spec.js
@@ -53,8 +53,6 @@ describe('IssuableByEmail', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
});
diff --git a/spec/frontend/issuable/components/issuable_header_warnings_spec.js b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
index 99aa6778e1e..ff772040d22 100644
--- a/spec/frontend/issuable/components/issuable_header_warnings_spec.js
+++ b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
@@ -25,16 +25,11 @@ describe('IssuableHeaderWarnings', () => {
store,
provide,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
issuableType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
diff --git a/spec/frontend/issuable/components/issue_assignees_spec.js b/spec/frontend/issuable/components/issue_assignees_spec.js
index 9a33bfae240..8ed51120508 100644
--- a/spec/frontend/issuable/components/issue_assignees_spec.js
+++ b/spec/frontend/issuable/components/issue_assignees_spec.js
@@ -21,11 +21,6 @@ describe('IssueAssigneesComponent', () => {
vm = wrapper.vm;
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text();
const findAvatars = () => wrapper.findAllComponents(UserAvatarLink);
const findOverflowCounter = () => wrapper.find('.avatar-counter');
diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js
index eac53c5f761..232d6177862 100644
--- a/spec/frontend/issuable/components/issue_milestone_spec.js
+++ b/spec/frontend/issuable/components/issue_milestone_spec.js
@@ -1,160 +1,61 @@
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-
import { mockMilestone } from 'jest/boards/mock_data';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
-const createComponent = (milestone = mockMilestone) => {
- const Component = Vue.extend(IssueMilestone);
-
- return shallowMount(Component, {
- propsData: {
- milestone,
- },
- });
-};
-
-describe('IssueMilestoneComponent', () => {
+describe('IssueMilestone component', () => {
let wrapper;
- let vm;
- beforeEach(async () => {
- wrapper = createComponent();
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
- ({ vm } = wrapper);
+ const createComponent = (milestone = mockMilestone) =>
+ shallowMount(IssueMilestone, { propsData: { milestone } });
- await nextTick();
+ beforeEach(() => {
+ wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
+ it('renders milestone icon', () => {
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
});
- describe('computed', () => {
- describe('isMilestoneStarted', () => {
- it('should return `false` when milestoneStart prop is not defined', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '' },
- });
- await nextTick();
-
- expect(wrapper.vm.isMilestoneStarted).toBe(false);
- });
-
- it('should return `true` when milestone start date is past current date', async () => {
- await wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '1990-07-22' },
- });
- await nextTick();
+ it('renders milestone title', () => {
+ expect(wrapper.find('.milestone-title').text()).toBe(mockMilestone.title);
+ });
- expect(wrapper.vm.isMilestoneStarted).toBe(true);
- });
+ describe('tooltip', () => {
+ it('renders `Milestone`', () => {
+ expect(findTooltip().text()).toContain('Milestone');
});
- describe('isMilestonePastDue', () => {
- it('should return `false` when milestoneDue prop is not defined', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, due_date: '' },
- });
- await nextTick();
-
- expect(wrapper.vm.isMilestonePastDue).toBe(false);
- });
-
- it('should return `true` when milestone due is past current date', () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, due_date: '1990-07-22' },
- });
-
- expect(wrapper.vm.isMilestonePastDue).toBe(true);
- });
+ it('renders milestone title', () => {
+ expect(findTooltip().text()).toContain(mockMilestone.title);
});
- describe('milestoneDatesAbsolute', () => {
- it('returns string containing absolute milestone due date', () => {
- expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
- });
+ describe('humanized dates', () => {
+ it('renders `Expired` when there is a due date in the past', () => {
+ wrapper = createComponent({ ...mockMilestone, due_date: '2019-12-31', start_date: '' });
- it('returns string containing absolute milestone start date when due date is not present', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, due_date: '' },
- });
- await nextTick();
-
- expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ expect(findTooltip().text()).toContain('Expired 6 months ago(December 31, 2019)');
});
- it('returns empty string when both milestone start and due dates are not present', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '', due_date: '' },
- });
- await nextTick();
+ it('renders `remaining` when there is a due date in the future', () => {
+ wrapper = createComponent({ ...mockMilestone, due_date: '2020-12-31', start_date: '' });
- expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
+ expect(findTooltip().text()).toContain('5 months remaining(December 31, 2020)');
});
- });
- describe('milestoneDatesHuman', () => {
- it('returns string containing milestone due date when date is yet to be due', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` },
- });
- await nextTick();
+ it('renders `Started` when there is a start date in the past', () => {
+ wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2019-12-31' });
- expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
+ expect(findTooltip().text()).toContain('Started 6 months ago(December 31, 2019)');
});
- it('returns string containing milestone start date when date has already started and due date is not present', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' },
- });
- await nextTick();
+ it('renders `Starts` when there is a start date in the future', () => {
+ wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2020-12-31' });
- expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
+ expect(findTooltip().text()).toContain('Starts in 5 months(December 31, 2020)');
});
-
- it('returns string containing milestone start date when date is yet to start and due date is not present', async () => {
- wrapper.setProps({
- milestone: {
- ...mockMilestone,
- start_date: `${new Date().getFullYear() + 10}-01-01`,
- due_date: '',
- },
- });
- await nextTick();
-
- expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
- });
-
- it('returns empty string when milestone start and due dates are not present', async () => {
- wrapper.setProps({
- milestone: { ...mockMilestone, start_date: '', due_date: '' },
- });
- await nextTick();
-
- expect(wrapper.vm.milestoneDatesHuman).toBe('');
- });
- });
- });
-
- describe('template', () => {
- it('renders component root element with class `issue-milestone-details`', () => {
- expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
- });
-
- it('renders milestone icon', () => {
- expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
- });
-
- it('renders milestone title', () => {
- expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
- });
-
- it('renders milestone tooltip', () => {
- expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
- mockMilestone.title,
- );
});
});
});
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 3f9f048605a..7322894164b 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -1,14 +1,18 @@
import { GlIcon, GlLink, GlButton } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import { stubComponent } from 'helpers/stub_component';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
import IssueAssignees from '~/issuable/components/issue_assignees.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -18,9 +22,11 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('RelatedIssuableItem', () => {
let wrapper;
+ let showModalSpy;
const defaultProps = {
idKey: 1,
+ iid: 1,
displayReference: 'gitlab-org/gitlab-test#1',
pathIdSeparator: '#',
path: `${TEST_HOST}/path`,
@@ -40,23 +46,31 @@ describe('RelatedIssuableItem', () => {
const findRemoveButton = () => wrapper.findComponent(GlButton);
const findTitleLink = () => wrapper.findComponent(GlLink);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
function mountComponent({ data = {}, props = {} } = {}) {
+ showModalSpy = jest.fn();
wrapper = shallowMount(RelatedIssuableItem, {
propsData: {
...defaultProps,
...props,
},
+ provide: {
+ reportAbusePath: '/report/abuse/path',
+ },
+ stubs: {
+ WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
+ methods: {
+ show: showModalSpy,
+ },
+ }),
+ },
data() {
return data;
},
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains issuable-info-container class when canReorder is false', () => {
mountComponent({ props: { canReorder: false } });
@@ -181,7 +195,7 @@ describe('RelatedIssuableItem', () => {
});
it('renders disabled button when removeDisabled', () => {
- expect(findRemoveButton().attributes('disabled')).toBe('true');
+ expect(findRemoveButton().attributes('disabled')).toBeDefined();
});
it('triggers onRemoveRequest when clicked', () => {
@@ -208,12 +222,15 @@ describe('RelatedIssuableItem', () => {
});
describe('work item modal', () => {
- const workItem = 'gid://gitlab/WorkItem/1';
+ const workItemId = 'gid://gitlab/WorkItem/1';
it('renders', () => {
mountComponent();
- expect(findWorkItemDetailModal().props('workItemId')).toBe(workItem);
+ expect(findWorkItemDetailModal().props()).toMatchObject({
+ workItemId,
+ workItemIid: '1',
+ });
});
describe('when work item is issue and the related issue title is clicked', () => {
@@ -240,7 +257,7 @@ describe('RelatedIssuableItem', () => {
it('updates the url params with the work item id', () => {
expect(updateHistory).toHaveBeenCalledWith({
- url: `${TEST_HOST}/?work_item_id=1`,
+ url: `${TEST_HOST}/?work_item_iid=1`,
replace: true,
});
});
@@ -250,9 +267,9 @@ describe('RelatedIssuableItem', () => {
it('emits "relatedIssueRemoveRequest" event', () => {
mountComponent();
- findWorkItemDetailModal().vm.$emit('workItemDeleted', workItem);
+ findWorkItemDetailModal().vm.$emit('workItemDeleted', workItemId);
- expect(wrapper.emitted('relatedIssueRemoveRequest')).toEqual([[workItem]]);
+ expect(wrapper.emitted('relatedIssueRemoveRequest')).toEqual([[workItemId]]);
});
});
@@ -269,4 +286,30 @@ describe('RelatedIssuableItem', () => {
});
});
});
+
+ describe('abuse category selector', () => {
+ beforeEach(() => {
+ mountComponent({ props: { workItemType: 'TASK' } });
+ findTitleLink().vm.$emit('click', { preventDefault: () => {} });
+ });
+
+ it('should not be visible by default', () => {
+ expect(showModalSpy).toHaveBeenCalled();
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+
+ it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
+ findWorkItemDetailModal().vm.$emit('openReportAbuse', mockWorkItemCommentNote);
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+
+ findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js
index 728b8958b9b..d26f287d90c 100644
--- a/spec/frontend/issuable/components/status_box_spec.js
+++ b/spec/frontend/issuable/components/status_box_spec.js
@@ -11,11 +11,6 @@ function factory(propsData) {
describe('Merge request status box component', () => {
const findBadge = () => wrapper.findComponent(GlBadge);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
issuableType | badgeText | initialState | badgeClass | badgeVariant | badgeIcon
${'merge_request'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'merge-request-open'}
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index 3e778e50fb8..d7e5f9083b0 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -3,11 +3,18 @@ import Autosave from '~/autosave';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
-
+import { confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
import { getSaveableFormChildren } from './helpers';
jest.mock('~/autosave');
+jest.mock('~/lib/utils/secret_detection', () => {
+ return {
+ ...jest.requireActual('~/lib/utils/secret_detection'),
+ confirmSensitiveAction: jest.fn(() => Promise.resolve(false)),
+ };
+});
+
const createIssuable = (form) => {
return new IssuableForm(form);
};
@@ -35,16 +42,13 @@ describe('IssuableForm', () => {
describe('autosave', () => {
let $title;
- let $description;
beforeEach(() => {
$title = $form.find('input[name*="[title]"]').get(0);
- $description = $form.find('textarea[name*="[description]"]').get(0);
});
afterEach(() => {
$title = null;
- $description = null;
});
describe('initAutosave', () => {
@@ -64,11 +68,6 @@ describe('IssuableForm', () => {
['/foo', 'bar=true', 'title'],
'autosave//foo/bar=true=title',
);
- expect(Autosave).toHaveBeenCalledWith(
- $description,
- ['/foo', 'bar=true', 'description'],
- 'autosave//foo/bar=true=description',
- );
});
it("creates autosave fields without the searchTerm if it's an issue new form", () => {
@@ -81,11 +80,6 @@ describe('IssuableForm', () => {
['/issues/new', '', 'title'],
'autosave//issues/new/bar=true=title',
);
- expect(Autosave).toHaveBeenCalledWith(
- $description,
- ['/issues/new', '', 'description'],
- 'autosave//issues/new/bar=true=description',
- );
});
it.each([
@@ -106,7 +100,9 @@ describe('IssuableForm', () => {
const children = getSaveableFormChildren($form[0]);
- expect(Autosave).toHaveBeenCalledTimes(children.length);
+ // description autosave is being handled separately
+ // hence we're using children.length - 1
+ expect(Autosave).toHaveBeenCalledTimes(children.length - 1);
expect(Autosave).toHaveBeenLastCalledWith(
$input.get(0),
['/', '', id],
@@ -116,13 +112,12 @@ describe('IssuableForm', () => {
});
describe('resetAutosave', () => {
- it('calls reset on title and description', () => {
+ it('calls reset on title', () => {
instance = createIssuable($form);
instance.resetAutosave();
expect(instance.autosaves.get('title').reset).toHaveBeenCalledTimes(1);
- expect(instance.autosaves.get('description').reset).toHaveBeenCalledTimes(1);
});
it('resets autosave when submit', () => {
@@ -245,4 +240,44 @@ describe('IssuableForm', () => {
);
});
});
+
+ describe('Checks for sensitive token', () => {
+ let issueDescription;
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+
+ beforeEach(() => {
+ issueDescription = $form.find('textarea[name*="[description]"]').get(0);
+ });
+
+ afterEach(() => {
+ issueDescription = null;
+ });
+
+ it('submits the form when no token is present', () => {
+ issueDescription.value = 'sample message';
+
+ const handleSubmit = jest.spyOn(IssuableForm.prototype, 'handleSubmit');
+ const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
+ createIssuable($form);
+
+ $form.submit();
+
+ expect(handleSubmit).toHaveBeenCalled();
+ expect(resetAutosave).toHaveBeenCalled();
+ });
+
+ it('prevents form submission when token is present', () => {
+ issueDescription.value = sensitiveMessage;
+
+ const handleSubmit = jest.spyOn(IssuableForm.prototype, 'handleSubmit');
+ const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave');
+ createIssuable($form);
+
+ $form.submit();
+
+ expect(handleSubmit).toHaveBeenCalled();
+ expect(confirmSensitiveAction).toHaveBeenCalledWith(i18n.descriptionPrompt);
+ expect(resetAutosave).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js
index 444165f61c7..a7605016039 100644
--- a/spec/frontend/issuable/popover/components/issue_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js
@@ -33,10 +33,6 @@ describe('Issue Popover', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows skeleton-loader while apollo is loading', () => {
mountComponent();
diff --git a/spec/frontend/issuable/popover/components/mr_popover_spec.js b/spec/frontend/issuable/popover/components/mr_popover_spec.js
index 5fdd1e6e8fc..5b29ecfc0ba 100644
--- a/spec/frontend/issuable/popover/components/mr_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/mr_popover_spec.js
@@ -71,10 +71,6 @@ describe('MR Popover', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows skeleton-loader while apollo is loading', () => {
mountComponent();
@@ -99,7 +95,7 @@ describe('MR Popover', () => {
expect(wrapper.text()).toContain('foo/bar!1');
});
- it('shows CI Icon if there is pipeline data', async () => {
+ it('shows CI Icon if there is pipeline data', () => {
expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
});
});
@@ -112,7 +108,7 @@ describe('MR Popover', () => {
return waitForPromises();
});
- it('does not show CI icon if there is no pipeline data', async () => {
+ it('does not show CI icon if there is no pipeline data', () => {
expect(wrapper.findComponent(CiIcon).exists()).toBe(false);
});
});
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
index 72fcab63ba7..f90b9117688 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -1,9 +1,9 @@
-import { GlFormGroup } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
import IssueToken from '~/related_issues/components/issue_token.vue';
+import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
import { linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
const issuable1 = {
@@ -26,71 +26,60 @@ const issuable2 = {
const pathIdSeparator = PathIdSeparator.Issue;
-const findFormInput = (wrapper) => wrapper.find('input').element;
-
-const findRadioInput = (inputs, value) =>
- inputs.filter((input) => input.element.value === value)[0];
-
-const findRadioInputs = (wrapper) => wrapper.findAll('[name="linked-issue-type-radio"]');
-
-const constructWrapper = (props) => {
- return shallowMount(AddIssuableForm, {
- propsData: {
- inputValue: '',
- pendingReferences: [],
- pathIdSeparator,
- ...props,
- },
- });
-};
-
describe('AddIssuableForm', () => {
let wrapper;
- afterEach(() => {
- // Jest doesn't blur an item even if it is destroyed,
- // so blur the input manually after each test
- const input = findFormInput(wrapper);
- if (input) input.blur();
+ const createComponent = (props = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(AddIssuableForm, {
+ propsData: {
+ inputValue: '',
+ pendingReferences: [],
+ pathIdSeparator,
+ ...props,
+ },
+ stubs: {
+ RelatedIssuableInput,
+ },
+ });
+ };
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
+ const findAddIssuableForm = () => wrapper.find('form');
+ const findFormInput = () => wrapper.find('input').element;
+ const findRadioInput = (inputs, value) =>
+ inputs.filter((input) => input.element.value === value)[0];
+ const findAllIssueTokens = () => wrapper.findAllComponents(IssueToken);
+ const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findRadioInputs = () => wrapper.findAllComponents(GlFormRadio);
+
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findFormButtons = () => wrapper.findAllComponents(GlButton);
+ const findSubmitButton = () => findFormButtons().at(0);
+ const findRelatedIssuableInput = () => wrapper.findComponent(RelatedIssuableInput);
describe('with data', () => {
describe('without references', () => {
describe('without any input text', () => {
beforeEach(() => {
- wrapper = shallowMount(AddIssuableForm, {
- propsData: {
- inputValue: '',
- pendingReferences: [],
- pathIdSeparator,
- },
- });
+ createComponent();
});
it('should have disabled submit button', () => {
- expect(wrapper.vm.$refs.addButton.disabled).toBe(true);
- expect(wrapper.vm.$refs.loadingIcon).toBeUndefined();
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ expect(findSubmitButton().props('loading')).toBe(false);
});
});
describe('with input text', () => {
beforeEach(() => {
- wrapper = shallowMount(AddIssuableForm, {
- propsData: {
- inputValue: 'foo',
- pendingReferences: [],
- pathIdSeparator,
- },
+ createComponent({
+ inputValue: 'foo',
+ pendingReferences: [],
+ pathIdSeparator,
});
});
it('should not have disabled submit button', () => {
- expect(wrapper.vm.$refs.addButton.disabled).toBe(false);
+ expect(findSubmitButton().props('disabled')).toBe(false);
});
});
});
@@ -99,59 +88,56 @@ describe('AddIssuableForm', () => {
const inputValue = 'foo #123';
beforeEach(() => {
- wrapper = mount(AddIssuableForm, {
- propsData: {
- inputValue,
- pendingReferences: [issuable1.reference, issuable2.reference],
- pathIdSeparator,
- },
+ createComponent({
+ inputValue,
+ pendingReferences: [issuable1.reference, issuable2.reference],
+ pathIdSeparator,
});
- });
+ }, mount);
it('should put input value in place', () => {
expect(findFormInput(wrapper).value).toBe(inputValue);
});
it('should render pending issuables items', () => {
- expect(wrapper.findAllComponents(IssueToken)).toHaveLength(2);
+ expect(findAllIssueTokens()).toHaveLength(2);
});
it('should not have disabled submit button', () => {
- expect(wrapper.vm.$refs.addButton.disabled).toBe(false);
+ expect(findSubmitButton().props('disabled')).toBe(false);
});
});
describe('when issuable type is "issue"', () => {
beforeEach(() => {
- wrapper = mount(AddIssuableForm, {
- propsData: {
+ createComponent(
+ {
inputValue: '',
issuableType: TYPE_ISSUE,
pathIdSeparator,
pendingReferences: [],
},
- });
+ mount,
+ );
});
it('does not show radio inputs', () => {
- expect(findRadioInputs(wrapper).length).toBe(0);
+ expect(findRadioInputs()).toHaveLength(0);
});
});
describe('when issuable type is "epic"', () => {
beforeEach(() => {
- wrapper = shallowMount(AddIssuableForm, {
- propsData: {
- inputValue: '',
- issuableType: TYPE_EPIC,
- pathIdSeparator,
- pendingReferences: [],
- },
+ createComponent({
+ inputValue: '',
+ issuableType: TYPE_EPIC,
+ pathIdSeparator,
+ pendingReferences: [],
});
});
it('does not show radio inputs', () => {
- expect(findRadioInputs(wrapper).length).toBe(0);
+ expect(findRadioInputs()).toHaveLength(0);
});
});
@@ -163,17 +149,15 @@ describe('AddIssuableForm', () => {
`(
'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType',
({ issuableType, contextHeader, contextFooter }) => {
- wrapper = shallowMount(AddIssuableForm, {
- propsData: {
- issuableType,
- inputValue: '',
- showCategorizedIssues: true,
- pathIdSeparator,
- pendingReferences: [],
- },
+ createComponent({
+ issuableType,
+ inputValue: '',
+ showCategorizedIssues: true,
+ pathIdSeparator,
+ pendingReferences: [],
});
- expect(wrapper.findComponent(GlFormGroup).attributes('label')).toBe(contextHeader);
+ expect(findFormGroup().attributes('label')).toBe(contextHeader);
expect(wrapper.find('p.bold').text()).toContain(contextFooter);
},
);
@@ -181,26 +165,24 @@ describe('AddIssuableForm', () => {
describe('when it is a Linked Issues form', () => {
beforeEach(() => {
- wrapper = mount(AddIssuableForm, {
- propsData: {
- inputValue: '',
- showCategorizedIssues: true,
- issuableType: TYPE_ISSUE,
- pathIdSeparator,
- pendingReferences: [],
- },
+ createComponent({
+ inputValue: '',
+ showCategorizedIssues: true,
+ issuableType: TYPE_ISSUE,
+ pathIdSeparator,
+ pendingReferences: [],
});
});
it('shows radio inputs to allow categorisation of blocking issues', () => {
- expect(findRadioInputs(wrapper).length).toBeGreaterThan(0);
+ expect(findRadioGroup().props('options').length).toBeGreaterThan(0);
});
describe('form radio buttons', () => {
let radioInputs;
beforeEach(() => {
- radioInputs = findRadioInputs(wrapper);
+ radioInputs = findRadioInputs();
});
it('shows "relates to" option', () => {
@@ -216,58 +198,59 @@ describe('AddIssuableForm', () => {
});
it('shows 3 options in total', () => {
- expect(radioInputs.length).toBe(3);
+ expect(findRadioGroup().props('options')).toHaveLength(3);
});
});
describe('when the form is submitted', () => {
- it('emits an event with a "relates_to" link type when the "relates to" radio input selected', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- wrapper.vm.linkedIssueType = linkedIssueTypesMap.RELATES_TO;
- wrapper.vm.onFormSubmit();
-
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.RELATES_TO,
- });
+ it('emits an event with a "relates_to" link type when the "relates to" radio input selected', () => {
+ findAddIssuableForm().trigger('submit');
+
+ expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([
+ [
+ {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.RELATES_TO,
+ },
+ ],
+ ]);
});
- it('emits an event with a "blocks" link type when the "blocks" radio input selected', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- wrapper.vm.linkedIssueType = linkedIssueTypesMap.BLOCKS;
- wrapper.vm.onFormSubmit();
-
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.BLOCKS,
- });
+ it('emits an event with a "blocks" link type when the "blocks" radio input selected', () => {
+ findRadioGroup().vm.$emit('input', linkedIssueTypesMap.BLOCKS);
+ findAddIssuableForm().trigger('submit');
+
+ expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([
+ [
+ {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.BLOCKS,
+ },
+ ],
+ ]);
});
- it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- wrapper.vm.linkedIssueType = linkedIssueTypesMap.IS_BLOCKED_BY;
- wrapper.vm.onFormSubmit();
-
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormSubmit', {
- pendingReferences: '',
- linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY,
- });
+ it('emits an event with a "is_blocked_by" link type when the "is blocked by" radio input selected', () => {
+ findRadioGroup().vm.$emit('input', linkedIssueTypesMap.IS_BLOCKED_BY);
+ findAddIssuableForm().trigger('submit');
+
+ expect(wrapper.emitted('addIssuableFormSubmit')).toEqual([
+ [
+ {
+ pendingReferences: '',
+ linkedIssueType: linkedIssueTypesMap.IS_BLOCKED_BY,
+ },
+ ],
+ ]);
});
- it('shows error message when error is present', async () => {
+ it('shows error message when error is present', () => {
const itemAddFailureMessage = 'Something went wrong while submitting.';
- wrapper.setProps({
+ createComponent({
hasError: true,
itemAddFailureMessage,
});
- await nextTick();
expect(wrapper.find('.gl-field-error').exists()).toBe(true);
expect(wrapper.find('.gl-field-error').text()).toContain(itemAddFailureMessage);
});
@@ -283,27 +266,31 @@ describe('AddIssuableForm', () => {
};
it('returns autocomplete object', () => {
- wrapper = constructWrapper({
+ createComponent({
autoCompleteSources,
});
- expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources);
+ expect(findRelatedIssuableInput().props('autoCompleteSources')).toEqual(
+ autoCompleteSources,
+ );
- wrapper = constructWrapper({
+ createComponent({
autoCompleteSources,
confidential: false,
});
- expect(wrapper.vm.transformedAutocompleteSources).toBe(autoCompleteSources);
+ expect(findRelatedIssuableInput().props('autoCompleteSources')).toEqual(
+ autoCompleteSources,
+ );
});
it('returns autocomplete sources with query `confidential_only`, when it is confidential', () => {
- wrapper = constructWrapper({
+ createComponent({
autoCompleteSources,
confidential: true,
});
- const actualSources = wrapper.vm.transformedAutocompleteSources;
+ const actualSources = findRelatedIssuableInput().props('autoCompleteSources');
expect(actualSources.epics).toContain('?confidential_only=true');
expect(actualSources.issues).toContain('?confidential_only=true');
diff --git a/spec/frontend/issuable/related_issues/components/issue_token_spec.js b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
index bacebbade7f..4f2a96306e3 100644
--- a/spec/frontend/issuable/related_issues/components/issue_token_spec.js
+++ b/spec/frontend/issuable/related_issues/components/issue_token_spec.js
@@ -24,13 +24,6 @@ describe('IssueToken', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findLink = () => wrapper.findComponent({ ref: 'link' });
const findReference = () => wrapper.findComponent({ ref: 'reference' });
const findReferenceIcon = () => wrapper.find('[data-testid="referenceIcon"]');
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index ff8d5073005..e97c0312181 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlCard } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
issuable1,
@@ -22,21 +22,44 @@ describe('RelatedIssuesBlock', () => {
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button');
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
+ const createComponent = ({
+ mountFn = mountExtended,
+ pathIdSeparator = PathIdSeparator.Issue,
+ issuableType = TYPE_ISSUE,
+ canAdmin = false,
+ helpPath = '',
+ isFetching = false,
+ isFormVisible = false,
+ relatedIssues = [],
+ showCategorizedIssues = false,
+ autoCompleteEpics = true,
+ slots = '',
+ } = {}) => {
+ wrapper = mountFn(RelatedIssuesBlock, {
+ propsData: {
+ pathIdSeparator,
+ issuableType,
+ canAdmin,
+ helpPath,
+ isFetching,
+ isFormVisible,
+ relatedIssues,
+ showCategorizedIssues,
+ autoCompleteEpics,
+ },
+ provide: {
+ reportAbusePath: '/report/abuse/path',
+ },
+ stubs: {
+ GlCard,
+ },
+ slots,
+ });
+ };
describe('with defaults', () => {
beforeEach(() => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: TYPE_ISSUE,
- },
- });
+ createComponent();
});
it.each`
@@ -46,13 +69,11 @@ describe('RelatedIssuesBlock', () => {
`(
'displays "$titleText" in the header and "$addButtonText" aria-label for add button when issuableType is set to "$issuableType"',
({ issuableType, pathIdSeparator, titleText, addButtonText }) => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator,
- issuableType,
- canAdmin: true,
- helpPath: '/help/user/project/issues/related_issues',
- },
+ createComponent({
+ pathIdSeparator,
+ issuableType,
+ canAdmin: true,
+ helpPath: '/help/user/project/issues/related_issues',
});
expect(wrapper.find('.card-title').text()).toContain(titleText);
@@ -73,11 +94,8 @@ describe('RelatedIssuesBlock', () => {
it('displays header text slot data', () => {
const headerText = '<div>custom header text</div>';
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- },
+ createComponent({
+ mountFn: shallowMountExtended,
slots: { 'header-text': headerText },
});
@@ -89,11 +107,8 @@ describe('RelatedIssuesBlock', () => {
it('displays header actions slot data', () => {
const headerActions = '<button data-testid="custom-button">custom button</button>';
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- },
+ createComponent({
+ mountFn: shallowMountExtended,
slots: { 'header-actions': headerActions },
});
@@ -103,12 +118,8 @@ describe('RelatedIssuesBlock', () => {
describe('with isFetching=true', () => {
beforeEach(() => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- isFetching: true,
- issuableType: 'issue',
- },
+ createComponent({
+ isFetching: true,
});
});
@@ -119,13 +130,7 @@ describe('RelatedIssuesBlock', () => {
describe('with canAddRelatedIssues=true', () => {
beforeEach(() => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- canAdmin: true,
- issuableType: 'issue',
- },
- });
+ createComponent({ canAdmin: true });
});
it('can add new related issues', () => {
@@ -135,14 +140,7 @@ describe('RelatedIssuesBlock', () => {
describe('with isFormVisible=true', () => {
beforeEach(() => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- isFormVisible: true,
- issuableType: 'issue',
- autoCompleteEpics: false,
- },
- });
+ createComponent({ isFormVisible: true, autoCompleteEpics: false });
});
it('shows add related issues form', () => {
@@ -158,19 +156,14 @@ describe('RelatedIssuesBlock', () => {
const issueList = () => wrapper.findAll('.js-related-issues-token-list-item');
const categorizedHeadings = () => wrapper.findAll('h4');
const headingTextAt = (index) => categorizedHeadings().at(index).text();
- const mountComponent = (showCategorizedIssues) => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [issuable1, issuable2, issuable3],
- issuableType: 'issue',
- showCategorizedIssues,
- },
- });
- };
describe('when showCategorizedIssues=true', () => {
- beforeEach(() => mountComponent(true));
+ beforeEach(() =>
+ createComponent({
+ showCategorizedIssues: true,
+ relatedIssues: [issuable1, issuable2, issuable3],
+ }),
+ );
it('should render issue tokens items', () => {
expect(issueList()).toHaveLength(3);
@@ -197,8 +190,10 @@ describe('RelatedIssuesBlock', () => {
describe('when showCategorizedIssues=false', () => {
it('should render issues as a flat list with no header', () => {
- mountComponent(false);
-
+ createComponent({
+ showCategorizedIssues: false,
+ relatedIssues: [issuable1, issuable2, issuable3],
+ });
expect(issueList()).toHaveLength(3);
expect(categorizedHeadings()).toHaveLength(0);
});
@@ -217,11 +212,8 @@ describe('RelatedIssuesBlock', () => {
},
].forEach(({ issuableType, icon }) => {
it(`issuableType=${issuableType} is passed`, () => {
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType,
- },
+ createComponent({
+ issuableType,
});
const iconComponent = wrapper.findComponent(GlIcon);
@@ -233,12 +225,8 @@ describe('RelatedIssuesBlock', () => {
describe('toggle', () => {
beforeEach(() => {
- wrapper = shallowMountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [issuable1, issuable2, issuable3],
- issuableType: TYPE_ISSUE,
- },
+ createComponent({
+ relatedIssues: [issuable1, issuable2, issuable3],
});
});
@@ -268,14 +256,12 @@ describe('RelatedIssuesBlock', () => {
`(
'displays "$emptyText" in the body and "$helpLinkText" aria-label for help link',
({ issuableType, pathIdSeparator, showCategorizedIssues, emptyText, helpLinkText }) => {
- wrapper = mountExtended(RelatedIssuesBlock, {
- propsData: {
- pathIdSeparator,
- issuableType,
- canAdmin: true,
- helpPath: '/help/user/project/issues/related_issues',
- showCategorizedIssues,
- },
+ createComponent({
+ pathIdSeparator,
+ issuableType,
+ canAdmin: true,
+ helpPath: '/help/user/project/issues/related_issues',
+ showCategorizedIssues,
});
expect(wrapper.findByTestId('related-issues-body').text()).toContain(emptyText);
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
index 9bb71ec3dcb..592dc19f0ea 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_list_spec.js
@@ -13,25 +13,35 @@ import { PathIdSeparator } from '~/related_issues/constants';
describe('RelatedIssuesList', () => {
let wrapper;
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
+ const createComponent = ({
+ mountFn = shallowMount,
+ pathIdSeparator = PathIdSeparator.Issue,
+ issuableType = 'issue',
+ listLinkType = 'relates_to',
+ heading = '',
+ isFetching = false,
+ relatedIssues = [],
+ } = {}) => {
+ wrapper = mountFn(RelatedIssuesList, {
+ propsData: {
+ pathIdSeparator,
+ issuableType,
+ listLinkType,
+ heading,
+ isFetching,
+ relatedIssues,
+ },
+ provide: {
+ reportAbusePath: '/report/abuse/path',
+ },
+ });
+ };
describe('with defaults', () => {
const heading = 'Related to';
beforeEach(() => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- listLinkType: 'relates_to',
- heading,
- },
- });
+ createComponent({ heading });
});
it('assigns value of listLinkType prop to data attribute', () => {
@@ -49,13 +59,7 @@ describe('RelatedIssuesList', () => {
describe('with isFetching=true', () => {
beforeEach(() => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- isFetching: true,
- issuableType: 'issue',
- },
- });
+ createComponent({ isFetching: true });
});
it('should show loading icon', () => {
@@ -65,13 +69,7 @@ describe('RelatedIssuesList', () => {
describe('methods', () => {
beforeEach(() => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5],
- issuableType: 'issue',
- },
- });
+ createComponent({ relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5] });
});
it('updates the order correctly when an item is moved to the top', () => {
@@ -112,23 +110,17 @@ describe('RelatedIssuesList', () => {
});
describe('issuableOrderingId returns correct issuable order id when', () => {
- it('issuableType is epic', () => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- },
+ it('issuableType is issue', () => {
+ createComponent({
+ issuableType: 'issue',
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.epicIssueId);
});
- it('issuableType is issue', () => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'epic',
- },
+ it('issuableType is epic', () => {
+ createComponent({
+ issuableType: 'epic',
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.id);
@@ -143,12 +135,9 @@ describe('RelatedIssuesList', () => {
});
it('issuableType is epic', () => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'epic',
- relatedIssues,
- },
+ createComponent({
+ issuableType: 'epic',
+ relatedIssues,
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
@@ -159,12 +148,9 @@ describe('RelatedIssuesList', () => {
});
it('issuableType is issue', () => {
- wrapper = shallowMount(RelatedIssuesList, {
- propsData: {
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: 'issue',
- relatedIssues,
- },
+ createComponent({
+ issuableType: 'issue',
+ relatedIssues,
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
@@ -177,13 +163,7 @@ describe('RelatedIssuesList', () => {
describe('related item contents', () => {
beforeAll(() => {
- wrapper = mount(RelatedIssuesList, {
- propsData: {
- issuableType: 'issue',
- pathIdSeparator: PathIdSeparator.Issue,
- relatedIssues: [issuable1],
- },
- });
+ createComponent({ mountFn: mount, relatedIssues: [issuable1] });
});
it('shows due date', () => {
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index 96c0b87e2cb..b119c836411 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -7,7 +7,7 @@ import {
issuable1,
issuable2,
} from 'jest/issuable/components/related_issuable_mock_data';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
HTTP_STATUS_CONFLICT,
@@ -19,7 +19,7 @@ import RelatedIssuesBlock from '~/related_issues/components/related_issues_block
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import relatedIssuesService from '~/related_issues/services/related_issues_service';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RelatedIssuesRoot', () => {
let wrapper;
@@ -34,7 +34,6 @@ describe('RelatedIssuesRoot', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
const createComponent = ({ props = {}, data = {} } = {}) => {
@@ -43,6 +42,9 @@ describe('RelatedIssuesRoot', () => {
...defaultProps,
...props,
},
+ provide: {
+ reportAbusePath: '/report/abuse/path',
+ },
data() {
return data;
},
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index cc2ee84348a..21ae844e2dd 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -65,6 +65,14 @@ describe('CreateMergeRequestDropdown', () => {
expect(dropdown.createMrPath).toBe(
`${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`,
);
+
+ expect(dropdown.wrapperEl.dataset.createBranchPath).toBe(
+ `${TEST_HOST}/branches?branch_name=contains%23hash&issue=42`,
+ );
+
+ expect(dropdown.wrapperEl.dataset.createMrPath).toBe(
+ `${TEST_HOST}/create_merge_request?merge_request%5Bsource_branch%5D=contains%23hash&merge_request%5Btarget_branch%5D=master&merge_request%5Bissue_iid%5D=42`,
+ );
});
});
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 77d5a0579a4..c152a5ef9a8 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
@@ -18,6 +18,7 @@ import {
setSortPreferenceMutationResponse,
setSortPreferenceMutationResponseWithErrors,
} from 'jest/issues/list/mock_data';
+import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
import getIssuesCountsQuery from '~/issues/dashboard/queries/get_issues_counts.query.graphql';
import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants';
@@ -36,7 +37,6 @@ import {
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
-import { IssuableStates } from '~/vue_shared/issuable/list/constants';
import {
emptyIssuesQueryResponse,
issuesCountsQueryResponse,
@@ -78,6 +78,7 @@ describe('IssuesDashboardApp component', () => {
}
const findCalendarButton = () => wrapper.findByRole('link', { name: i18n.calendarLabel });
+ const findDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
@@ -113,18 +114,17 @@ describe('IssuesDashboardApp component', () => {
});
describe('UI components', () => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(locationSearch);
mountComponent();
- jest.runOnlyPendingTimers();
- return waitForPromises();
+ await waitForPromises();
});
// https://gitlab.com/gitlab-org/gitlab/-/issues/391722
// eslint-disable-next-line jest/no-disabled-tests
it.skip('renders IssuableList component', () => {
expect(findIssuableList().props()).toMatchObject({
- currentTab: IssuableStates.Opened,
+ currentTab: STATUS_OPEN,
hasNextPage: true,
hasPreviousPage: false,
hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
@@ -145,21 +145,33 @@ describe('IssuesDashboardApp component', () => {
closed: 2,
all: 3,
},
- tabs: IssuesDashboardApp.IssuableListTabs,
+ tabs: IssuesDashboardApp.issuableListTabs,
urlParams: {
sort: urlSortParams[CREATED_DESC],
- state: IssuableStates.Opened,
+ state: STATUS_OPEN,
},
useKeysetPagination: true,
});
});
- it('renders RSS button link', () => {
- expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
- });
+ describe('actions dropdown', () => {
+ it('renders', () => {
+ expect(findDisclosureDropdown().props()).toMatchObject({
+ category: 'tertiary',
+ icon: 'ellipsis_v',
+ noCaret: true,
+ textSrOnly: true,
+ toggleText: 'Actions',
+ });
+ });
- it('renders calendar button link', () => {
- expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
+ it('renders RSS button link', () => {
+ expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
+ });
+
+ it('renders calendar button link', () => {
+ expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
+ });
});
it('renders issue time information', () => {
@@ -174,11 +186,10 @@ describe('IssuesDashboardApp component', () => {
describe('fetching issues', () => {
describe('with a search query', () => {
describe('when there are issues returned', () => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(locationSearch);
mountComponent();
- jest.runOnlyPendingTimers();
- return waitForPromises();
+ await waitForPromises();
});
it('renders the issues', () => {
@@ -193,12 +204,12 @@ describe('IssuesDashboardApp component', () => {
});
describe('when there are no issues returned', () => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(locationSearch);
mountComponent({
issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse),
});
- return waitForPromises();
+ await waitForPromises();
});
it('renders no issues', () => {
@@ -218,10 +229,10 @@ describe('IssuesDashboardApp component', () => {
describe('with no search query', () => {
let issuesQueryHandler;
- beforeEach(() => {
+ beforeEach(async () => {
issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse);
mountComponent({ issuesQueryHandler });
- return waitForPromises();
+ await waitForPromises();
});
it('does not call issues query', () => {
@@ -283,7 +294,7 @@ describe('IssuesDashboardApp component', () => {
describe('state', () => {
it('is set from the url params', () => {
- const initialState = IssuableStates.All;
+ const initialState = STATUS_ALL;
setWindowLocation(`?state=${initialState}`);
mountComponent();
@@ -307,11 +318,10 @@ describe('IssuesDashboardApp component', () => {
${'fetching issues'} | ${'issuesQueryHandler'} | ${i18n.errorFetchingIssues}
${'fetching issue counts'} | ${'issuesCountsQueryHandler'} | ${i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(locationSearch);
mountComponent({ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')) });
- jest.runOnlyPendingTimers();
- return waitForPromises();
+ await waitForPromises();
});
it('shows an error message', () => {
@@ -337,11 +347,9 @@ describe('IssuesDashboardApp component', () => {
username: 'root',
avatar_url: 'avatar/url',
};
- const originalGon = window.gon;
beforeEach(() => {
window.gon = {
- ...originalGon,
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
current_username: mockCurrentUser.username,
@@ -350,10 +358,6 @@ describe('IssuesDashboardApp component', () => {
mountComponent();
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('renders all tokens alphabetically', () => {
const preloadedUsers = [{ ...mockCurrentUser, id: mockCurrentUser.id }];
@@ -375,16 +379,16 @@ describe('IssuesDashboardApp component', () => {
beforeEach(() => {
mountComponent();
- findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
+ findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
});
it('updates ui to the new tab', () => {
- expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
+ expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
});
it('updates url to the new tab', () => {
expect(findIssuableList().props('urlParams')).toMatchObject({
- state: IssuableStates.Closed,
+ state: STATUS_CLOSED,
});
});
});
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index f04e766a78c..3b8a09714a7 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -1,6 +1,8 @@
import { getByText } from '@testing-library/dom';
+import htmlOpenIssue from 'test_fixtures/issues/open-issue.html';
+import htmlClosedIssue from 'test_fixtures/issues/closed-issue.html';
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import Issue from '~/issues/issue';
import axios from '~/lib/utils/axios_utils';
@@ -40,9 +42,9 @@ describe('Issue', () => {
`('$desc', ({ isIssueInitiallyOpen, expectedCounterText }) => {
beforeEach(() => {
if (isIssueInitiallyOpen) {
- loadHTMLFixture('issues/open-issue.html');
+ setHTMLFixture(htmlOpenIssue);
} else {
- loadHTMLFixture('issues/closed-issue.html');
+ setHTMLFixture(htmlClosedIssue);
}
testContext.issueCounter = getIssueCounter();
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
index 0a2e4e7c671..4ea3a39f15b 100644
--- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlLink } from '@gitlab/ui';
+import { GlDropdown, GlEmptyState, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
@@ -26,6 +26,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
};
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
+ const findCsvImportExportDropdown = () => wrapper.findComponent(GlDropdown);
const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
const findGlLink = () => wrapper.findComponent(GlLink);
const findIssuesHelpPageLink = () =>
@@ -135,6 +136,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('renders', () => {
mountComponent({ props: { showCsvButtons: true } });
+ expect(findCsvImportExportDropdown().props('text')).toBe('Import issues');
expect(findCsvImportExportButtons().props()).toMatchObject({
exportCsvPath: defaultProps.exportCsvPathWithQuery,
issuableCount: 0,
@@ -146,6 +148,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('does not render', () => {
mountComponent({ props: { showCsvButtons: false } });
+ expect(findCsvImportExportDropdown().exists()).toBe(false);
expect(findCsvImportExportButtons().exists()).toBe(false);
});
});
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 ab4d023ee39..e80ffea0591 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
@@ -45,10 +45,6 @@ describe('CE IssueCardTimeInfo component', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('milestone', () => {
it('renders', () => {
wrapper = mountComponent();
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 8281ce0ed1a..af24b547545 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -1,4 +1,4 @@
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlDropdown } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -11,23 +11,26 @@ import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_coun
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
getIssuesCountsQueryResponse,
getIssuesQueryResponse,
+ getIssuesQueryEmptyResponse,
filteredTokens,
locationSearch,
setSortPreferenceMutationResponse,
setSortPreferenceMutationResponseWithErrors,
urlParams,
} from 'jest/issues/list/mock_data';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
-import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
@@ -70,7 +73,7 @@ import('~/issuable');
import('~/users_select');
jest.mock('@sentry/browser');
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('CE IssuesListApp component', () => {
@@ -124,12 +127,16 @@ describe('CE IssuesListApp component', () => {
const mockIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
+ const findCalendarButton = () =>
+ wrapper.findByRole('menuitem', { name: IssuesListApp.i18n.calendarLabel });
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
+ const findGlButton = () => wrapper.findComponent(GlButton);
const findGlButtons = () => wrapper.findAllComponents(GlButton);
- const findGlButtonAt = (index) => findGlButtons().at(index);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown);
+ const findRssButton = () => wrapper.findByRole('menuitem', { name: IssuesListApp.i18n.rssLabel });
const findLabelsToken = () =>
findIssuableList()
@@ -154,7 +161,24 @@ describe('CE IssuesListApp component', () => {
router = new VueRouter({ mode: 'history' });
return mountFn(IssuesListApp, {
- apolloProvider: createMockApollo(requestHandlers),
+ apolloProvider: createMockApollo(
+ requestHandlers,
+ {},
+ {
+ typePolicies: {
+ Query: {
+ fields: {
+ project: {
+ merge: true,
+ },
+ group: {
+ merge: true,
+ },
+ },
+ },
+ },
+ },
+ ),
router,
provide: {
...defaultProvide,
@@ -174,17 +198,15 @@ describe('CE IssuesListApp component', () => {
afterEach(() => {
axiosMock.reset();
- wrapper.destroy();
});
describe('IssuableList', () => {
beforeEach(() => {
wrapper = mountComponent();
- jest.runOnlyPendingTimers();
return waitForPromises();
});
- it('renders', async () => {
+ it('renders', () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
@@ -196,8 +218,8 @@ describe('CE IssuesListApp component', () => {
}),
initialSortBy: CREATED_DESC,
issuables: getIssuesQueryResponse.data.project.issues.nodes,
- tabs: IssuableListTabs,
- currentTab: IssuableStates.Opened,
+ tabs: issuableListTabs,
+ currentTab: STATUS_OPEN,
tabCounts: {
opened: 1,
closed: 1,
@@ -215,64 +237,66 @@ describe('CE IssuesListApp component', () => {
});
describe('header action buttons', () => {
- it('renders rss button', async () => {
- wrapper = mountComponent({ mountFn: mount });
- await waitForPromises();
+ describe('actions dropdown', () => {
+ it('renders', () => {
+ wrapper = mountComponent({ mountFn: mount });
- expect(findGlButtonAt(0).props('icon')).toBe('rss');
- expect(findGlButtonAt(0).attributes()).toMatchObject({
- href: defaultProvide.rssPath,
- 'aria-label': IssuesListApp.i18n.rssLabel,
+ expect(findDropdown().props()).toMatchObject({
+ category: 'tertiary',
+ icon: 'ellipsis_v',
+ text: 'Actions',
+ textSrOnly: true,
+ });
});
- });
- it('renders calendar button', async () => {
- wrapper = mountComponent({ mountFn: mount });
- await waitForPromises();
+ describe('csv import/export buttons', () => {
+ describe('when user is signed in', () => {
+ beforeEach(() => {
+ setWindowLocation('?search=refactor&state=opened');
- expect(findGlButtonAt(1).props('icon')).toBe('calendar');
- expect(findGlButtonAt(1).attributes()).toMatchObject({
- href: defaultProvide.calendarPath,
- 'aria-label': IssuesListApp.i18n.calendarLabel,
- });
- });
+ wrapper = mountComponent({
+ provide: { initialSortBy: CREATED_DESC, isSignedIn: true },
+ mountFn: mount,
+ });
- describe('csv import/export component', () => {
- describe('when user is signed in', () => {
- beforeEach(() => {
- setWindowLocation('?search=refactor&state=opened');
+ return waitForPromises();
+ });
- wrapper = mountComponent({
- provide: { initialSortBy: CREATED_DESC, isSignedIn: true },
- mountFn: mount,
+ it('renders', () => {
+ expect(findCsvImportExportButtons().props()).toMatchObject({
+ exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&state=opened`,
+ issuableCount: 1,
+ });
});
+ });
- jest.runOnlyPendingTimers();
- return waitForPromises();
+ describe('when user is not signed in', () => {
+ it('does not render', () => {
+ wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount });
+
+ expect(findCsvImportExportButtons().exists()).toBe(false);
+ });
});
- it('renders', () => {
- expect(findCsvImportExportButtons().props()).toMatchObject({
- exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&state=opened`,
- issuableCount: 1,
+ describe('when in a group context', () => {
+ it('does not render', () => {
+ wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount });
+
+ expect(findCsvImportExportButtons().exists()).toBe(false);
});
});
});
- describe('when user is not signed in', () => {
- it('does not render', () => {
- wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount });
+ it('renders RSS button link', () => {
+ wrapper = mountComponent({ mountFn: mountExtended });
- expect(findCsvImportExportButtons().exists()).toBe(false);
- });
+ expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
});
- describe('when in a group context', () => {
- it('does not render', () => {
- wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount });
+ it('renders calendar button link', () => {
+ wrapper = mountComponent({ mountFn: mountExtended });
- expect(findCsvImportExportButtons().exists()).toBe(false);
- });
+ expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
});
});
@@ -280,20 +304,20 @@ describe('CE IssuesListApp component', () => {
it('renders when user has permissions', () => {
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
- expect(findGlButtonAt(2).text()).toBe('Edit issues');
+ expect(findGlButton().text()).toBe('Bulk edit');
});
it('does not render when user does not have permissions', () => {
wrapper = mountComponent({ provide: { canBulkUpdate: false }, mountFn: mount });
- expect(findGlButtons().filter((button) => button.text() === 'Edit issues')).toHaveLength(0);
+ expect(findGlButtons().filter((button) => button.text() === 'Bulk edit')).toHaveLength(0);
});
it('emits "issuables:enableBulkEdit" event to legacy bulk edit class', async () => {
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
jest.spyOn(eventHub, '$emit');
- findGlButtonAt(2).vm.$emit('click');
+ findGlButton().vm.$emit('click');
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:enableBulkEdit');
@@ -304,8 +328,8 @@ describe('CE IssuesListApp component', () => {
it('renders when user has permissions', () => {
wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount });
- expect(findGlButtonAt(2).text()).toBe('New issue');
- expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath);
+ expect(findGlButton().text()).toBe('New issue');
+ expect(findGlButton().attributes('href')).toBe(defaultProvide.newIssuePath);
});
it('does not render when user does not have permissions', () => {
@@ -416,7 +440,7 @@ describe('CE IssuesListApp component', () => {
describe('state', () => {
it('is set from the url params', () => {
- const initialState = IssuableStates.All;
+ const initialState = STATUS_ALL;
setWindowLocation(`?state=${initialState}`);
wrapper = mountComponent();
@@ -477,7 +501,12 @@ describe('CE IssuesListApp component', () => {
describe('empty states', () => {
describe('when there are issues', () => {
beforeEach(() => {
- wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount });
+ wrapper = mountComponent({
+ provide: { hasAnyIssues: true },
+ mountFn: mount,
+ issuesQueryResponse: getIssuesQueryEmptyResponse,
+ });
+ return waitForPromises();
});
it('shows EmptyStateWithAnyIssues empty state', () => {
@@ -543,11 +572,8 @@ describe('CE IssuesListApp component', () => {
});
describe('when all tokens are available', () => {
- const originalGon = window.gon;
-
beforeEach(() => {
window.gon = {
- ...originalGon,
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
current_username: mockCurrentUser.username,
@@ -563,10 +589,6 @@ describe('CE IssuesListApp component', () => {
});
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('renders all tokens alphabetically', () => {
const preloadedUsers = [
{ ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
@@ -599,7 +621,6 @@ describe('CE IssuesListApp component', () => {
wrapper = mountComponent({
[mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
});
- jest.runOnlyPendingTimers();
return waitForPromises();
});
@@ -620,40 +641,31 @@ describe('CE IssuesListApp component', () => {
describe('events', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = mountComponent();
+ await waitForPromises();
router.push = jest.fn();
- findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
+ findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
});
it('updates ui to the new tab', () => {
- expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
+ expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
});
it('updates url to the new tab', () => {
expect(router.push).toHaveBeenCalledWith({
- query: expect.objectContaining({ state: IssuableStates.Closed }),
+ query: expect.objectContaining({ state: STATUS_CLOSED }),
});
});
});
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,
-}}
+ 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(() => {
+ beforeEach(async () => {
wrapper = mountComponent({
data: {
pageInfo: {
@@ -662,6 +674,7 @@ describe('CE IssuesListApp component', () => {
},
},
});
+ await waitForPromises();
router.push = jest.fn();
findIssuableList().vm.$emit(event);
@@ -735,7 +748,6 @@ describe('CE IssuesListApp component', () => {
provide: { isProject },
issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)),
});
- jest.runOnlyPendingTimers();
return waitForPromises();
});
@@ -761,7 +773,6 @@ describe('CE IssuesListApp component', () => {
wrapper = mountComponent({
issuesQueryResponse: jest.fn().mockResolvedValue(response()),
});
- jest.runOnlyPendingTimers();
return waitForPromises();
});
@@ -784,7 +795,7 @@ describe('CE IssuesListApp component', () => {
describe('when "sort" event is emitted by IssuableList', () => {
it.each(Object.keys(urlSortParams))(
'updates to the new sort when payload is `%s`',
- async (sortKey) => {
+ (sortKey) => {
// Ensure initial sort key is different so we can trigger an update when emitting a sort key
wrapper =
sortKey === CREATED_DESC
@@ -793,8 +804,6 @@ describe('CE IssuesListApp component', () => {
router.push = jest.fn();
findIssuableList().vm.$emit('sort', sortKey);
- jest.runOnlyPendingTimers();
- await nextTick();
expect(router.push).toHaveBeenCalledWith({
query: expect.objectContaining({ sort: urlSortParams[sortKey] }),
@@ -914,13 +923,13 @@ describe('CE IssuesListApp component', () => {
${'shows users when public visibility is not restricted and is signed in'} | ${false} | ${true} | ${false}
${'hides users when public visibility is restricted and is not signed in'} | ${true} | ${false} | ${true}
${'shows users when public visibility is restricted and is signed in'} | ${true} | ${true} | ${false}
- `('$description', ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => {
+ `('$description', async ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => {
const mockQuery = jest.fn().mockResolvedValue(defaultQueryResponse);
wrapper = mountComponent({
provide: { isPublicVisibilityRestricted, isSignedIn },
issuesQueryResponse: mockQuery,
});
- jest.runOnlyPendingTimers();
+ await waitForPromises();
expect(mockQuery).toHaveBeenCalledWith(expect.objectContaining({ hideUsers }));
});
@@ -929,7 +938,6 @@ describe('CE IssuesListApp component', () => {
describe('fetching issues', () => {
beforeEach(() => {
wrapper = mountComponent();
- jest.runOnlyPendingTimers();
});
it('fetches issue, incident, test case, and task types', () => {
diff --git a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
index 406b1fbc1af..81739f6ef1d 100644
--- a/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
+++ b/spec/frontend/issues/list/components/jira_issues_import_status_app_spec.js
@@ -38,11 +38,6 @@ describe('JiraIssuesImportStatus', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when Jira import is neither in progress nor finished', () => {
beforeEach(() => {
wrapper = mountComponent();
@@ -99,7 +94,7 @@ describe('JiraIssuesImportStatus', () => {
});
});
- describe('alert message', () => {
+ describe('alert', () => {
it('is hidden when dismissed', async () => {
wrapper = mountComponent({
shouldShowInProgressAlert: true,
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 1e8a81116f3..bd006a6b3ce 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -101,6 +101,26 @@ export const getIssuesQueryResponse = {
},
};
+export const getIssuesQueryEmptyResponse = {
+ data: {
+ project: {
+ id: '1',
+ __typename: 'Project',
+ issues: {
+ __persist: true,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ nodes: [],
+ },
+ },
+ },
+};
+
export const getIssuesCountsQueryResponse = {
data: {
project: {
@@ -196,6 +216,7 @@ export const locationSearchWithSpecialValues = [
].join('&');
export const filteredTokens = [
+ { type: FILTERED_SEARCH_TERM, value: { data: 'find issues', operator: 'undefined' } },
{ type: TOKEN_TYPE_AUTHOR, value: { data: 'homer', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_AUTHOR, value: { data: 'marge', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_AUTHOR, value: { data: 'burns', operator: OPERATOR_OR } },
@@ -240,8 +261,6 @@ export const filteredTokens = [
{ type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: OPERATOR_NOT } },
- { type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
- { type: FILTERED_SEARCH_TERM, value: { data: 'issues' } },
];
export const filteredTokensWithSpecialValues = [
@@ -258,6 +277,7 @@ export const filteredTokensWithSpecialValues = [
];
export const apiParams = {
+ search: 'find issues',
authorUsername: 'homer',
assigneeUsernames: ['bart', 'lisa', '5'],
milestoneTitle: ['season 3', 'season 4'],
@@ -306,6 +326,7 @@ export const apiParamsWithSpecialValues = {
};
export const urlParams = {
+ search: 'find issues',
author_username: 'homer',
'not[author_username]': 'marge',
'or[author_username]': ['burns', 'smithers'],
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index a281ed1c989..c14dcf96c98 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -10,7 +10,7 @@ import {
urlParams,
urlParamsWithSpecialValues,
} from 'jest/issues/list/mock_data';
-import { PAGE_SIZE, urlSortParams } from '~/issues/list/constants';
+import { urlSortParams } from '~/issues/list/constants';
import {
convertToApiParams,
convertToSearchQuery,
@@ -21,11 +21,11 @@ import {
getSortOptions,
isSortKey,
} from '~/issues/list/utils';
-import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants';
describe('getInitialPageParams', () => {
it('returns page params with a default page size when no arguments are given', () => {
- expect(getInitialPageParams()).toEqual({ firstPageSize: PAGE_SIZE });
+ expect(getInitialPageParams()).toEqual({ firstPageSize: DEFAULT_PAGE_SIZE });
});
it('returns page params with the given page size', () => {
@@ -124,20 +124,6 @@ describe('getFilterTokens', () => {
filteredTokensWithSpecialValues,
);
});
-
- it.each`
- description | argument
- ${'an undefined value'} | ${undefined}
- ${'an irrelevant value'} | ${'?unrecognised=parameter'}
- `('returns an empty filtered search term given $description', ({ argument }) => {
- expect(getFilterTokens(argument)).toEqual([
- {
- id: expect.any(String),
- type: FILTERED_SEARCH_TERM,
- value: { data: '' },
- },
- ]);
- });
});
describe('convertToApiParams', () => {
diff --git a/spec/frontend/issues/new/components/title_suggestions_item_spec.js b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
index c54a762440f..4454ef81416 100644
--- a/spec/frontend/issues/new/components/title_suggestions_item_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_item_spec.js
@@ -25,10 +25,6 @@ describe('Issue title suggestions item component', () => {
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findUserAvatar = () => wrapper.findComponent(UserAvatarImage);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders title', () => {
createComponent();
diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js
index 1cd6576967a..343bdbba301 100644
--- a/spec/frontend/issues/new/components/title_suggestions_spec.js
+++ b/spec/frontend/issues/new/components/title_suggestions_spec.js
@@ -1,106 +1,95 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import TitleSuggestions from '~/issues/new/components/title_suggestions.vue';
import TitleSuggestionsItem from '~/issues/new/components/title_suggestions_item.vue';
+import getIssueSuggestionsQuery from '~/issues/new/queries/issues.query.graphql';
+import { mockIssueSuggestionResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+
+const MOCK_PROJECT_PATH = 'project';
+const MOCK_ISSUES_COUNT = mockIssueSuggestionResponse.data.project.issues.edges.length;
describe('Issue title suggestions component', () => {
let wrapper;
+ let mockApollo;
+
+ function createComponent({
+ search = 'search',
+ queryResponse = jest.fn().mockResolvedValue(mockIssueSuggestionResponse),
+ } = {}) {
+ mockApollo = createMockApollo([[getIssueSuggestionsQuery, queryResponse]]);
- function createComponent(search = 'search') {
wrapper = shallowMount(TitleSuggestions, {
propsData: {
search,
- projectPath: 'project',
+ projectPath: MOCK_PROJECT_PATH,
},
+ apolloProvider: mockApollo,
});
}
- beforeEach(() => {
- createComponent();
- });
+ const waitForDebounce = () => {
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
+ };
afterEach(() => {
- wrapper.destroy();
+ mockApollo = null;
});
it('does not render with empty search', async () => {
- wrapper.setProps({ search: '' });
+ createComponent({ search: '' });
+ await waitForDebounce();
- await nextTick();
expect(wrapper.isVisible()).toBe(false);
});
- describe('with data', () => {
- let data;
-
- beforeEach(() => {
- data = { issues: [{ id: 1 }, { id: 2 }] };
- });
-
- it('renders component', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
-
- await nextTick();
- expect(wrapper.findAll('li').length).toBe(data.issues.length);
- });
+ it('does not render when loading', () => {
+ createComponent();
+ expect(wrapper.isVisible()).toBe(false);
+ });
- it('does not render with empty search', async () => {
- wrapper.setProps({ search: '' });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
+ it('does not render with empty issues data', async () => {
+ const emptyIssuesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ issues: {
+ edges: [],
+ },
+ },
+ },
+ };
- await nextTick();
- expect(wrapper.isVisible()).toBe(false);
- });
+ createComponent({ queryResponse: jest.fn().mockResolvedValue(emptyIssuesResponse) });
+ await waitForDebounce();
- it('does not render when loading', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- ...data,
- loading: 1,
- });
+ expect(wrapper.isVisible()).toBe(false);
+ });
- await nextTick();
- expect(wrapper.isVisible()).toBe(false);
+ describe('with data', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForDebounce();
});
- it('does not render with empty issues data', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ issues: [] });
-
- await nextTick();
- expect(wrapper.isVisible()).toBe(false);
+ it('renders component', () => {
+ expect(wrapper.findAll('li').length).toBe(MOCK_ISSUES_COUNT);
});
- it('renders list of issues', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
-
- await nextTick();
- expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(2);
+ it('renders list of issues', () => {
+ expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(MOCK_ISSUES_COUNT);
});
- it('adds margin class to first item', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
-
- await nextTick();
+ it('adds margin class to first item', () => {
expect(wrapper.findAll('li').at(0).classes()).toContain('gl-mb-3');
});
- it('does not add margin class to last item', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData(data);
-
- await nextTick();
+ it('does not add margin class to last item', () => {
expect(wrapper.findAll('li').at(1).classes()).not.toContain('gl-mb-3');
});
});
diff --git a/spec/frontend/issues/new/components/type_popover_spec.js b/spec/frontend/issues/new/components/type_popover_spec.js
index fe3d5207516..1ae150797c3 100644
--- a/spec/frontend/issues/new/components/type_popover_spec.js
+++ b/spec/frontend/issues/new/components/type_popover_spec.js
@@ -8,10 +8,6 @@ describe('Issue type info popover', () => {
wrapper = shallowMount(TypePopover);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
createComponent();
diff --git a/spec/frontend/issues/new/components/type_select_spec.js b/spec/frontend/issues/new/components/type_select_spec.js
new file mode 100644
index 00000000000..a25ace10fe7
--- /dev/null
+++ b/spec/frontend/issues/new/components/type_select_spec.js
@@ -0,0 +1,141 @@
+import { GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import * as urlUtility from '~/lib/utils/url_utility';
+import TypeSelect from '~/issues/new/components/type_select.vue';
+import { TYPE_ISSUE, TYPE_INCIDENT } from '~/issues/constants';
+import { __ } from '~/locale';
+
+const issuePath = 'issues/new';
+const incidentPath = 'issues/new?issuable_template=incident';
+const tracking = {
+ action: 'select_issue_type_incident',
+ label: 'select_issue_type_incident_dropdown_option',
+};
+
+const defaultProps = {
+ selectedType: '',
+ isIssueAllowed: true,
+ isIncidentAllowed: true,
+ issuePath,
+ incidentPath,
+};
+
+const issue = {
+ value: TYPE_ISSUE,
+ text: __('Issue'),
+ icon: 'issue-type-issue',
+ href: issuePath,
+};
+const incident = {
+ value: TYPE_INCIDENT,
+ text: __('Incident'),
+ icon: 'issue-type-incident',
+ href: incidentPath,
+ tracking,
+};
+
+describe('Issue type select component', () => {
+ let wrapper;
+ let trackingSpy;
+ let navigationSpy;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(TypeSelect, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllIcons = () => wrapper.findAllComponents(GlIcon);
+ const findListboxItemIcon = () => findAllIcons().at(2);
+
+ describe('initial state', () => {
+ it('renders listbox with the correct header text', () => {
+ createComponent();
+
+ expect(findListbox().props('headerText')).toBe(TypeSelect.i18n.selectType);
+ });
+
+ it.each`
+ selectedType | toggleText
+ ${''} | ${TypeSelect.i18n.selectType}
+ ${TYPE_ISSUE} | ${TypeSelect.i18n.issuableType[TYPE_ISSUE]}
+ ${TYPE_INCIDENT} | ${TypeSelect.i18n.issuableType[TYPE_INCIDENT]}
+ `(
+ 'renders listbox with the correct toggle text when selectedType is "$selectedType"',
+ ({ selectedType, toggleText }) => {
+ createComponent({ selectedType });
+
+ expect(findListbox().props('toggleText')).toBe(toggleText);
+ },
+ );
+
+ it.each`
+ isIssueAllowed | isIncidentAllowed | items
+ ${true} | ${true} | ${[issue, incident]}
+ ${true} | ${false} | ${[issue]}
+ ${false} | ${true} | ${[incident]}
+ `(
+ 'renders listbox with the correct items when isIssueAllowed is "$isIssueAllowed" and isIncidentAllowed is "$isIncidentAllowed"',
+ ({ isIssueAllowed, isIncidentAllowed, items }) => {
+ createComponent({ isIssueAllowed, isIncidentAllowed });
+
+ expect(findListbox().props('items')).toMatchObject(items);
+ },
+ );
+
+ it.each`
+ isIssueAllowed | isIncidentAllowed | icon
+ ${true} | ${false} | ${issue.icon}
+ ${false} | ${true} | ${incident.icon}
+ `(
+ 'renders listbox item with the correct $icon icon',
+ ({ isIssueAllowed, isIncidentAllowed, icon }) => {
+ createComponent({ isIssueAllowed, isIncidentAllowed });
+ findListbox().vm.$emit('shown');
+
+ expect(findListboxItemIcon().props('name')).toBe(icon);
+ },
+ );
+ });
+
+ describe('on type selected', () => {
+ beforeEach(() => {
+ navigationSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ navigationSpy.mockRestore();
+ });
+
+ it.each`
+ selectedType | expectedUrl
+ ${TYPE_ISSUE} | ${issuePath}
+ ${TYPE_INCIDENT} | ${incidentPath}
+ `('navigates to the $selectedType issuable page', ({ selectedType, expectedUrl }) => {
+ createComponent();
+ findListbox().vm.$emit('select', selectedType);
+
+ expect(navigationSpy).toHaveBeenCalledWith(expectedUrl);
+ });
+
+ it("doesn't call tracking APIs when tracking is not available for the issuable type", () => {
+ createComponent();
+ findListbox().vm.$emit('select', TYPE_ISSUE);
+
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+
+ it('calls tracking APIs when tracking is available for the issuable type', () => {
+ createComponent();
+ findListbox().vm.$emit('select', TYPE_INCIDENT);
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, tracking.action, {
+ label: tracking.label,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issues/new/mock_data.js b/spec/frontend/issues/new/mock_data.js
index 74b569d9833..0d2a388cd86 100644
--- a/spec/frontend/issues/new/mock_data.js
+++ b/spec/frontend/issues/new/mock_data.js
@@ -26,3 +26,67 @@ export default () => ({
webUrl: `${TEST_HOST}/author`,
},
});
+
+export const mockIssueSuggestionResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/278964',
+ issues: {
+ edges: [
+ {
+ node: {
+ id: 'gid://gitlab/Issue/123725957',
+ iid: '696',
+ title: 'Remove unused MR widget extension expand success, failed, warning events',
+ confidential: false,
+ userNotesCount: 16,
+ upvotes: 0,
+ webUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/696',
+ state: 'opened',
+ closedAt: null,
+ createdAt: '2023-02-15T12:29:59Z',
+ updatedAt: '2023-03-01T19:38:22Z',
+ author: {
+ id: 'gid://gitlab/User/325',
+ name: 'User Name',
+ username: 'user-name',
+ avatarUrl: '/uploads/-/system/user/avatar/325/avatar.png',
+ webUrl: 'https://gitlab.com/user-name',
+ __typename: 'UserCore',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'IssueEdge',
+ },
+ {
+ node: {
+ id: 'gid://gitlab/Issue/123',
+ iid: '391',
+ title: 'Remove unused MR widget extension expand success, failed, warning events',
+ confidential: false,
+ userNotesCount: 16,
+ upvotes: 0,
+ webUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391',
+ state: 'opened',
+ closedAt: null,
+ createdAt: '2023-02-15T12:29:59Z',
+ updatedAt: '2023-03-01T19:38:22Z',
+ author: {
+ id: 'gid://gitlab/User/2080',
+ name: 'User Name',
+ username: 'user-name',
+ avatarUrl: '/uploads/-/system/user/avatar/2080/avatar.png',
+ webUrl: 'https://gitlab.com/user-name',
+ __typename: 'UserCore',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'IssueEdge',
+ },
+ ],
+ __typename: 'IssueConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
diff --git a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
index 010c719bd84..c5507c88fd7 100644
--- a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
+++ b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
@@ -34,7 +34,6 @@ describe('RelatedMergeRequests', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/issues/related_merge_requests/store/actions_spec.js b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
index 7339372a8d1..31c96265f8d 100644
--- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/issues/related_merge_requests/store/actions';
import * as types from '~/issues/related_merge_requests/store/mutation_types';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RelatedMergeRequest store actions', () => {
let state;
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 9fa0ce6f93d..83707dfd254 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -1,13 +1,11 @@
import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
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 '~/flash';
+import { createAlert } from '~/alert';
import {
- IssuableStatusText,
+ issuableStatusText,
STATUS_CLOSED,
STATUS_OPEN,
STATUS_REOPENED,
@@ -21,29 +19,27 @@ import FormComponent from '~/issues/show/components/form.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';
-import { POLLING_DELAY } from '~/issues/show/constants';
import eventHub from '~/issues/show/event_hub';
import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import {
appProps,
initialRequest,
publishedIncidentUrl,
+ putRequest,
secondRequest,
zoomMeetingUrl,
} from '../mock_data/mock_data';
-jest.mock('~/flash');
-jest.mock('~/issues/show/event_hub');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/behaviors/markdown/render_gfm');
const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
describe('Issuable output', () => {
- let mock;
- let realtimeRequestCount = 0;
+ let axiosMock;
let wrapper;
const findStickyHeader = () => wrapper.findByTestId('issue-sticky-header');
@@ -57,15 +53,14 @@ describe('Issuable output', () => {
const findForm = () => wrapper.findComponent(FormComponent);
const findPinnedLinks = () => wrapper.findComponent(PinnedLinks);
- const mountComponent = (props = {}, options = {}, data = {}) => {
+ const createComponent = ({ props = {}, options = {}, data = {} } = {}) => {
wrapper = shallowMountExtended(IssuableApp, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: { ...appProps, ...props },
provide: {
fullPath: 'gitlab-org/incidents',
- iid: '19',
uploadMetricsFeatureAvailable: false,
},
stubs: {
@@ -79,387 +74,256 @@ describe('Issuable output', () => {
},
...options,
});
+
+ jest.advanceTimersToNextTimer(2);
+ return waitForPromises();
};
- beforeEach(() => {
- setHTMLFixture(`
- <div>
- <title>Title</title>
- <div class="detail-page-description content-block">
- <details open>
- <summary>One</summary>
- </details>
- <details>
- <summary>Two</summary>
- </details>
- </div>
- <div class="flash-container"></div>
- <span id="task_status"></span>
- </div>
- `);
-
- mock = new MockAdapter(axios);
- mock
- .onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
- .reply(() => {
- const res = Promise.resolve([HTTP_STATUS_OK, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
- realtimeRequestCount += 1;
- return res;
- });
+ const emitHubEvent = (event) => {
+ eventHub.$emit(event);
+ return waitForPromises();
+ };
- mountComponent();
+ const openForm = () => {
+ return emitHubEvent('open.form');
+ };
- jest.advanceTimersByTime(2);
- });
+ const updateIssuable = () => {
+ return emitHubEvent('update.issuable');
+ };
- afterEach(() => {
- mock.restore();
- realtimeRequestCount = 0;
- wrapper.vm.poll.stop();
- wrapper.destroy();
- resetHTMLFixture();
- });
+ const advanceToNextPoll = () => {
+ // We get new data through the HTTP request.
+ jest.advanceTimersToNextTimer();
+ return waitForPromises();
+ };
- it('should render a title/description/edited and update title/description/edited on update', () => {
- return axios
- .waitForAll()
- .then(() => {
- expect(findTitle().props('titleText')).toContain('this is a title');
- expect(findDescription().props('descriptionText')).toContain('this is a description');
-
- expect(findEdited().exists()).toBe(true);
- expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
- expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at);
- expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version);
- })
- .then(() => {
- wrapper.vm.poll.makeRequest();
- return axios.waitForAll();
- })
- .then(() => {
- expect(findTitle().props('titleText')).toContain('2');
- expect(findDescription().props('descriptionText')).toContain('42');
-
- expect(findEdited().exists()).toBe(true);
- expect(findEdited().props('updatedByName')).toBe('Other User');
- expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
- expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at);
- });
- });
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit');
- it('shows actions if permissions are correct', async () => {
- wrapper.vm.showForm = true;
+ axiosMock = new MockAdapter(axios);
+ const endpoint = '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes';
- await nextTick();
- expect(findForm().exists()).toBe(true);
+ axiosMock.onGet(endpoint).replyOnce(HTTP_STATUS_OK, REALTIME_REQUEST_STACK[0], {
+ 'POLL-INTERVAL': '1',
+ });
+ axiosMock.onGet(endpoint).reply(HTTP_STATUS_OK, REALTIME_REQUEST_STACK[1], {
+ 'POLL-INTERVAL': '-1',
+ });
+ axiosMock.onPut().reply(HTTP_STATUS_OK, putRequest);
});
- it('does not show actions if permissions are incorrect', async () => {
- wrapper.vm.showForm = true;
- wrapper.setProps({ canUpdate: false });
-
- await nextTick();
- expect(findForm().exists()).toBe(false);
- });
+ describe('update', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
- it('does not update formState if form is already open', async () => {
- wrapper.vm.updateAndShowForm();
+ it('should render a title/description/edited and update title/description/edited on update', async () => {
+ expect(findTitle().props('titleText')).toContain(initialRequest.title_text);
+ expect(findDescription().props('descriptionText')).toContain('this is a description');
- wrapper.vm.state.titleText = 'testing 123';
+ expect(findEdited().exists()).toBe(true);
+ expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
+ expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at);
+ expect(findDescription().props().lockVersion).toBe(initialRequest.lock_version);
- wrapper.vm.updateAndShowForm();
+ await advanceToNextPoll();
- await nextTick();
- expect(wrapper.vm.store.formState.title).not.toBe('testing 123');
- });
+ expect(findTitle().props('titleText')).toContain('2');
+ expect(findDescription().props('descriptionText')).toContain('42');
- describe('Pinned links propagated', () => {
- it.each`
- prop | value
- ${'zoomMeetingUrl'} | ${zoomMeetingUrl}
- ${'publishedIncidentUrl'} | ${publishedIncidentUrl}
- `('sets the $prop correctly on underlying pinned links', ({ prop, value }) => {
- expect(findPinnedLinks().props(prop)).toBe(value);
+ expect(findEdited().exists()).toBe(true);
+ expect(findEdited().props('updatedByName')).toBe('Other User');
+ expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
+ expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at);
});
});
- describe('updateIssuable', () => {
- it('fetches new data after update', async () => {
- const updateStoreSpy = jest.spyOn(wrapper.vm, 'updateStoreState');
- const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData');
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: { web_url: window.location.pathname },
- });
-
- await wrapper.vm.updateIssuable();
- expect(updateStoreSpy).toHaveBeenCalled();
- expect(getDataSpy).toHaveBeenCalled();
+ describe('with permissions', () => {
+ beforeEach(async () => {
+ await createComponent();
});
- it('correctly updates issuable data', async () => {
- const spy = jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: { web_url: window.location.pathname },
- });
+ it('shows actions on `open.form` event', async () => {
+ expect(findForm().exists()).toBe(false);
- await wrapper.vm.updateIssuable();
- expect(spy).toHaveBeenCalledWith(wrapper.vm.formState);
- expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
- });
+ await openForm();
- it('does not redirect if issue has not moved', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: {
- web_url: window.location.pathname,
- confidential: wrapper.vm.isConfidential,
- },
- });
-
- await wrapper.vm.updateIssuable();
- expect(visitUrl).not.toHaveBeenCalled();
+ expect(findForm().exists()).toBe(true);
});
- it('does not redirect if issue has not moved and user has switched tabs', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: {
- web_url: '',
- confidential: wrapper.vm.isConfidential,
- },
- });
+ it('update formState if form is not open', async () => {
+ const titleValue = initialRequest.title_text;
- await wrapper.vm.updateIssuable();
- expect(visitUrl).not.toHaveBeenCalled();
- });
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().props('titleText')).toBe(titleValue);
- it('redirects if returned web_url has changed', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockResolvedValue({
- data: {
- web_url: '/testing-issue-move',
- confidential: wrapper.vm.isConfidential,
- },
- });
+ await advanceToNextPoll();
- wrapper.vm.updateIssuable();
-
- await wrapper.vm.updateIssuable();
- expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
+ // The title component has the new data, so the state was updated
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().props('titleText')).toBe(secondRequest.title_text);
});
- describe('shows dialog when issue has unsaved changed', () => {
- it('confirms on title change', async () => {
- wrapper.vm.showForm = true;
- wrapper.vm.state.titleText = 'title has changed';
- const e = { returnValue: null };
- wrapper.vm.handleBeforeUnloadEvent(e);
+ it('does not update formState if form is already open', async () => {
+ const titleValue = initialRequest.title_text;
- await nextTick();
- expect(e.returnValue).not.toBeNull();
- });
+ expect(findTitle().exists()).toBe(true);
+ expect(findTitle().props('titleText')).toBe(titleValue);
- it('confirms on description change', async () => {
- wrapper.vm.showForm = true;
- wrapper.vm.state.descriptionText = 'description has changed';
- const e = { returnValue: null };
- wrapper.vm.handleBeforeUnloadEvent(e);
+ await openForm();
- await nextTick();
- expect(e.returnValue).not.toBeNull();
- });
+ // Opening the form, the data has not changed
+ expect(findForm().props().formState.title).toBe(titleValue);
- it('does nothing when nothing has changed', async () => {
- const e = { returnValue: null };
- wrapper.vm.handleBeforeUnloadEvent(e);
+ await advanceToNextPoll();
- await nextTick();
- expect(e.returnValue).toBeNull();
- });
+ // We expect the prop value not to have changed after another API call
+ expect(findForm().props().formState.title).toBe(titleValue);
});
+ });
- describe('error when updating', () => {
- it('closes form on error', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue();
+ describe('without permissions', () => {
+ beforeEach(async () => {
+ await createComponent({ props: { canUpdate: false } });
+ });
- await wrapper.vm.updateIssuable();
- expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(createAlert).toHaveBeenCalledWith({ message: `Error updating issue` });
- });
+ it('does not show actions if permissions are incorrect', async () => {
+ await openForm();
- it('returns the correct error message for issuableType', async () => {
- jest.spyOn(wrapper.vm.service, 'updateIssuable').mockRejectedValue();
- wrapper.setProps({ issuableType: 'merge request' });
-
- await nextTick();
- await wrapper.vm.updateIssuable();
- expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(createAlert).toHaveBeenCalledWith({ message: `Error updating merge request` });
- });
+ expect(findForm().exists()).toBe(false);
+ });
+ });
- it('shows error message from backend if exists', async () => {
- const msg = 'Custom error message from backend';
- jest
- .spyOn(wrapper.vm.service, 'updateIssuable')
- .mockRejectedValue({ response: { data: { errors: [msg] } } });
+ describe('Pinned links propagated', () => {
+ it.each`
+ prop | value
+ ${'zoomMeetingUrl'} | ${zoomMeetingUrl}
+ ${'publishedIncidentUrl'} | ${publishedIncidentUrl}
+ `('sets the $prop correctly on underlying pinned links', async ({ prop, value }) => {
+ await createComponent();
- await wrapper.vm.updateIssuable();
- expect(createAlert).toHaveBeenCalledWith({
- message: `${wrapper.vm.defaultErrorMessage}. ${msg}`,
- });
- });
+ expect(findPinnedLinks().props(prop)).toBe(value);
});
});
- describe('updateAndShowForm', () => {
- it('shows locked warning if form is open & data is different', async () => {
- await nextTick();
- wrapper.vm.updateAndShowForm();
+ describe('updating an issue', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
- wrapper.vm.poll.makeRequest();
+ it('fetches new data after update', async () => {
+ await advanceToNextPoll();
- await new Promise((resolve) => {
- wrapper.vm.$watch('formState.lockedWarningVisible', (value) => {
- if (value) {
- resolve();
- }
- });
- });
+ await updateIssuable();
- expect(wrapper.vm.formState.lockedWarningVisible).toBe(true);
- expect(wrapper.vm.formState.lock_version).toBe(1);
+ expect(axiosMock.history.put).toHaveLength(1);
+ // The call was made with the new data
+ expect(axiosMock.history.put[0].data.title).toEqual(findTitle().props().title);
});
- });
- describe('requestTemplatesAndShowForm', () => {
- let formSpy;
+ it('closes the form after fetching data', async () => {
+ await updateIssuable();
- beforeEach(() => {
- formSpy = jest.spyOn(wrapper.vm, 'updateAndShowForm');
+ expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
});
- it('shows the form if template names as hash request is successful', () => {
- const mockData = {
- test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
- };
- mock
- .onGet('/issuable-templates-path')
- .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
-
- return wrapper.vm.requestTemplatesAndShowForm().then(() => {
- expect(formSpy).toHaveBeenCalledWith(mockData);
+ it('does not redirect if issue has not moved', async () => {
+ axiosMock.onPut().reply(HTTP_STATUS_OK, {
+ ...putRequest,
+ confidential: appProps.isConfidential,
});
- });
- it('shows the form if template names as array request is successful', () => {
- const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }];
- mock
- .onGet('/issuable-templates-path')
- .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
+ await updateIssuable();
- return wrapper.vm.requestTemplatesAndShowForm().then(() => {
- expect(formSpy).toHaveBeenCalledWith(mockData);
- });
+ expect(visitUrl).not.toHaveBeenCalled();
});
- it('shows the form if template names request failed', () => {
- mock
- .onGet('/issuable-templates-path')
- .reply(() => Promise.reject(new Error('something went wrong')));
-
- return wrapper.vm.requestTemplatesAndShowForm().then(() => {
- expect(createAlert).toHaveBeenCalledWith({ message: 'Error updating issue' });
-
- expect(formSpy).toHaveBeenCalledWith();
+ it('does not redirect if issue has not moved and user has switched tabs', async () => {
+ axiosMock.onPut().reply(HTTP_STATUS_OK, {
+ ...putRequest,
+ web_url: '',
+ confidential: appProps.isConfidential,
});
- });
- });
- describe('show inline edit button', () => {
- it('should render by default', () => {
- expect(findTitle().props('showInlineEditButton')).toBe(true);
+ await updateIssuable();
+
+ expect(visitUrl).not.toHaveBeenCalled();
});
- it('should render if showInlineEditButton', async () => {
- wrapper.setProps({ showInlineEditButton: true });
+ it('redirects if returned web_url has changed', async () => {
+ const webUrl = '/testing-issue-move';
- await nextTick();
- expect(findTitle().props('showInlineEditButton')).toBe(true);
- });
+ axiosMock.onPut().reply(HTTP_STATUS_OK, {
+ ...putRequest,
+ web_url: webUrl,
+ confidential: appProps.isConfidential,
+ });
- it('should not render if showInlineEditButton is false', async () => {
- wrapper.setProps({ showInlineEditButton: false });
+ await updateIssuable();
- await nextTick();
- expect(findTitle().props('showInlineEditButton')).toBe(false);
+ expect(visitUrl).toHaveBeenCalledWith(webUrl);
});
- });
- describe('updateStoreState', () => {
- it('should make a request and update the state of the store', () => {
- const data = { foo: 1 };
- const getDataSpy = jest.spyOn(wrapper.vm.service, 'getData').mockResolvedValue({ data });
- const updateStateSpy = jest
- .spyOn(wrapper.vm.store, 'updateState')
- .mockImplementation(jest.fn);
-
- return wrapper.vm.updateStoreState().then(() => {
- expect(getDataSpy).toHaveBeenCalled();
- expect(updateStateSpy).toHaveBeenCalledWith(data);
- });
- });
+ describe('error when updating', () => {
+ it('closes form', async () => {
+ axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED);
- it('should show error message if store update fails', () => {
- jest.spyOn(wrapper.vm.service, 'getData').mockRejectedValue();
- wrapper.setProps({ issuableType: 'merge request' });
+ await updateIssuable();
- return wrapper.vm.updateStoreState().then(() => {
+ expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
expect(createAlert).toHaveBeenCalledWith({
- message: `Error updating ${wrapper.vm.issuableType}`,
+ message: `Error updating issue. Request failed with status code 401`,
});
});
- });
- });
- describe('issueChanged', () => {
- beforeEach(() => {
- wrapper.vm.store.formState.title = '';
- wrapper.vm.store.formState.description = '';
- wrapper.setProps({
- initialDescriptionText: '',
- initialTitleText: '',
- });
- });
+ it('returns the correct error message for issuableType', async () => {
+ axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED);
- it('returns true when title is changed', () => {
- wrapper.vm.store.formState.title = 'RandomText';
+ await updateIssuable();
- expect(wrapper.vm.issueChanged).toBe(true);
- });
+ wrapper.setProps({ issuableType: 'merge request' });
- it('returns false when title is empty null', () => {
- wrapper.vm.store.formState.title = null;
+ await updateIssuable();
- expect(wrapper.vm.issueChanged).toBe(false);
- });
+ expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Error updating merge request. Request failed with status code 401`,
+ });
+ });
- it('returns true when description is changed', () => {
- wrapper.vm.store.formState.description = 'RandomText';
+ it('shows error message from backend if exists', async () => {
+ const msg = 'Custom error message from backend';
+ axiosMock.onPut().reply(HTTP_STATUS_UNAUTHORIZED, { errors: [msg] });
- expect(wrapper.vm.issueChanged).toBe(true);
- });
+ await updateIssuable();
- it('returns false when description is empty null', () => {
- wrapper.vm.store.formState.description = null;
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Error updating issue. ${msg}`,
+ });
+ });
+ });
+ });
- expect(wrapper.vm.issueChanged).toBe(false);
+ describe('Locked warning', () => {
+ beforeEach(async () => {
+ await createComponent();
});
- it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => {
- wrapper.vm.store.formState.description = '';
- wrapper.setProps({ initialDescriptionText: null });
+ it('shows locked warning if form is open & data is different', async () => {
+ await openForm();
+ await advanceToNextPoll();
- expect(wrapper.vm.issueChanged).toBe(false);
+ expect(findForm().props().formState.lockedWarningVisible).toBe(true);
+ expect(findForm().props().formState.lock_version).toBe(1);
});
});
describe('sticky header', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
describe('when title is in view', () => {
it('is not shown', () => {
expect(findStickyHeader().exists()).toBe(false);
@@ -468,20 +332,17 @@ describe('Issuable output', () => {
describe('when title is not in view', () => {
beforeEach(() => {
- wrapper.vm.state.titleText = 'Sticky header title';
wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
});
it('shows with title', () => {
- expect(findStickyHeader().text()).toContain('Sticky header title');
+ expect(findStickyHeader().text()).toContain(initialRequest.title_text);
});
it('shows with title for an epic', async () => {
- wrapper.setProps({ issuableType: 'epic' });
-
- await nextTick();
+ await wrapper.setProps({ issuableType: 'epic' });
- expect(findStickyHeader().text()).toContain('Sticky header title');
+ expect(findStickyHeader().text()).toContain(' this is a title');
});
it.each`
@@ -493,9 +354,7 @@ describe('Issuable output', () => {
`(
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
async ({ issuableType, issuableStatus, statusIcon }) => {
- wrapper.setProps({ issuableType, issuableStatus });
-
- await nextTick();
+ await wrapper.setProps({ issuableType, issuableStatus });
expect(findStickyHeader().findComponent(GlIcon).props('name')).toBe(statusIcon);
},
@@ -507,11 +366,9 @@ describe('Issuable output', () => {
${'shows with Closed when status is closed'} | ${STATUS_CLOSED}
${'shows with Open when status is reopened'} | ${STATUS_REOPENED}
`('$title', async ({ state }) => {
- wrapper.setProps({ issuableStatus: state });
-
- await nextTick();
+ await wrapper.setProps({ issuableStatus: state });
- expect(findStickyHeader().text()).toContain(IssuableStatusText[state]);
+ expect(findStickyHeader().text()).toContain(issuableStatusText[state]);
});
it.each`
@@ -519,9 +376,7 @@ describe('Issuable output', () => {
${'does not show confidential badge when issue is not confidential'} | ${false}
${'shows confidential badge when issue is confidential'} | ${true}
`('$title', async ({ isConfidential }) => {
- wrapper.setProps({ isConfidential });
-
- await nextTick();
+ await wrapper.setProps({ isConfidential });
const confidentialEl = findConfidentialBadge();
expect(confidentialEl.exists()).toBe(isConfidential);
@@ -538,9 +393,7 @@ describe('Issuable output', () => {
${'does not show locked badge when issue is not locked'} | ${false}
${'shows locked badge when issue is locked'} | ${true}
`('$title', async ({ isLocked }) => {
- wrapper.setProps({ isLocked });
-
- await nextTick();
+ await wrapper.setProps({ isLocked });
expect(findLockedBadge().exists()).toBe(isLocked);
});
@@ -550,9 +403,7 @@ describe('Issuable output', () => {
${'does not show hidden badge when issue is not hidden'} | ${false}
${'shows hidden badge when issue is hidden'} | ${true}
`('$title', async ({ isHidden }) => {
- wrapper.setProps({ isHidden });
-
- await nextTick();
+ await wrapper.setProps({ isHidden });
const hiddenBadge = findHiddenBadge();
@@ -569,6 +420,10 @@ describe('Issuable output', () => {
});
describe('Composable description component', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
const findIncidentTabs = () => wrapper.findComponent(IncidentTabs);
const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6';
@@ -587,13 +442,13 @@ describe('Issuable output', () => {
});
describe('when using incident tabs description wrapper', () => {
- beforeEach(() => {
- mountComponent(
- {
+ beforeEach(async () => {
+ await createComponent({
+ props: {
descriptionComponent: IncidentTabs,
showTitleBorder: false,
},
- {
+ options: {
mocks: {
$apollo: {
queries: {
@@ -604,7 +459,7 @@ describe('Issuable output', () => {
},
},
},
- );
+ });
});
it('does not the description component', () => {
@@ -622,48 +477,77 @@ describe('Issuable output', () => {
});
describe('taskListUpdateStarted', () => {
- it('stops polling', () => {
- jest.spyOn(wrapper.vm.poll, 'stop');
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('stops polling', async () => {
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
- wrapper.vm.taskListUpdateStarted();
+ findDescription().vm.$emit('taskListUpdateStarted');
- expect(wrapper.vm.poll.stop).toHaveBeenCalled();
+ await advanceToNextPoll();
+
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
});
});
describe('taskListUpdateSucceeded', () => {
- it('enables polling', () => {
- jest.spyOn(wrapper.vm.poll, 'enable');
- jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest');
+ beforeEach(async () => {
+ await createComponent();
+ findDescription().vm.$emit('taskListUpdateStarted');
+ });
- wrapper.vm.taskListUpdateSucceeded();
+ it('enables polling', async () => {
+ // Ensure that polling is not working before
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
+ await advanceToNextPoll();
- expect(wrapper.vm.poll.enable).toHaveBeenCalled();
- expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY);
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
+
+ // Enable Polling an move forward
+ findDescription().vm.$emit('taskListUpdateSucceeded');
+ await advanceToNextPoll();
+
+ // Title has changed: polling works!
+ expect(findTitle().props().titleText).toBe(secondRequest.title_text);
});
});
describe('taskListUpdateFailed', () => {
- it('enables polling and calls updateStoreState', () => {
- jest.spyOn(wrapper.vm.poll, 'enable');
- jest.spyOn(wrapper.vm.poll, 'makeDelayedRequest');
- jest.spyOn(wrapper.vm, 'updateStoreState');
+ beforeEach(async () => {
+ await createComponent();
+ findDescription().vm.$emit('taskListUpdateStarted');
+ });
+
+ it('enables polling and calls updateStoreState', async () => {
+ // Ensure that polling is not working before
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
+ await advanceToNextPoll();
- wrapper.vm.taskListUpdateFailed();
+ expect(findTitle().props().titleText).toBe(initialRequest.title_text);
- expect(wrapper.vm.poll.enable).toHaveBeenCalled();
- expect(wrapper.vm.poll.makeDelayedRequest).toHaveBeenCalledWith(POLLING_DELAY);
- expect(wrapper.vm.updateStoreState).toHaveBeenCalled();
+ // Enable Polling an move forward
+ findDescription().vm.$emit('taskListUpdateFailed');
+ await advanceToNextPoll();
+
+ // Title has changed: polling works!
+ expect(findTitle().props().titleText).toBe(secondRequest.title_text);
});
});
describe('saveDescription event', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
it('makes request to update issue', async () => {
const description = 'I have been updated!';
findDescription().vm.$emit('saveDescription', description);
+
await waitForPromises();
- expect(mock.history.put[0].data).toContain(description);
+ expect(axiosMock.history.put[0].data).toContain(description);
});
});
});
diff --git a/spec/frontend/issues/show/components/delete_issue_modal_spec.js b/spec/frontend/issues/show/components/delete_issue_modal_spec.js
index 97a091a1748..b8adeb24005 100644
--- a/spec/frontend/issues/show/components/delete_issue_modal_spec.js
+++ b/spec/frontend/issues/show/components/delete_issue_modal_spec.js
@@ -20,10 +20,6 @@ describe('DeleteIssueModal component', () => {
const mountComponent = (props = {}) =>
shallowMount(DeleteIssueModal, { propsData: { ...defaultProps, ...props } });
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('modal', () => {
it('renders', () => {
wrapper = mountComponent();
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index da51372dd3d..9a0cde15b24 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,25 +1,16 @@
-import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlModal } from '@gitlab/ui';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
-import setWindowLocation from 'helpers/set_window_location_helper';
-import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
-import { mockTracking } from 'helpers/tracking_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Description from '~/issues/show/components/description.vue';
import eventHub from '~/issues/show/event_hub';
-import { updateHistory } from '~/lib/utils/url_utility';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import TaskList from '~/task_list';
-import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import {
createWorkItemMutationErrorResponse,
@@ -30,39 +21,23 @@ import {
import {
descriptionProps as initialProps,
descriptionHtmlWithList,
- descriptionHtmlWithCheckboxes,
- descriptionHtmlWithTask,
+ descriptionHtmlWithDetailsTag,
} from '../mock_data/mock_data';
-jest.mock('~/flash');
-jest.mock('~/lib/utils/url_utility', () => ({
- ...jest.requireActual('~/lib/utils/url_utility'),
- updateHistory: jest.fn(),
-}));
+jest.mock('~/alert');
jest.mock('~/task_list');
jest.mock('~/behaviors/markdown/render_gfm');
const mockSpriteIcons = '/icons.svg';
-const showModal = jest.fn();
-const hideModal = jest.fn();
-const showDetailsModal = jest.fn();
const $toast = {
show: jest.fn(),
};
const issueDetailsResponse = getIssueDetailsResponse();
-const workItemQueryResponse = {
- data: {
- workItem: null,
- },
-};
-
-const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
describe('Description component', () => {
let wrapper;
- let originalGon;
Vue.use(VueApollo);
@@ -70,21 +45,16 @@ describe('Description component', () => {
const findTextarea = () => wrapper.find('[data-testid="textarea"]');
const findListItems = () => findGfmContent().findAll('ul > li');
const findTaskActionButtons = () => wrapper.findAll('.task-list-item-actions');
- const findTaskLink = () => wrapper.find('a.gfm-issue');
- const findModal = () => wrapper.findComponent(GlModal);
- const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
function createComponent({
props = {},
provide,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse),
createWorkItemMutationHandler,
- ...options
} = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
issueId: 1,
- issueIid: 1,
...initialProps,
...props,
},
@@ -94,7 +64,6 @@ describe('Description component', () => {
...provide,
},
apolloProvider: createMockApollo([
- [workItemQuery, queryHandler],
[workItemTypesQuery, workItemTypesQueryHandler],
[getIssueDetailsQuery, issueDetailsQueryHandler],
[createWorkItemMutation, createWorkItemMutationHandler],
@@ -102,43 +71,11 @@ describe('Description component', () => {
mocks: {
$toast,
},
- stubs: {
- GlModal: stubComponent(GlModal, {
- methods: {
- show: showModal,
- hide: hideModal,
- },
- }),
- WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
- methods: {
- show: showDetailsModal,
- },
- }),
- },
- ...options,
});
}
beforeEach(() => {
- originalGon = window.gon;
window.gon = { sprite_icons: mockSpriteIcons };
-
- setWindowLocation(TEST_HOST);
-
- if (!document.querySelector('.issuable-meta')) {
- const metaData = document.createElement('div');
- metaData.classList.add('issuable-meta');
- metaData.innerHTML =
- '<div class="flash-container"></div><span id="task_status"></span><span id="task_status_short"></span>';
-
- document.body.appendChild(metaData);
- }
- });
-
- afterAll(() => {
- window.gon = originalGon;
-
- $('.issuable-meta .flash-container').remove();
});
it('doesnt animate first description changes', async () => {
@@ -169,6 +106,19 @@ describe('Description component', () => {
expect(findGfmContent().classes()).toContain('issue-realtime-trigger-pulse');
});
+ it('doesnt animate expand/collapse of details elements', async () => {
+ createComponent();
+
+ await wrapper.setProps({ descriptionHtml: descriptionHtmlWithDetailsTag.collapsed });
+ expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse');
+
+ await wrapper.setProps({ descriptionHtml: descriptionHtmlWithDetailsTag.expanded });
+ expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse');
+
+ await wrapper.setProps({ descriptionHtml: descriptionHtmlWithDetailsTag.collapsed });
+ expect(findGfmContent().classes()).not.toContain('issue-realtime-pre-pulse');
+ });
+
it('applies syntax highlighting and math when description changed', async () => {
createComponent();
@@ -203,7 +153,7 @@ describe('Description component', () => {
expect(TaskList).toHaveBeenCalled();
});
- it('does not re-init the TaskList when canUpdate is false', async () => {
+ it('does not re-init the TaskList when canUpdate is false', () => {
createComponent({
props: {
issuableType: 'issuableType',
@@ -239,53 +189,12 @@ describe('Description component', () => {
});
});
- describe('taskStatus', () => {
- it('adds full taskStatus', async () => {
- createComponent({
- props: {
- taskStatus: '1 of 1',
- },
- });
- await nextTick();
-
- expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(
- '1 of 1',
- );
- });
-
- it('adds short taskStatus', async () => {
- createComponent({
- props: {
- taskStatus: '1 of 1',
- },
- });
- await nextTick();
-
- expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe(
- '1/1 checklist item',
- );
- });
-
- it('clears task status text when no tasks are present', async () => {
- createComponent({
- props: {
- taskStatus: '0 of 0',
- },
- });
-
- await nextTick();
-
- expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe('');
- });
- });
-
describe('with list', () => {
beforeEach(async () => {
createComponent({
props: {
descriptionHtml: descriptionHtmlWithList,
},
- attachTo: document.body,
});
await nextTick();
});
@@ -325,33 +234,6 @@ describe('Description component', () => {
});
});
- describe('description with checkboxes', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- });
- return nextTick();
- });
-
- it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
- expect(findTaskActionButtons()).toHaveLength(3);
- });
-
- it('does not show a modal by default', () => {
- expect(findModal().exists()).toBe(false);
- });
-
- it('shows toast after delete success', async () => {
- const newDesc = 'description';
- findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
-
- expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
- expect($toast.show).toHaveBeenCalledWith('Task deleted');
- });
- });
-
describe('task list item actions', () => {
describe('converting the task list item to a task', () => {
describe('when successful', () => {
@@ -391,11 +273,7 @@ describe('Description component', () => {
});
it('calls a mutation to create a task', () => {
- const {
- confidential,
- iteration,
- milestone,
- } = issueDetailsResponse.data.workspace.issuable;
+ const { confidential, iteration, milestone } = issueDetailsResponse.data.issue;
expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
input: {
confidential,
@@ -468,109 +346,4 @@ describe('Description component', () => {
});
});
});
-
- describe('work items detail', () => {
- describe('when opening and closing', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- });
- return nextTick();
- });
-
- it('opens when task button is clicked', async () => {
- await findTaskLink().trigger('click');
-
- expect(showDetailsModal).toHaveBeenCalled();
- expect(updateHistory).toHaveBeenCalledWith({
- url: `${TEST_HOST}/?work_item_id=2`,
- replace: true,
- });
- });
-
- it('closes from an open state', async () => {
- await findTaskLink().trigger('click');
-
- findWorkItemDetailModal().vm.$emit('close');
- await nextTick();
-
- expect(updateHistory).toHaveBeenLastCalledWith({
- url: `${TEST_HOST}/`,
- replace: true,
- });
- });
-
- it('tracks when opened', async () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- await findTaskLink().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'viewed_work_item_from_modal',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'work_item_view',
- property: 'type_task',
- },
- );
- });
- });
-
- describe('when url query `work_item_id` exists', () => {
- it.each`
- behavior | workItemId | modalOpened
- ${'opens'} | ${'2'} | ${1}
- ${'does not open'} | ${'123'} | ${0}
- ${'does not open'} | ${'123e'} | ${0}
- ${'does not open'} | ${'12e3'} | ${0}
- ${'does not open'} | ${'1e23'} | ${0}
- ${'does not open'} | ${'x'} | ${0}
- ${'does not open'} | ${'undefined'} | ${0}
- `(
- '$behavior when url contains `work_item_id=$workItemId`',
- async ({ workItemId, modalOpened }) => {
- setWindowLocation(`?work_item_id=${workItemId}`);
-
- createComponent({
- props: { descriptionHtml: descriptionHtmlWithTask },
- });
-
- expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
- },
- );
- });
- });
-
- describe('when hovering task links', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- });
- return nextTick();
- });
-
- it('prefetches work item detail after work item link is hovered for 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- jest.advanceTimersByTime(150);
- await waitForPromises();
-
- expect(queryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/2',
- });
- });
-
- it('does not work item detail after work item link is hovered for less than 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- await findTaskLink().trigger('mouseout');
- jest.advanceTimersByTime(150);
- await waitForPromises();
-
- expect(queryHandler).not.toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js
index 11c43ea4388..0ebeb1b7b56 100644
--- a/spec/frontend/issues/show/components/edit_actions_spec.js
+++ b/spec/frontend/issues/show/components/edit_actions_spec.js
@@ -56,10 +56,6 @@ describe('Edit Actions component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all buttons as enabled', () => {
const buttons = findEditButtons().wrappers;
buttons.forEach((button) => {
@@ -70,7 +66,7 @@ describe('Edit Actions component', () => {
it('disables save button when title is blank', () => {
createComponent({ props: { formState: { title: '', issue_type: '' } } });
- expect(findSaveButton().attributes('disabled')).toBe('true');
+ expect(findSaveButton().attributes('disabled')).toBeDefined();
});
describe('updateIssuable', () => {
diff --git a/spec/frontend/issues/show/components/edited_spec.js b/spec/frontend/issues/show/components/edited_spec.js
index aa6e0a9dceb..dc0c7f5be46 100644
--- a/spec/frontend/issues/show/components/edited_spec.js
+++ b/spec/frontend/issues/show/components/edited_spec.js
@@ -1,22 +1,72 @@
+import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { getTimeago } from '~/lib/utils/datetime_utility';
import Edited from '~/issues/show/components/edited.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-const timeago = getTimeago();
-
describe('Edited component', () => {
let wrapper;
- const findAuthorLink = () => wrapper.find('a');
+ const timeago = getTimeago();
+ const updatedAt = '2017-05-15T12:31:04.428Z';
+
+ const findAuthorLink = () => wrapper.findComponent(GlLink);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
const formatText = (text) => text.trim().replace(/\s\s+/g, ' ');
const mountComponent = (propsData) => mount(Edited, { propsData });
- const updatedAt = '2017-05-15T12:31:04.428Z';
- afterEach(() => {
- wrapper.destroy();
+ describe('task status section', () => {
+ describe('task status text', () => {
+ it('renders when there is a task status', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 1, count: 3 } });
+
+ expect(wrapper.text()).toContain('1 of 3 checklist items completed');
+ });
+
+ it('does not render when task count is 0', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 0, count: 0 } });
+
+ expect(wrapper.text()).not.toContain('0 of 0 checklist items completed');
+ });
+ });
+
+ describe('checkmark', () => {
+ it('renders when all tasks are completed', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 3, count: 3 } });
+
+ expect(wrapper.text()).toContain('✓');
+ });
+
+ it('does not render when tasks are incomplete', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 2, count: 3 } });
+
+ expect(wrapper.text()).not.toContain('✓');
+ });
+
+ it('does not render when task count is 0', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 0, count: 0 } });
+
+ expect(wrapper.text()).not.toContain('✓');
+ });
+ });
+
+ describe('middot', () => {
+ it('renders when there is also "Edited by" text', () => {
+ wrapper = mountComponent({
+ taskCompletionStatus: { completed_count: 3, count: 3 },
+ updatedAt,
+ });
+
+ expect(wrapper.text()).toContain('·');
+ });
+
+ it('does not render when there is no "Edited by" text', () => {
+ wrapper = mountComponent({ taskCompletionStatus: { completed_count: 3, count: 3 } });
+
+ expect(wrapper.text()).not.toContain('·');
+ });
+ });
});
it('renders an edited at+by string', () => {
@@ -31,17 +81,6 @@ describe('Edited component', () => {
expect(findTimeAgoTooltip().exists()).toBe(true);
});
- it('if no updatedAt is provided, no time element will be rendered', () => {
- wrapper = mountComponent({
- updatedByName: 'Some User',
- updatedByPath: '/some_user',
- });
-
- expect(formatText(wrapper.text())).toBe('Edited by Some User');
- expect(findAuthorLink().attributes('href')).toBe('/some_user');
- expect(findTimeAgoTooltip().exists()).toBe(false);
- });
-
it('if no updatedByName and updatedByPath is provided, no user element will be rendered', () => {
wrapper = mountComponent({
updatedAt,
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 273ddfdd5d4..c7116f380a1 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -33,11 +33,6 @@ describe('Description field component', () => {
jest.spyOn(eventHub, '$emit');
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders markdown field with description', () => {
wrapper = mountComponent();
@@ -80,17 +75,15 @@ describe('Description field component', () => {
});
it('uses the MarkdownEditor component to edit markdown', () => {
- expect(findMarkdownEditor().props()).toEqual(
- expect.objectContaining({
- value: 'test',
- renderMarkdownPath: '/',
- markdownDocsPath: '/',
- quickActionsDocsPath: expect.any(String),
- autofocus: true,
- supportsQuickActions: true,
- enableAutocomplete: true,
- }),
- );
+ expect(findMarkdownEditor().props()).toMatchObject({
+ value: 'test',
+ renderMarkdownPath: '/',
+ autofocus: true,
+ supportsQuickActions: true,
+ quickActionsDocsPath: expect.any(String),
+ markdownDocsPath: '/',
+ enableAutocomplete: true,
+ });
});
it('triggers update with meta+enter', () => {
diff --git a/spec/frontend/issues/show/components/fields/description_template_spec.js b/spec/frontend/issues/show/components/fields/description_template_spec.js
index 79a3bfa9840..1e8d5e2dd95 100644
--- a/spec/frontend/issues/show/components/fields/description_template_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_template_spec.js
@@ -22,10 +22,6 @@ describe('Issue description template component with templates as hash', () => {
wrapper = shallowMount(descriptionTemplate, options);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders templates as JSON hash in data attribute', () => {
createComponent();
expect(findIssuableSelector().attributes('data-data')).toBe(
diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js
index a5fa96d8d64..b28762f1520 100644
--- a/spec/frontend/issues/show/components/fields/title_spec.js
+++ b/spec/frontend/issues/show/components/fields/title_spec.js
@@ -17,11 +17,6 @@ describe('Title field component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders form control with formState title', () => {
expect(findInput().element.value).toBe('test');
});
diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js
index 27ac0e1baf3..e655cf3b37d 100644
--- a/spec/frontend/issues/show/components/fields/type_spec.js
+++ b/spec/frontend/issues/show/components/fields/type_spec.js
@@ -1,4 +1,4 @@
-import { GlFormGroup, GlListbox, GlIcon } from '@gitlab/ui';
+import { GlFormGroup, GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -32,7 +32,7 @@ describe('Issue type field component', () => {
},
};
- const findListBox = () => wrapper.findComponent(GlListbox);
+ const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findAllIssueItems = () => wrapper.findAll('[data-testid="issue-type-list-item"]');
const findIssueItemAt = (at) => findAllIssueItems().at(at);
@@ -60,10 +60,6 @@ describe('Issue type field component', () => {
mockIssueStateData = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
at | text | icon
${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon}
diff --git a/spec/frontend/issues/show/components/form_spec.js b/spec/frontend/issues/show/components/form_spec.js
index aedb974cbd0..b8ed33801f2 100644
--- a/spec/frontend/issues/show/components/form_spec.js
+++ b/spec/frontend/issues/show/components/form_spec.js
@@ -30,10 +30,6 @@ describe('Inline edit form component', () => {
projectNamespace: '/',
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const createComponent = (props) => {
wrapper = shallowMount(formComponent, {
propsData: {
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 3d9dad3a721..a5ba512434c 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -1,28 +1,37 @@
import Vue, { nextTick } from 'vue';
-import { GlButton, GlDropdownItem, GlLink, GlModal } from '@gitlab/ui';
+import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { IssueType, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
+import issuesEventHub from '~/issues/show/event_hub';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
+import updateIssueMutation from '~/issues/show/queries/update_issue.mutation.graphql';
+import toast from '~/vue_shared/plugins/global_toast';
-jest.mock('~/flash');
+jest.mock('~/alert');
+jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() }));
+jest.mock('~/vue_shared/plugins/global_toast');
describe('HeaderActions component', () => {
let dispatchEventSpy;
- let mutateMock;
let wrapper;
let visitUrlSpy;
Vue.use(Vuex);
+ Vue.use(VueApollo);
const store = createStore();
@@ -36,22 +45,35 @@ describe('HeaderActions component', () => {
iid: '32',
isIssueAuthor: true,
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
- issueType: IssueType.Issue,
+ issueType: TYPE_ISSUE,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
reportAbusePath: '-/abuse_reports/add_category',
reportedUserId: 1,
reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32',
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
+ issuableEmailAddress: null,
+ fullPath: 'full-path',
};
- const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } };
+ const updateIssueMutationResponse = {
+ data: {
+ updateIssue: {
+ errors: [],
+ issuable: {
+ id: 'gid://gitlab/Issue/511',
+ state: STATUS_OPEN,
+ },
+ },
+ },
+ };
const promoteToEpicMutationResponse = {
data: {
promoteToEpic: {
errors: [],
epic: {
+ id: 'gid://gitlab/Epic/1',
webPath: '/groups/gitlab-org/-/epics/1',
},
},
@@ -67,42 +89,81 @@ describe('HeaderActions component', () => {
},
};
- const findToggleIssueStateButton = () => wrapper.findComponent(GlButton);
+ const mockIssueReferenceData = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/7',
+ issuable: {
+ id: 'gid://gitlab/Issue/511',
+ reference: 'flightjs/Flight#33',
+ __typename: 'Issue',
+ },
+ __typename: 'Project',
+ },
+ },
+ };
+
+ const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`);
+ const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`);
const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`);
const findMobileDropdown = () => findDropdownBy('mobile-dropdown');
const findDesktopDropdown = () => findDropdownBy('desktop-dropdown');
const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+ const findReportAbuseSelectorItem = () => wrapper.find(`[data-testid="report-abuse-item"]`);
+ const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
+ const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`);
+ const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`);
+ const findCopyEmailItem = () => wrapper.find(`[data-testid="copy-email"]`);
const findModal = () => wrapper.findComponent(GlModal);
const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index);
+ const issueReferenceSuccessHandler = jest.fn().mockResolvedValue(mockIssueReferenceData);
+ const updateIssueMutationResponseHandler = jest
+ .fn()
+ .mockResolvedValue(updateIssueMutationResponse);
+ const promoteToEpicMutationSuccessResponseHandler = jest
+ .fn()
+ .mockResolvedValue(promoteToEpicMutationResponse);
+ const promoteToEpicMutationErrorHandler = jest
+ .fn()
+ .mockResolvedValue(promoteToEpicMutationErrorResponse);
+
const mountComponent = ({
props = {},
issueState = STATUS_OPEN,
blockedByIssues = [],
- mutateResponse = {},
+ movedMrSidebarEnabled = false,
+ promoteToEpicHandler = promoteToEpicMutationSuccessResponseHandler,
} = {}) => {
- mutateMock = jest.fn().mockResolvedValue(mutateResponse);
-
store.dispatch('setNoteableData', {
blocked_by_issues: blockedByIssues,
state: issueState,
});
+ const handlers = [
+ [issueReferenceQuery, issueReferenceSuccessHandler],
+ [updateIssueMutation, updateIssueMutationResponseHandler],
+ [promoteToEpicMutation, promoteToEpicHandler],
+ ];
+
return shallowMount(HeaderActions, {
+ apolloProvider: createMockApollo(handlers),
store,
provide: {
...defaultProps,
...props,
- },
- mocks: {
- $apollo: {
- mutate: mutateMock,
+ glFeatures: {
+ movedMrSidebar: movedMrSidebarEnabled,
},
},
+ stubs: {
+ GlButton,
+ },
});
};
@@ -113,13 +174,12 @@ describe('HeaderActions component', () => {
if (visitUrlSpy) {
visitUrlSpy.mockRestore();
}
- wrapper.destroy();
});
describe.each`
issueType
- ${IssueType.Issue}
- ${IssueType.Incident}
+ ${TYPE_ISSUE}
+ ${TYPE_INCIDENT}
`('when issue type is $issueType', ({ issueType }) => {
describe('close/reopen button', () => {
describe.each`
@@ -133,7 +193,6 @@ describe('HeaderActions component', () => {
wrapper = mountComponent({
props: { issueType },
issueState,
- mutateResponse: updateIssueMutationResponse,
});
});
@@ -144,23 +203,19 @@ describe('HeaderActions component', () => {
it('calls apollo mutation', () => {
findToggleIssueStateButton().vm.$emit('click');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- stateEvent: newIssueState,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: newIssueState,
+ },
+ });
});
it('dispatches a custom event to update the issue page', async () => {
findToggleIssueStateButton().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
});
@@ -240,6 +295,30 @@ describe('HeaderActions component', () => {
});
});
});
+
+ describe(`show edit button ${issueType}`, () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ canUpdateIssue: true,
+ canCreateIssue: false,
+ isIssueAuthor: true,
+ issueType,
+ canReportSpam: false,
+ canPromoteToEpic: false,
+ },
+ });
+ });
+ it(`shows the edit button`, () => {
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('should trigger "open.form" event when clicked', async () => {
+ expect(issuesEventHub.$emit).not.toHaveBeenCalled();
+ await findEditButton().trigger('click');
+ expect(issuesEventHub.$emit).toHaveBeenCalledWith('open.form');
+ });
+ });
});
describe('delete issue button', () => {
@@ -261,28 +340,25 @@ describe('HeaderActions component', () => {
describe('when "Promote to epic" button is clicked', () => {
describe('when response is successful', () => {
- beforeEach(() => {
+ beforeEach(async () => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
- mutateResponse: promoteToEpicMutationResponse,
+ promoteToEpicHandler: promoteToEpicMutationSuccessResponseHandler,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+
+ await waitForPromises();
});
it('invokes GraphQL mutation when clicked', () => {
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- mutation: promoteToEpicMutation,
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- },
- },
- }),
- );
+ expect(promoteToEpicMutationSuccessResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ },
+ });
});
it('shows a success message and tells the user they are being redirected', () => {
@@ -300,14 +376,16 @@ describe('HeaderActions component', () => {
});
describe('when response contains errors', () => {
- beforeEach(() => {
+ beforeEach(async () => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
- mutateResponse: promoteToEpicMutationErrorResponse,
+ promoteToEpicHandler: promoteToEpicMutationErrorHandler,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
+
+ await waitForPromises();
});
it('shows an error message', () => {
@@ -320,21 +398,17 @@ describe('HeaderActions component', () => {
describe('when `toggle.issuable.state` event is emitted', () => {
it('invokes a method to toggle the issue state', () => {
- wrapper = mountComponent({ mutateResponse: updateIssueMutationResponse });
+ wrapper = mountComponent();
eventHub.$emit('toggle.issuable.state');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- stateEvent: ISSUE_STATE_EVENT_CLOSE,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
+ },
+ });
});
});
@@ -363,17 +437,13 @@ describe('HeaderActions component', () => {
it('calls apollo mutation when primary button is clicked', () => {
findModal().vm.$emit('primary');
- expect(mutateMock).toHaveBeenCalledWith(
- expect.objectContaining({
- variables: {
- input: {
- iid: defaultProps.iid.toString(),
- projectPath: defaultProps.projectPath,
- stateEvent: ISSUE_STATE_EVENT_CLOSE,
- },
- },
- }),
- );
+ expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
+ input: {
+ iid: defaultProps.iid.toString(),
+ projectPath: defaultProps.projectPath,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
+ },
+ });
});
describe.each`
@@ -405,18 +475,16 @@ describe('HeaderActions component', () => {
});
describe('abuse category selector', () => {
- const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
-
beforeEach(() => {
wrapper = mountComponent({ props: { isIssueAuthor: false } });
});
- it("doesn't render", async () => {
+ it("doesn't render", () => {
expect(findAbuseCategorySelector().exists()).toEqual(false);
});
it('opens the drawer', async () => {
- findDesktopDropdownItems().at(2).vm.$emit('click');
+ findReportAbuseSelectorItem().vm.$emit('click');
await nextTick();
@@ -424,10 +492,160 @@ describe('HeaderActions component', () => {
});
it('closes the drawer', async () => {
- await findDesktopDropdownItems().at(2).vm.$emit('click');
+ await findReportAbuseSelectorItem().vm.$emit('click');
await findAbuseCategorySelector().vm.$emit('close-drawer');
expect(findAbuseCategorySelector().exists()).toEqual(false);
});
});
+
+ describe('notification toggle', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${true}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`,
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Notification toggle`, () => {
+ expect(findNotificationWidget().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+ });
+
+ describe('lock issue option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${false}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`,
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Lock issue option`, () => {
+ expect(findLockIssueWidget().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+ });
+
+ describe('copy reference option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | visible
+ ${true} | ${TYPE_ISSUE} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${true}
+ ${false} | ${TYPE_ISSUE} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${false}
+ `(
+ 'when movedMrSidebarFlagEnabled is "$movedMrSidebarEnabled" with issue type "$issueType"',
+ ({ movedMrSidebarEnabled, issueType, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Copy reference option`, () => {
+ expect(findCopyRefenceDropdownItem().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+
+ describe('clicking when visible', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType: TYPE_ISSUE,
+ },
+ movedMrSidebarEnabled: true,
+ });
+ });
+
+ it('shows toast message', () => {
+ findCopyRefenceDropdownItem().vm.$emit('click');
+
+ expect(toast).toHaveBeenCalledWith('Reference copied');
+ });
+ });
+ });
+
+ describe('copy email option', () => {
+ describe('visibility', () => {
+ describe.each`
+ movedMrSidebarEnabled | issueType | issuableEmailAddress | visible
+ ${true} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${true}
+ ${true} | ${TYPE_ISSUE} | ${''} | ${false}
+ ${true} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${true}
+ ${true} | ${TYPE_INCIDENT} | ${''} | ${false}
+ ${false} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${false}
+ ${false} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${false}
+ `(
+ 'when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" issue type is "$issueType" and issuableEmailAddress="$issuableEmailAddress"',
+ ({ movedMrSidebarEnabled, issueType, issuableEmailAddress, visible }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType,
+ issuableEmailAddress,
+ },
+ movedMrSidebarEnabled,
+ });
+ });
+
+ it(`${visible ? 'shows' : 'hides'} Copy email option`, () => {
+ expect(findCopyEmailItem().exists()).toBe(visible);
+ });
+ },
+ );
+ });
+
+ describe('clicking when visible', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ props: {
+ issueType: TYPE_ISSUE,
+ issuableEmailAddress: 'mock-email-address',
+ },
+ movedMrSidebarEnabled: true,
+ });
+ });
+
+ it('shows toast message', () => {
+ findCopyEmailItem().vm.$emit('click');
+
+ expect(toast).toHaveBeenCalledWith('Email address copied');
+ });
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
index 6c923cae0cc..b13a1041eda 100644
--- a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
@@ -9,7 +9,7 @@ import createTimelineEventMutation from '~/issues/show/components/incidents/grap
import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { useFakeDate } from 'helpers/fake_date';
import {
timelineEventsCreateEventResponse,
@@ -19,7 +19,7 @@ import {
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
const fakeDate = '2020-07-08T00:00:00.000Z';
@@ -86,7 +86,6 @@ describe('Create Timeline events', () => {
provide: {
fullPath: 'group/project',
issuableId: '1',
- glFeatures: { incidentEventTags: true },
},
apolloProvider,
});
@@ -99,7 +98,6 @@ describe('Create Timeline events', () => {
afterEach(() => {
createAlert.mockReset();
- wrapper.destroy();
});
describe('createIncidentTimelineEvent', () => {
diff --git a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
index 1cfb7d12a91..ad730fd69f7 100644
--- a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
+++ b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js
@@ -34,13 +34,6 @@ describe('Highlight Bar', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findLink = () => wrapper.findComponent(GlLink);
describe('empty state', () => {
diff --git a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
index 33a3a6eddfc..5a49b29c458 100644
--- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js
@@ -1,4 +1,5 @@
import merge from 'lodash/merge';
+import { nextTick } from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import DescriptionComponent from '~/issues/show/components/description.vue';
@@ -11,6 +12,11 @@ import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import { descriptionProps } from '../../mock_data/mock_data';
+const push = jest.fn();
+const $router = {
+ push,
+};
+
const mockAlert = {
__typename: 'AlertManagementAlert',
detailsUrl: INVALID_URL,
@@ -28,12 +34,20 @@ const defaultMocks = {
},
},
},
+ $route: { params: {} },
+ $router,
};
describe('Incident Tabs component', () => {
let wrapper;
- const mountComponent = ({ data = {}, options = {}, mount = shallowMountExtended } = {}) => {
+ const mountComponent = ({
+ data = {},
+ options = {},
+ mount = shallowMountExtended,
+ hasLinkedAlerts = false,
+ mocks = {},
+ } = {}) => {
wrapper = mount(
IncidentTabs,
merge(
@@ -54,11 +68,12 @@ describe('Incident Tabs component', () => {
slaFeatureAvailable: true,
canUpdate: true,
canUpdateTimelineEvent: true,
+ hasLinkedAlerts,
},
data() {
return { alert: mockAlert, ...data };
},
- mocks: defaultMocks,
+ mocks: { ...defaultMocks, ...mocks },
},
options,
),
@@ -102,11 +117,13 @@ describe('Incident Tabs component', () => {
});
it('renders the alert details tab', () => {
+ mountComponent({ hasLinkedAlerts: true });
expect(findAlertDetailsTab().exists()).toBe(true);
expect(findAlertDetailsTab().attributes('title')).toBe('Alert details');
});
it('renders the alert details table with the correct props', () => {
+ mountComponent({ hasLinkedAlerts: true });
const alert = { iid: mockAlert.iid };
expect(findAlertDetailsComponent().props('alert')).toMatchObject(alert);
@@ -146,7 +163,7 @@ describe('Incident Tabs component', () => {
mountComponent({ mount: mountExtended });
});
- it('shows only the summary tab by default', async () => {
+ it('shows only the summary tab by default', () => {
expect(findActiveTabs()).toHaveLength(1);
expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.summaryTitle);
});
@@ -156,6 +173,40 @@ describe('Incident Tabs component', () => {
expect(findActiveTabs()).toHaveLength(1);
expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle);
+ expect(push).toHaveBeenCalledWith('/timeline');
+ });
+ });
+
+ describe('loading page with tab', () => {
+ it('shows the timeline tab when timeline path is passed', async () => {
+ mountComponent({
+ mount: mountExtended,
+ mocks: { $route: { params: { tabId: 'timeline' } } },
+ });
+ await nextTick();
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.timelineTitle);
+ });
+
+ it('shows the alerts tab when timeline path is passed', async () => {
+ mountComponent({
+ mount: mountExtended,
+ mocks: { $route: { params: { tabId: 'alerts' } } },
+ hasLinkedAlerts: true,
+ });
+ await nextTick();
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.alertsTitle);
+ });
+
+ it('shows the metrics tab when metrics path is passed', async () => {
+ mountComponent({
+ mount: mountExtended,
+ mocks: { $route: { params: { tabId: 'metrics' } } },
+ });
+ await nextTick();
+ expect(findActiveTabs()).toHaveLength(1);
+ expect(findActiveTabs().at(0).text()).toBe(incidentTabsI18n.metricsTitle);
});
});
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index e352f9708e4..9c4662ce38f 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -10,12 +10,12 @@ import {
TIMELINE_EVENT_TAGS,
timelineEventTagsI18n,
} from '~/issues/show/components/incidents/constants';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { useFakeDate } from 'helpers/fake_date';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
const fakeDate = '2020-07-08T00:00:00.000Z';
@@ -51,7 +51,6 @@ describe('Timeline events form', () => {
afterEach(() => {
createAlert.mockReset();
- wrapper.destroy();
});
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
@@ -114,17 +113,7 @@ describe('Timeline events form', () => {
]);
});
- describe('with incident_event_tag feature flag enabled', () => {
- beforeEach(() => {
- mountComponent(
- {},
- {},
- {
- incidentEventTags: true,
- },
- );
- });
-
+ describe('Event Tags', () => {
describe('event tags listbox', () => {
it('should render option list from provided array', () => {
expect(findTagsListbox().props('items')).toEqual(mockTags);
@@ -256,7 +245,7 @@ describe('Timeline events form', () => {
expect(findMinuteInput().element.value).toBe('0');
});
- it('should disable the save buttons when event content does not exist', async () => {
+ it('should disable the save buttons when event content does not exist', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
expect(findSubmitAndAddButton().props('disabled')).toBe(true);
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
index 26fda877089..8d79dece888 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
@@ -11,7 +11,7 @@ import deleteTimelineEventMutation from '~/issues/show/components/incidents/grap
import editTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { useFakeDate } from 'helpers/fake_date';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
mockEvents,
timelineEventsDeleteEventResponse,
@@ -26,7 +26,7 @@ import {
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
const mockConfirmAction = ({ confirmed }) => {
@@ -77,10 +77,6 @@ describe('IncidentTimelineEventList', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('groups items correctly', () => {
expect(findTimelineEventGroups()).toHaveLength(2);
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
index 63474070701..41c103d5bcb 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
@@ -8,13 +8,13 @@ import IncidentTimelineEventsList from '~/issues/show/components/incidents/timel
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { timelineTabI18n } from '~/issues/show/components/incidents/constants';
import { timelineEventsQueryListResponse, timelineEventsQueryEmptyResponse } from './mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
const graphQLError = new Error('GraphQL error');
const listResponse = jest.fn().mockResolvedValue(timelineEventsQueryListResponse);
@@ -44,12 +44,6 @@ describe('TimelineEventsTab', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTimelineEventsList = () => wrapper.findComponent(IncidentTimelineEventsList);
diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js
index 75be17f9889..8ee0d906dd4 100644
--- a/spec/frontend/issues/show/components/incidents/utils_spec.js
+++ b/spec/frontend/issues/show/components/incidents/utils_spec.js
@@ -5,10 +5,10 @@ import {
getUtcShiftedDate,
getPreviousEventTags,
} from '~/issues/show/components/incidents/utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { mockTimelineEventTags } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('incident utils', () => {
describe('display and log error', () => {
diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js
index dd3c7c58380..2e786b665d0 100644
--- a/spec/frontend/issues/show/components/locked_warning_spec.js
+++ b/spec/frontend/issues/show/components/locked_warning_spec.js
@@ -13,11 +13,6 @@ describe('LockedWarning component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
@@ -40,11 +35,11 @@ describe('LockedWarning component', () => {
expect(alert.props('dismissible')).toBe(false);
});
- it(`displays correct message`, async () => {
+ it(`displays correct message`, () => {
expect(alert.text()).toMatchInterpolatedText(sprintf(i18n.alertMessage, { issuableType }));
});
- it(`displays a link with correct text`, async () => {
+ it(`displays a link with correct text`, () => {
expect(link.exists()).toBe(true);
expect(link.text()).toBe(`the ${issuableType}`);
});
diff --git a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
new file mode 100644
index 00000000000..bf3e81c7d3a
--- /dev/null
+++ b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
@@ -0,0 +1,77 @@
+import { GlPopover } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
+import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
+import * as utils from '~/lib/utils/common_utils';
+
+describe('NewHeaderActionsPopover', () => {
+ let wrapper;
+
+ const createComponent = ({ issueType = TYPE_ISSUE, movedMrSidebarEnabled = true }) => {
+ wrapper = shallowMountExtended(NewHeaderActionsPopover, {
+ propsData: {
+ issueType,
+ },
+ stubs: {
+ GlPopover,
+ },
+ provide: {
+ glFeatures: {
+ movedMrSidebar: movedMrSidebarEnabled,
+ },
+ },
+ });
+ };
+
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findConfirmButton = () => wrapper.findByTestId('confirm-button');
+
+ it('should not be visible when the feature flag :moved_mr_sidebar is disabled', () => {
+ createComponent({ movedMrSidebarEnabled: false });
+ expect(findPopover().exists()).toBe(false);
+ });
+
+ describe('without the popover cookie', () => {
+ beforeEach(() => {
+ utils.setCookie = jest.fn();
+
+ createComponent({});
+ });
+
+ it('renders the popover with correct text', () => {
+ expect(findPopover().exists()).toBe(true);
+ expect(findPopover().text()).toContain('issue actions');
+ });
+
+ it('does not call setCookie', () => {
+ expect(utils.setCookie).not.toHaveBeenCalled();
+ });
+
+ describe('when the confirm button is clicked', () => {
+ beforeEach(() => {
+ findConfirmButton().vm.$emit('click');
+ });
+
+ it('sets the popover cookie', () => {
+ expect(utils.setCookie).toHaveBeenCalledWith(NEW_ACTIONS_POPOVER_KEY, true);
+ });
+
+ it('hides the popover', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('with the popover cookie', () => {
+ beforeEach(() => {
+ jest.spyOn(utils, 'getCookie').mockReturnValue('true');
+
+ createComponent({});
+ });
+
+ it('does not render the popover', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
index d4202f4a6ab..02b20b9e7b7 100644
--- a/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
+++ b/spec/frontend/issues/show/components/sentry_error_stack_trace_spec.js
@@ -53,12 +53,6 @@ describe('Sentry Error Stack Trace', () => {
});
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('loading', () => {
it('should show spinner while loading', () => {
mountComponent();
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 d52f9d57453..7dacbefaeff 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,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
import eventHub from '~/issues/show/event_hub';
@@ -6,9 +6,9 @@ import eventHub from '~/issues/show/event_hub';
describe('TaskListItemActions component', () => {
let wrapper;
- const findGlDropdown = () => wrapper.findComponent(GlDropdown);
- const findConvertToTaskItem = () => wrapper.findAllComponents(GlDropdownItem).at(0);
- const findDeleteItem = () => wrapper.findAllComponents(GlDropdownItem).at(1);
+ const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findConvertToTaskItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(0);
+ const findDeleteItem = () => wrapper.findAllComponents(GlDisclosureDropdownItem).at(1);
const mountComponent = () => {
const li = document.createElement('li');
@@ -17,9 +17,10 @@ describe('TaskListItemActions component', () => {
document.body.appendChild(li);
wrapper = shallowMount(TaskListItemActions, {
- provide: { canUpdate: true, toggleClass: 'task-list-item-actions' },
+ provide: { canUpdate: true },
attachTo: document.querySelector('div'),
});
+ wrapper.vm.$refs.dropdown.close = jest.fn();
};
beforeEach(() => {
@@ -30,8 +31,8 @@ describe('TaskListItemActions component', () => {
expect(findGlDropdown().props()).toMatchObject({
category: 'tertiary',
icon: 'ellipsis_v',
- right: true,
- text: TaskListItemActions.i18n.taskActions,
+ placement: 'right',
+ toggleText: TaskListItemActions.i18n.taskActions,
textSrOnly: true,
});
});
@@ -39,7 +40,7 @@ describe('TaskListItemActions component', () => {
it('emits event when `Convert to task` dropdown item is clicked', () => {
jest.spyOn(eventHub, '$emit');
- findConvertToTaskItem().vm.$emit('click');
+ findConvertToTaskItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
});
@@ -47,7 +48,7 @@ describe('TaskListItemActions component', () => {
it('emits event when `Delete` dropdown item is clicked', () => {
jest.spyOn(eventHub, '$emit');
- findDeleteItem().vm.$emit('click');
+ findDeleteItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
});
diff --git a/spec/frontend/issues/show/components/title_spec.js b/spec/frontend/issues/show/components/title_spec.js
index 7560b733ae6..16ac675e12c 100644
--- a/spec/frontend/issues/show/components/title_spec.js
+++ b/spec/frontend/issues/show/components/title_spec.js
@@ -1,96 +1,59 @@
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import titleComponent from '~/issues/show/components/title.vue';
-import eventHub from '~/issues/show/event_hub';
-import Store from '~/issues/show/stores';
+import Title from '~/issues/show/components/title.vue';
describe('Title component', () => {
- let vm;
- beforeEach(() => {
+ let wrapper;
+
+ const getTitleHeader = () => wrapper.findByTestId('issue-title');
+
+ const createWrapper = (props) => {
setHTMLFixture(`<title />`);
- const Component = Vue.extend(titleComponent);
- const store = new Store({
- titleHtml: '',
- descriptionHtml: '',
- issuableRef: '',
- });
- vm = new Component({
+ wrapper = shallowMountExtended(Title, {
propsData: {
issuableRef: '#1',
titleHtml: 'Testing <img />',
titleText: 'Testing',
- showForm: false,
- formState: store.formState,
+ ...props,
},
- }).$mount();
- });
+ });
+ };
afterEach(() => {
resetHTMLFixture();
});
it('renders title HTML', () => {
- expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
- });
-
- it('updates page title when changing titleHtml', async () => {
- const spy = jest.spyOn(vm, 'setPageTitle');
- vm.titleHtml = 'test';
+ createWrapper();
- await nextTick();
- expect(spy).toHaveBeenCalled();
+ expect(getTitleHeader().element.innerHTML.trim()).toBe('Testing <img>');
});
it('animates title changes', async () => {
- vm.titleHtml = 'test';
+ createWrapper();
- await nextTick();
+ await wrapper.setProps({
+ titleHtml: 'test',
+ });
- expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse');
- jest.runAllTimers();
+ expect(getTitleHeader().classes('issue-realtime-pre-pulse')).toBe(true);
+ jest.runAllTimers();
await nextTick();
- expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse');
+ expect(getTitleHeader().classes('issue-realtime-trigger-pulse')).toBe(true);
});
it('updates page title after changing title', async () => {
- vm.titleHtml = 'changed';
- vm.titleText = 'changed';
-
- await nextTick();
- expect(document.querySelector('title').textContent.trim()).toContain('changed');
- });
+ createWrapper();
- describe('inline edit button', () => {
- it('should not show by default', () => {
- expect(vm.$el.querySelector('.btn-edit')).toBeNull();
+ await wrapper.setProps({
+ titleHtml: 'changed',
+ titleText: 'changed',
});
- it('should not show if canUpdate is false', () => {
- vm.showInlineEditButton = true;
- vm.canUpdate = false;
-
- expect(vm.$el.querySelector('.btn-edit')).toBeNull();
- });
-
- it('should show if showInlineEditButton and canUpdate', () => {
- vm.showInlineEditButton = true;
- vm.canUpdate = true;
-
- expect(vm.$el.querySelector('.btn-edit')).toBeDefined();
- });
-
- it('should trigger open.form event when clicked', async () => {
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- vm.showInlineEditButton = true;
- vm.canUpdate = true;
-
- await nextTick();
- vm.$el.querySelector('.btn-edit').click();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('open.form');
- });
+ expect(document.querySelector('title').textContent.trim()).toContain('changed');
});
});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 9f0b6fb1148..ed969a08ac5 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -5,7 +5,7 @@ export const initialRequest = {
title_text: 'this is a title',
description: '<p>this is a description!</p>',
description_text: 'this is a description',
- task_status: '2 of 4 completed',
+ task_completion_status: { completed_count: 2, count: 4 },
updated_at: '2015-05-15T12:31:04.428Z',
updated_by_name: 'Some User',
updated_by_path: '/some_user',
@@ -17,7 +17,20 @@ export const secondRequest = {
title_text: '2',
description: '<p>42</p>',
description_text: '42',
- task_status: '0 of 0 completed',
+ task_completion_status: { completed_count: 0, count: 0 },
+ updated_at: '2016-05-15T12:31:04.428Z',
+ updated_by_name: 'Other User',
+ updated_by_path: '/other_user',
+ lock_version: 2,
+};
+
+export const putRequest = {
+ web_url: window.location.pathname,
+ title: '<p>PUT</p>',
+ title_text: 'PUT',
+ description: '<p>PUT_DESC</p>',
+ description_text: 'PUT_DESC',
+ task_completion_status: { completed_count: 0, count: 0 },
updated_at: '2016-05-15T12:31:04.428Z',
updated_by_name: 'Other User',
updated_by_path: '/other_user',
@@ -28,7 +41,6 @@ export const descriptionProps = {
canUpdate: true,
descriptionHtml: 'test',
descriptionText: 'test',
- taskStatus: '',
updateUrl: TEST_HOST,
};
@@ -47,6 +59,7 @@ export const appProps = {
initialTitleText: '',
initialDescriptionHtml: 'test',
initialDescriptionText: 'test',
+ initialTaskCompletionStatus: { completed_count: 2, count: 4 },
lockVersion: 1,
issueType: 'issue',
markdownPreviewPath: '/',
@@ -67,46 +80,15 @@ export const descriptionHtmlWithList = `
</ul>
`;
-export const descriptionHtmlWithCheckboxes = `
- <ul dir="auto" class="task-list" data-sourcepos"3:1-5:12">
- <li class="task-list-item" data-sourcepos="3:1-3:11">
- <input class="task-list-item-checkbox" type="checkbox"> todo 1
- </li>
- <li class="task-list-item" data-sourcepos="4:1-4:12">
- <input class="task-list-item-checkbox" type="checkbox"> todo 2
- </li>
- <li class="task-list-item" data-sourcepos="5:1-5:12">
- <input class="task-list-item-checkbox" type="checkbox"> todo 3
- </li>
- </ul>
-`;
-
-export const descriptionHtmlWithTask = `
- <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
- <li data-sourcepos="1:1-1:10" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="task">1 (#48)</a>
- </li>
- <li data-sourcepos="2:1-2:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 2
- </li>
- <li data-sourcepos="3:1-3:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 3
- </li>
- </ul>
-`;
-
-export const descriptionHtmlWithIssue = `
- <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
- <li data-sourcepos="1:1-1:10" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="issue">1 (#48)</a>
- </li>
- <li data-sourcepos="2:1-2:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 2
- </li>
- <li data-sourcepos="3:1-3:7" class="task-list-item">
- <input type="checkbox" class="task-list-item-checkbox" disabled> 3
- </li>
- </ul>
-`;
+export const descriptionHtmlWithDetailsTag = {
+ expanded: `
+ <details open="true">
+ <summary>Section 1</summary>
+ <p>Data</p>
+ </details>'`,
+ collapsed: `
+ <details>
+ <summary>Section 1</summary>
+ <p>Data</p>
+ </details>'`,
+};
diff --git a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
index d41031f9eaa..5e6b67aec40 100644
--- a/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
+++ b/spec/frontend/jira_connect/branches/components/new_branch_form_spec.js
@@ -78,10 +78,6 @@ describe('NewBranchForm', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when selecting items from dropdowns', () => {
describe('when no project selected', () => {
beforeEach(() => {
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
index 944854faab3..0a887efee4b 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -49,10 +49,6 @@ describe('ProjectDropdown', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when loading projects', () => {
beforeEach(() => {
createComponent({
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 56e425fa4eb..a3bc8e861b2 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
@@ -54,10 +54,6 @@ describe('SourceBranchDropdown', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when `selectedProject` prop is not specified', () => {
beforeEach(() => {
createComponent();
@@ -151,7 +147,7 @@ describe('SourceBranchDropdown', () => {
});
describe('when selecting a listbox item', () => {
- it('emits `change` event with the selected branch name', async () => {
+ it('emits `change` event with the selected branch name', () => {
const mockBranchName = mockProject.repository.branchNames[1];
findListbox().vm.$emit('select', mockBranchName);
expect(wrapper.emitted('change')[1]).toEqual([mockBranchName]);
@@ -161,7 +157,7 @@ describe('SourceBranchDropdown', () => {
describe('when `selectedBranchName` prop is specified', () => {
const mockBranchName = mockProject.repository.branchNames[2];
- beforeEach(async () => {
+ beforeEach(() => {
wrapper.setProps({
selectedBranchName: mockBranchName,
});
diff --git a/spec/frontend/jira_connect/branches/pages/index_spec.js b/spec/frontend/jira_connect/branches/pages/index_spec.js
index 92976dd28da..4b79d5feab5 100644
--- a/spec/frontend/jira_connect/branches/pages/index_spec.js
+++ b/spec/frontend/jira_connect/branches/pages/index_spec.js
@@ -25,10 +25,6 @@ describe('NewBranchForm', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('page title', () => {
it.each`
initialBranchName | pageTitle
diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js
index e2a14a9102f..36e2c7bbab2 100644
--- a/spec/frontend/jira_connect/subscriptions/api_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/api_spec.js
@@ -1,7 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import {
axiosInstance,
- addSubscription,
removeSubscription,
fetchGroups,
getCurrentUser,
@@ -17,10 +16,8 @@ jest.mock('~/jira_connect/subscriptions/utils', () => ({
describe('JiraConnect API', () => {
let axiosMock;
- let originalGon;
let response;
- const mockAddPath = 'addPath';
const mockRemovePath = 'removePath';
const mockNamespace = 'namespace';
const mockJwt = 'jwt';
@@ -29,39 +26,14 @@ describe('JiraConnect API', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axiosInstance);
- originalGon = window.gon;
window.gon = { api_version: 'v4' };
});
afterEach(() => {
axiosMock.restore();
- window.gon = originalGon;
response = null;
});
- describe('addSubscription', () => {
- const makeRequest = () => addSubscription(mockAddPath, mockNamespace);
-
- it('returns success response', async () => {
- jest.spyOn(axiosInstance, 'post');
- axiosMock
- .onPost(mockAddPath, {
- jwt: mockJwt,
- namespace_path: mockNamespace,
- })
- .replyOnce(HTTP_STATUS_OK, mockResponse);
-
- response = await makeRequest();
-
- expect(getJwt).toHaveBeenCalled();
- expect(axiosInstance.post).toHaveBeenCalledWith(mockAddPath, {
- jwt: mockJwt,
- namespace_path: mockNamespace,
- });
- expect(response.data).toEqual(mockResponse);
- });
- });
-
describe('removeSubscription', () => {
const makeRequest = () => removeSubscription(mockRemovePath);
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
index 9f92ad2adc1..934473c15ba 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_button_spec.js
@@ -11,7 +11,7 @@ describe('AddNamespaceButton', () => {
const createComponent = () => {
wrapper = shallowMount(AddNamespaceButton, {
directives: {
- glModal: createMockDirective(),
+ glModal: createMockDirective('gl-modal'),
},
});
};
@@ -23,10 +23,6 @@ describe('AddNamespaceButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a button', () => {
expect(findButton().exists()).toBe(true);
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
index d80381107f2..dbe8a734bb4 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/add_namespace_modal_spec.js
@@ -17,10 +17,6 @@ describe('AddNamespaceModal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays modal with correct props', () => {
const modal = findModal();
expect(modal.exists()).toBe(true);
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
index 5df54abfc05..c5035a12bd1 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
@@ -1,28 +1,15 @@
import { GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue';
import GroupsListItem from '~/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue';
-import { persistAlert, reloadPage } from '~/jira_connect/subscriptions/utils';
-import {
- I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
- I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
- INTEGRATIONS_DOC_LINK,
-} from '~/jira_connect/subscriptions/constants';
import createStore from '~/jira_connect/subscriptions/store';
import { mockGroup1 } from '../../mock_data';
-jest.mock('~/jira_connect/subscriptions/utils');
-
describe('GroupsListItem', () => {
let wrapper;
let store;
- const mockAddSubscriptionsPath = '/addSubscriptionsPath';
-
const createComponent = ({ mountFn = shallowMount, provide } = {}) => {
store = createStore();
@@ -34,16 +21,11 @@ describe('GroupsListItem', () => {
group: mockGroup1,
},
provide: {
- addSubscriptionsPath: mockAddSubscriptionsPath,
...provide,
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGroupItemName = () => wrapper.findComponent(GroupItemName);
const findLinkButton = () => wrapper.findComponent(GlButton);
const clickLinkButton = () => findLinkButton().trigger('click');
@@ -65,88 +47,24 @@ describe('GroupsListItem', () => {
});
describe('on Link button click', () => {
- describe('when jiraConnectOauth feature flag is disabled', () => {
- let addSubscriptionSpy;
-
- beforeEach(() => {
- createComponent({ mountFn: mount });
-
- addSubscriptionSpy = jest.spyOn(JiraConnectApi, 'addSubscription').mockResolvedValue();
- });
-
- it('sets button to loading and sends request', async () => {
- expect(findLinkButton().props('loading')).toBe(false);
-
- clickLinkButton();
- await nextTick();
-
- expect(findLinkButton().props('loading')).toBe(true);
- await waitForPromises();
-
- expect(addSubscriptionSpy).toHaveBeenCalledWith(
- mockAddSubscriptionsPath,
- mockGroup1.full_path,
- );
- expect(persistAlert).toHaveBeenCalledWith({
- linkUrl: INTEGRATIONS_DOC_LINK,
- message: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_MESSAGE,
- title: I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE,
- variant: 'success',
- });
- });
-
- describe('when request is successful', () => {
- it('reloads the page', async () => {
- clickLinkButton();
+ const mockSubscriptionsPath = '/subscriptions';
- await waitForPromises();
-
- expect(reloadPage).toHaveBeenCalled();
- });
- });
-
- describe('when request has errors', () => {
- const mockErrorMessage = 'error message';
- const mockError = { response: { data: { error: mockErrorMessage } } };
-
- beforeEach(() => {
- addSubscriptionSpy = jest
- .spyOn(JiraConnectApi, 'addSubscription')
- .mockRejectedValue(mockError);
- });
-
- it('emits `error` event', async () => {
- clickLinkButton();
-
- await waitForPromises();
-
- expect(reloadPage).not.toHaveBeenCalled();
- expect(wrapper.emitted('error')[0][0]).toBe(mockErrorMessage);
- });
+ beforeEach(() => {
+ createComponent({
+ mountFn: mount,
+ provide: {
+ subscriptionsPath: mockSubscriptionsPath,
+ },
});
});
- describe('when jiraConnectOauth feature flag is enabled', () => {
- const mockSubscriptionsPath = '/subscriptions';
-
- beforeEach(() => {
- createComponent({
- mountFn: mount,
- provide: {
- subscriptionsPath: mockSubscriptionsPath,
- glFeatures: { jiraConnectOauth: true },
- },
- });
- });
-
- it('dispatches `addSubscription` action', async () => {
- clickLinkButton();
- await nextTick();
+ it('dispatches `addSubscription` action', () => {
+ clickLinkButton();
- expect(store.dispatch).toHaveBeenCalledWith('addSubscription', {
- namespacePath: mockGroup1.full_path,
- subscriptionsPath: mockSubscriptionsPath,
- });
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenCalledWith('addSubscription', {
+ namespacePath: mockGroup1.full_path,
+ subscriptionsPath: mockSubscriptionsPath,
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
index 97038a2a231..9d5bc8dff2a 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
@@ -48,10 +48,6 @@ describe('GroupsList', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAllItems = () => wrapper.findAllComponents(GroupsListItem);
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 369ddda8dbe..26a9d07321c 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -1,6 +1,6 @@
-import { GlLink } from '@gitlab/ui';
+import { GlLink, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
import SignInPage from '~/jira_connect/subscriptions/pages/sign_in/sign_in_page.vue';
@@ -10,228 +10,214 @@ import BrowserSupportAlert from '~/jira_connect/subscriptions/components/browser
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants';
+import { retrieveAlert } from '~/jira_connect/subscriptions/utils';
import { __ } from '~/locale';
import AccessorUtilities from '~/lib/utils/accessor';
import * as api from '~/jira_connect/subscriptions/api';
import { mockSubscription } from '../mock_data';
-jest.mock('~/jira_connect/subscriptions/utils', () => ({
- retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }),
- getGitlabSignInURL: jest.fn(),
-}));
+jest.mock('~/jira_connect/subscriptions/utils');
describe('JiraConnectApp', () => {
let wrapper;
let store;
+ const mockCurrentUser = { name: 'root' };
+
const findAlert = () => wrapper.findByTestId('jira-connect-persisted-alert');
+ const findJiraConnectApp = () => wrapper.findByTestId('jira-connect-app');
const findAlertLink = () => findAlert().findComponent(GlLink);
const findSignInPage = () => wrapper.findComponent(SignInPage);
const findSubscriptionsPage = () => wrapper.findComponent(SubscriptionsPage);
const findUserLink = () => wrapper.findComponent(UserLink);
const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert);
- const createComponent = ({ provide, mountFn = shallowMountExtended, initialState = {} } = {}) => {
+ const createComponent = ({ provide, initialState = {} } = {}) => {
store = createStore({ ...initialState, subscriptions: [mockSubscription] });
jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = mountFn(JiraConnectApp, {
+ wrapper = shallowMountExtended(JiraConnectApp, {
store,
provide,
+ stubs: {
+ GlSprintf,
+ },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
- describe.each`
- scenario | usersPath | shouldRenderSignInPage | shouldRenderSubscriptionsPage
- ${'user is not signed in'} | ${'/users'} | ${true} | ${false}
- ${'user is signed in'} | ${undefined} | ${false} | ${true}
- `('when $scenario', ({ usersPath, shouldRenderSignInPage, shouldRenderSubscriptionsPage }) => {
- beforeEach(() => {
- createComponent({
- provide: {
- usersPath,
- },
- });
- });
-
- it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => {
- expect(findSignInPage().isVisible()).toBe(shouldRenderSignInPage);
- if (shouldRenderSignInPage) {
- expect(findSignInPage().props('hasSubscriptions')).toBe(true);
- }
- });
-
- it(`${
- shouldRenderSubscriptionsPage ? 'renders' : 'does not render'
- } subscriptions page`, () => {
- expect(findSubscriptionsPage().exists()).toBe(shouldRenderSubscriptionsPage);
- if (shouldRenderSubscriptionsPage) {
- expect(findSubscriptionsPage().props('hasSubscriptions')).toBe(true);
- }
- });
+ beforeEach(() => {
+ jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(true);
});
- it('renders UserLink component', () => {
- createComponent({
- provide: {
- usersPath: '/user',
- },
- });
+ it('renders only Jira Connect app', () => {
+ createComponent();
- const userLink = findUserLink();
- expect(userLink.exists()).toBe(true);
- expect(userLink.props()).toEqual({
- hasSubscriptions: true,
- user: null,
- userSignedIn: false,
- });
+ expect(findBrowserSupportAlert().exists()).toBe(false);
+ expect(findJiraConnectApp().exists()).toBe(true);
});
- });
-
- describe('alert', () => {
- it.each`
- message | variant | alertShouldRender
- ${'Test error'} | ${'danger'} | ${true}
- ${'Test notice'} | ${'info'} | ${true}
- ${''} | ${undefined} | ${false}
- ${undefined} | ${undefined} | ${false}
- `(
- 'renders correct alert when message is `$message` and variant is `$variant`',
- async ({ message, alertShouldRender, variant }) => {
- createComponent();
-
- store.commit(SET_ALERT, { message, variant });
- await nextTick();
- const alert = findAlert();
+ it('renders only BrowserSupportAlert when canUseCrypto is false', () => {
+ jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(false);
- expect(alert.exists()).toBe(alertShouldRender);
- if (alertShouldRender) {
- expect(alert.isVisible()).toBe(alertShouldRender);
- expect(alert.html()).toContain(message);
- expect(alert.props('variant')).toBe(variant);
- expect(findAlertLink().exists()).toBe(false);
- }
- },
- );
-
- it('hides alert on @dismiss event', async () => {
createComponent();
- store.commit(SET_ALERT, { message: 'test message' });
- await nextTick();
-
- findAlert().vm.$emit('dismiss');
- await nextTick();
-
- expect(findAlert().exists()).toBe(false);
+ expect(findBrowserSupportAlert().exists()).toBe(true);
+ expect(findJiraConnectApp().exists()).toBe(false);
});
- it('renders link when `linkUrl` is set', async () => {
- createComponent({ provide: { usersPath: '' }, mountFn: mountExtended });
+ describe.each`
+ scenario | currentUser | expectUserLink | expectSignInPage | expectSubscriptionsPage
+ ${'user is not signed in'} | ${undefined} | ${false} | ${true} | ${false}
+ ${'user is signed in'} | ${mockCurrentUser} | ${true} | ${false} | ${true}
+ `(
+ 'when $scenario',
+ ({ currentUser, expectUserLink, expectSignInPage, expectSubscriptionsPage }) => {
+ beforeEach(() => {
+ createComponent({
+ initialState: {
+ currentUser,
+ },
+ });
+ });
- store.commit(SET_ALERT, {
- message: __('test message %{linkStart}test link%{linkEnd}'),
- linkUrl: 'https://gitlab.com',
- });
- await nextTick();
+ it(`${expectUserLink ? 'renders' : 'does not render'} user link`, () => {
+ expect(findUserLink().exists()).toBe(expectUserLink);
+ if (expectUserLink) {
+ expect(findUserLink().props('user')).toBe(mockCurrentUser);
+ }
+ });
- const alertLink = findAlertLink();
+ it(`${expectSignInPage ? 'renders' : 'does not render'} sign in page`, () => {
+ expect(findSignInPage().isVisible()).toBe(expectSignInPage);
+ if (expectSignInPage) {
+ expect(findSignInPage().props('hasSubscriptions')).toBe(true);
+ }
+ });
- expect(alertLink.exists()).toBe(true);
- expect(alertLink.text()).toContain('test link');
- expect(alertLink.attributes('href')).toBe('https://gitlab.com');
- });
+ it(`${expectSubscriptionsPage ? 'renders' : 'does not render'} subscriptions page`, () => {
+ expect(findSubscriptionsPage().exists()).toBe(expectSubscriptionsPage);
+ if (expectSubscriptionsPage) {
+ expect(findSubscriptionsPage().props('hasSubscriptions')).toBe(true);
+ }
+ });
+ },
+ );
- describe('when alert is set in localStoage', () => {
- it('renders alert on mount', () => {
+ describe('when sign in page emits `error` event', () => {
+ beforeEach(() => {
createComponent();
+ findSignInPage().vm.$emit('error');
+ });
+ it('displays alert', () => {
const alert = findAlert();
expect(alert.exists()).toBe(true);
- expect(alert.html()).toContain('error message');
+ expect(alert.text()).toContain(I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE);
+ expect(alert.props('variant')).toBe('danger');
});
});
- });
- describe('when user signed out', () => {
- describe('when sign in page emits `error` event', () => {
- beforeEach(async () => {
+ describe('when sign in page emits `sign-in-oauth` event', () => {
+ const mockSubscriptionsPath = '/mockSubscriptionsPath';
+
+ beforeEach(() => {
+ jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } });
+
createComponent({
+ initialState: {
+ currentUser: mockCurrentUser,
+ },
provide: {
- usersPath: '/mock',
+ subscriptionsPath: mockSubscriptionsPath,
},
});
- findSignInPage().vm.$emit('error');
- await nextTick();
+ findSignInPage().vm.$emit('sign-in-oauth');
});
- it('displays alert', () => {
- const alert = findAlert();
-
- expect(alert.exists()).toBe(true);
- expect(alert.html()).toContain(I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE);
- expect(alert.props('variant')).toBe('danger');
+ it('dispatches `fetchSubscriptions` action', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath);
});
});
- });
- describe.each`
- jiraConnectOauthEnabled | canUseCrypto | shouldShowAlert
- ${false} | ${false} | ${false}
- ${false} | ${true} | ${false}
- ${true} | ${false} | ${true}
- ${true} | ${true} | ${false}
- `(
- 'when `jiraConnectOauth` feature flag is $jiraConnectOauthEnabled and `AccessorUtilities.canUseCrypto` returns $canUseCrypto',
- ({ jiraConnectOauthEnabled, canUseCrypto, shouldShowAlert }) => {
- beforeEach(() => {
- jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(canUseCrypto);
+ describe('alert', () => {
+ const mockAlertData = { message: 'error message' };
- createComponent({ provide: { glFeatures: { jiraConnectOauth: jiraConnectOauthEnabled } } });
- });
+ describe.each`
+ alertData | expectAlert
+ ${undefined} | ${false}
+ ${mockAlertData} | ${true}
+ `('when retrieveAlert returns $alertData', ({ alertData, expectAlert }) => {
+ beforeEach(() => {
+ retrieveAlert.mockReturnValue(alertData);
- it(`does ${shouldShowAlert ? '' : 'not'} render BrowserSupportAlert component`, () => {
- expect(findBrowserSupportAlert().exists()).toBe(shouldShowAlert);
- });
+ createComponent();
+ });
+
+ it(`${expectAlert ? 'renders' : 'does not render'} alert on mount`, () => {
+ const alert = findAlert();
- it(`does ${!shouldShowAlert ? '' : 'not'} render the main Jira Connect app template`, () => {
- expect(wrapper.findByTestId('jira-connect-app').exists()).toBe(!shouldShowAlert);
+ expect(alert.exists()).toBe(expectAlert);
+ if (expectAlert) {
+ expect(alert.text()).toContain(mockAlertData.message);
+ }
+ });
});
- },
- );
- describe('when `jiraConnectOauth` feature flag is enabled', () => {
- const mockSubscriptionsPath = '/mockSubscriptionsPath';
+ it.each`
+ message | variant | alertShouldRender
+ ${'Test error'} | ${'danger'} | ${true}
+ ${'Test notice'} | ${'info'} | ${true}
+ ${''} | ${undefined} | ${false}
+ ${undefined} | ${undefined} | ${false}
+ `(
+ 'renders correct alert when message is `$message` and variant is `$variant`',
+ async ({ message, alertShouldRender, variant }) => {
+ createComponent();
+
+ store.commit(SET_ALERT, { message, variant });
+ await nextTick();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(alertShouldRender);
+ if (alertShouldRender) {
+ expect(alert.isVisible()).toBe(alertShouldRender);
+ expect(alert.text()).toContain(message);
+ expect(alert.props('variant')).toBe(variant);
+ expect(findAlertLink().exists()).toBe(false);
+ }
+ },
+ );
- beforeEach(async () => {
- jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } });
- jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(true);
+ it('hides alert on @dismiss event', async () => {
+ createComponent();
- createComponent({
- initialState: {
- currentUser: { name: 'root' },
- },
- provide: {
- glFeatures: { jiraConnectOauth: true },
- subscriptionsPath: mockSubscriptionsPath,
- },
+ store.commit(SET_ALERT, { message: 'test message' });
+ await nextTick();
+
+ findAlert().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
});
- findSignInPage().vm.$emit('sign-in-oauth');
- await nextTick();
- });
+ it('renders link when `linkUrl` is set', async () => {
+ createComponent();
- describe('when oauth button emits `sign-in-oauth` event', () => {
- it('dispatches `fetchSubscriptions` action', () => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath);
+ store.commit(SET_ALERT, {
+ message: __('test message %{linkStart}test link%{linkEnd}'),
+ linkUrl: 'https://gitlab.com',
+ });
+ await nextTick();
+
+ const alertLink = findAlertLink();
+
+ expect(alertLink.exists()).toBe(true);
+ expect(alertLink.text()).toContain('test link');
+ expect(alertLink.attributes('href')).toBe('https://gitlab.com');
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js b/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js
index aa93a6be3c8..a8aa383d917 100644
--- a/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/browser_support_alert_spec.js
@@ -12,10 +12,6 @@ describe('BrowserSupportAlert', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a non-dismissible alert', () => {
createComponent();
diff --git a/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js b/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js
index b5fe08486b1..e4da10569f3 100644
--- a/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/group_item_name_spec.js
@@ -14,10 +14,6 @@ describe('GroupItemName', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('matches the snapshot', () => {
createComponent();
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js
deleted file mode 100644
index 4ebfaed261e..00000000000
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_legacy_button_spec.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils';
-import SignInLegacyButton from '~/jira_connect/subscriptions/components/sign_in_legacy_button.vue';
-import waitForPromises from 'helpers/wait_for_promises';
-
-const MOCK_USERS_PATH = '/user';
-
-jest.mock('~/jira_connect/subscriptions/utils');
-
-describe('SignInLegacyButton', () => {
- let wrapper;
-
- const createComponent = ({ slots } = {}) => {
- wrapper = shallowMount(SignInLegacyButton, {
- propsData: {
- usersPath: MOCK_USERS_PATH,
- },
- slots,
- });
- };
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays a button', () => {
- createComponent();
-
- expect(findButton().exists()).toBe(true);
- expect(findButton().text()).toBe(SignInLegacyButton.i18n.defaultButtonText);
- });
-
- describe.each`
- expectedHref
- ${MOCK_USERS_PATH}
- ${`${MOCK_USERS_PATH}?return_to=${encodeURIComponent('https://test.jira.com')}`}
- `('when getGitlabSignInURL resolves with `$expectedHref`', ({ expectedHref }) => {
- it(`sets button href to ${expectedHref}`, async () => {
- getGitlabSignInURL.mockResolvedValue(expectedHref);
- createComponent();
-
- await waitForPromises();
-
- expect(findButton().attributes('href')).toBe(expectedHref);
- });
- });
-
- describe('with slot', () => {
- const mockSlotContent = 'custom button content!';
- it('renders slot content in button', () => {
- createComponent({ slots: { default: mockSlotContent } });
- expect(wrapper.text()).toMatchInterpolatedText(mockSlotContent);
- });
- });
-});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
index e20c4b62e77..ee272d55e0e 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -56,10 +56,6 @@ describe('SignInOauthButton', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButton = () => wrapper.findComponent(GlButton);
describe('when `gitlabBasePath` is GitLab.com', () => {
it('displays a button', () => {
@@ -161,6 +157,7 @@ describe('SignInOauthButton', () => {
const mockEvent = {
origin: messageOrigin,
data: {
+ type: 'jiraConnectOauthCallback',
state: messageState,
code: '1234',
},
@@ -186,6 +183,7 @@ describe('SignInOauthButton', () => {
const mockEvent = {
origin: window.origin,
data: {
+ type: 'jiraConnectOauthCallback',
state: mockOauthMetadata.state,
code: '1234',
},
diff --git a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
index 2d7c58fc278..5337575d5ef 100644
--- a/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/subscriptions_list_spec.js
@@ -29,10 +29,6 @@ describe('SubscriptionsList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findUnlinkButton = () => wrapper.findComponent(GlButton);
const clickUnlinkButton = () => findUnlinkButton().trigger('click');
diff --git a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
index e16121243a0..77bc1d2004c 100644
--- a/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/user_link_spec.js
@@ -1,13 +1,7 @@
import { GlSprintf } from '@gitlab/ui';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
-import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-
-jest.mock('~/jira_connect/subscriptions/utils', () => ({
- getGitlabSignInURL: jest.fn().mockImplementation((path) => Promise.resolve(path)),
-}));
describe('UserLink', () => {
let wrapper;
@@ -18,76 +12,17 @@ describe('UserLink', () => {
provide,
stubs: {
GlSprintf,
- SignInOauthButton,
},
});
};
- const findSignInLink = () => wrapper.findByTestId('sign-in-link');
const findGitlabUserLink = () => wrapper.findByTestId('gitlab-user-link');
const findSprintf = () => wrapper.findComponent(GlSprintf);
- const findOauthButton = () => wrapper.findComponent(SignInOauthButton);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe.each`
- userSignedIn | hasSubscriptions | expectGlSprintf | expectGlLink | expectOauthButton | jiraConnectOauthEnabled
- ${true} | ${false} | ${true} | ${false} | ${false} | ${false}
- ${false} | ${true} | ${false} | ${true} | ${false} | ${false}
- ${true} | ${true} | ${true} | ${false} | ${false} | ${false}
- ${false} | ${false} | ${false} | ${false} | ${false} | ${false}
- ${false} | ${true} | ${false} | ${false} | ${true} | ${true}
- `(
- 'when `userSignedIn` is $userSignedIn, `hasSubscriptions` is $hasSubscriptions, `jiraConnectOauthEnabled` is $jiraConnectOauthEnabled',
- ({
- userSignedIn,
- hasSubscriptions,
- expectGlSprintf,
- expectGlLink,
- expectOauthButton,
- jiraConnectOauthEnabled,
- }) => {
- it('renders template correctly', () => {
- createComponent(
- {
- userSignedIn,
- hasSubscriptions,
- },
- {
- provide: {
- glFeatures: {
- jiraConnectOauth: jiraConnectOauthEnabled,
- },
- oauthMetadata: {},
- },
- },
- );
- expect(findSprintf().exists()).toBe(expectGlSprintf);
- expect(findSignInLink().exists()).toBe(expectGlLink);
- expect(findOauthButton().exists()).toBe(expectOauthButton);
- });
- },
- );
+ it('renders template correctly', () => {
+ createComponent();
- describe('sign in link', () => {
- it('renders with correct href', async () => {
- const mockUsersPath = '/user';
- createComponent(
- {
- userSignedIn: false,
- hasSubscriptions: true,
- },
- { provide: { usersPath: mockUsersPath } },
- );
-
- await waitForPromises();
-
- expect(findSignInLink().exists()).toBe(true);
- expect(findSignInLink().attributes('href')).toBe(mockUsersPath);
- });
+ expect(findSprintf().exists()).toBe(true);
});
describe('gitlab user link', () => {
@@ -102,14 +37,7 @@ describe('UserLink', () => {
beforeEach(() => {
window.gon = { current_username, relative_root_url: '' };
- createComponent(
- {
- userSignedIn: true,
- hasSubscriptions: true,
- user,
- },
- { provide: { gitlabUserPath } },
- );
+ createComponent({ user }, { provide: { gitlabUserPath } });
});
it(`sets href to ${expectedUserLink}`, () => {
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js
index b9a8451f3b3..4cfd925db34 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import SignInGitlabCom from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue';
-import SignInLegacyButton from '~/jira_connect/subscriptions/components/sign_in_legacy_button.vue';
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store';
@@ -9,100 +8,69 @@ import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT } from '~/jira_connect/subscriptions/c
jest.mock('~/jira_connect/subscriptions/utils');
-const mockUsersPath = '/test';
const defaultProvide = {
oauthMetadata: {},
- usersPath: mockUsersPath,
};
describe('SignInGitlabCom', () => {
let wrapper;
let store;
- const findSignInLegacyButton = () => wrapper.findComponent(SignInLegacyButton);
const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton);
const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
- const createComponent = ({ props, jiraConnectOauthEnabled } = {}) => {
+ const createComponent = ({ props } = {}) => {
store = createStore();
wrapper = shallowMount(SignInGitlabCom, {
store,
provide: {
...defaultProvide,
- glFeatures: {
- jiraConnectOauth: jiraConnectOauthEnabled,
- },
},
propsData: props,
stubs: {
- SignInLegacyButton,
SignInOauthButton,
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe.each`
scenario | hasSubscriptions | signInButtonText
${'with subscriptions'} | ${true} | ${SignInGitlabCom.i18n.signInButtonTextWithSubscriptions}
${'without subscriptions'} | ${false} | ${I18N_DEFAULT_SIGN_IN_BUTTON_TEXT}
`('$scenario', ({ hasSubscriptions, signInButtonText }) => {
- describe('when `jiraConnectOauthEnabled` feature flag is disabled', () => {
- beforeEach(() => {
- createComponent({
- jiraConnectOauthEnabled: false,
- props: {
- hasSubscriptions,
- },
- });
- });
-
- it('renders legacy sign in button', () => {
- const button = findSignInLegacyButton();
- expect(button.props('usersPath')).toBe(mockUsersPath);
- expect(button.text()).toMatchInterpolatedText(signInButtonText);
+ beforeEach(() => {
+ createComponent({
+ props: {
+ hasSubscriptions,
+ },
});
});
- describe('when `jiraConnectOauthEnabled` feature flag is enabled', () => {
- beforeEach(() => {
- createComponent({
- jiraConnectOauthEnabled: true,
- props: {
- hasSubscriptions,
- },
- });
+ describe('oauth sign in button', () => {
+ it('renders oauth sign in button', () => {
+ const button = findSignInOauthButton();
+ expect(button.text()).toMatchInterpolatedText(signInButtonText);
});
- describe('oauth sign in button', () => {
- it('renders oauth sign in button', () => {
+ describe('when button emits `sign-in` event', () => {
+ it('emits `sign-in-oauth` event', () => {
const button = findSignInOauthButton();
- expect(button.text()).toMatchInterpolatedText(signInButtonText);
- });
-
- describe('when button emits `sign-in` event', () => {
- it('emits `sign-in-oauth` event', () => {
- const button = findSignInOauthButton();
- const mockUser = { name: 'test' };
- button.vm.$emit('sign-in', mockUser);
+ const mockUser = { name: 'test' };
+ button.vm.$emit('sign-in', mockUser);
- expect(wrapper.emitted('sign-in-oauth')[0]).toEqual([mockUser]);
- });
+ expect(wrapper.emitted('sign-in-oauth')[0]).toEqual([mockUser]);
});
+ });
- describe('when button emits `error` event', () => {
- it('emits `error` event', () => {
- const button = findSignInOauthButton();
- button.vm.$emit('error');
+ describe('when button emits `error` event', () => {
+ it('emits `error` event', () => {
+ const button = findSignInOauthButton();
+ button.vm.$emit('error');
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
+ expect(wrapper.emitted('error')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
index e98c6ff1054..93663319e6d 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index_spec.js
@@ -1,12 +1,11 @@
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue';
import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue';
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
-import { updateInstallation } from '~/jira_connect/subscriptions/api';
+import { updateInstallation, setApiBaseURL } from '~/jira_connect/subscriptions/api';
import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils';
import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants';
@@ -23,7 +22,6 @@ describe('SignInGitlabMultiversion', () => {
const mockBasePath = 'gitlab.mycompany.com';
- const findSetupInstructions = () => wrapper.findComponent(SetupInstructions);
const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton);
const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm);
const findSubtitle = () => wrapper.findByTestId('subtitle');
@@ -32,10 +30,6 @@ describe('SignInGitlabMultiversion', () => {
wrapper = shallowMountExtended(SignInGitlabMultiversion);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when version is not selected', () => {
describe('VersionSelectForm', () => {
it('renders version select form', () => {
@@ -72,23 +66,12 @@ describe('SignInGitlabMultiversion', () => {
expect(findSubtitle().text()).toBe(SignInGitlabMultiversion.i18n.signInSubtitle);
});
- it('renders setup instructions', () => {
- expect(findSetupInstructions().exists()).toBe(true);
+ it('renders sign in button', () => {
+ expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath);
});
- describe('when SetupInstructions emits `next` event', () => {
- beforeEach(async () => {
- findSetupInstructions().vm.$emit('next');
- await nextTick();
- });
-
- it('renders sign in button', () => {
- expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath);
- });
-
- it('hides setup instructions', () => {
- expect(findSetupInstructions().exists()).toBe(false);
- });
+ it('calls setApiBaseURL with correct params', () => {
+ expect(setApiBaseURL).toHaveBeenCalledWith(mockBasePath);
});
});
@@ -98,14 +81,14 @@ describe('SignInGitlabMultiversion', () => {
createComponent();
});
- it('does not render setup instructions', () => {
- expect(findSetupInstructions().exists()).toBe(false);
- });
-
it('renders sign in button', () => {
expect(findSignInOauthButton().props('gitlabBasePath')).toBe(GITLAB_COM_BASE_PATH);
});
+ it('does not call setApiBaseURL', () => {
+ expect(setApiBaseURL).not.toHaveBeenCalled();
+ });
+
describe('when button emits `sign-in` event', () => {
it('emits `sign-in-oauth` event', () => {
const button = findSignInOauthButton();
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
index 5496cf008c5..40ea6058c70 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions_spec.js
@@ -7,8 +7,9 @@ import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_i
describe('SetupInstructions', () => {
let wrapper;
- const findGlButton = () => wrapper.findComponent(GlButton);
const findGlLink = () => wrapper.findComponent(GlLink);
+ const findBackButton = () => wrapper.findAllComponents(GlButton).at(0);
+ const findNextButton = () => wrapper.findAllComponents(GlButton).at(1);
const createComponent = () => {
wrapper = shallowMount(SetupInstructions);
@@ -23,12 +24,23 @@ describe('SetupInstructions', () => {
expect(findGlLink().attributes('href')).toBe(OAUTH_SELF_MANAGED_DOC_LINK);
});
- describe('when button is clicked', () => {
+ describe('when "Next" button is clicked', () => {
it('emits "next" event', () => {
expect(wrapper.emitted('next')).toBeUndefined();
- findGlButton().vm.$emit('click');
+ findNextButton().vm.$emit('click');
expect(wrapper.emitted('next')).toHaveLength(1);
+ expect(wrapper.emitted('back')).toBeUndefined();
+ });
+ });
+
+ describe('when "Back" button is clicked', () => {
+ it('emits "back" event', () => {
+ expect(wrapper.emitted('back')).toBeUndefined();
+ findBackButton().vm.$emit('click');
+
+ expect(wrapper.emitted('back')).toHaveLength(1);
+ expect(wrapper.emitted('next')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js
index 29e7fe7a5b2..2a08547b048 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form_spec.js
@@ -1,8 +1,9 @@
import { GlFormInput, GlFormRadioGroup, GlForm } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue';
+import SelfManagedAlert from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/self_managed_alert.vue';
+import SetupInstructions from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue';
describe('VersionSelectForm', () => {
let wrapper;
@@ -10,30 +11,53 @@ describe('VersionSelectForm', () => {
const findFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findForm = () => wrapper.findComponent(GlForm);
const findInput = () => wrapper.findComponent(GlFormInput);
+ const findSelfManagedAlert = () => wrapper.findComponent(SelfManagedAlert);
+ const findSetupInstructions = () => wrapper.findComponent(SetupInstructions);
+ const findBackButton = () => wrapper.findByTestId('back-button');
+ const findSubmitButton = () => wrapper.findByTestId('submit-button');
const submitForm = () => findForm().vm.$emit('submit', new Event('submit'));
+ const expectSelfManagedFlowAtStep = (step) => {
+ // step 0 is for SaaS which doesn't have any of the self-managed elements
+ const expectSelfManagedAlert = step === 1;
+ const expectSetupInstructions = step === 2;
+ const expectSelfManagedInput = step === 3;
+
+ it(`${expectSelfManagedAlert ? 'renders' : 'does not render'} self-managed alert`, () => {
+ expect(findSelfManagedAlert().exists()).toBe(expectSelfManagedAlert);
+ });
+
+ it(`${expectSetupInstructions ? 'renders' : 'does not render'} setup instructions`, () => {
+ expect(findSetupInstructions().exists()).toBe(expectSetupInstructions);
+ });
+
+ it(`${
+ expectSelfManagedInput ? 'renders' : 'does not render'
+ } self-managed instance URL input`, () => {
+ expect(findInput().exists()).toBe(expectSelfManagedInput);
+ });
+ };
+
const createComponent = () => {
wrapper = shallowMountExtended(VersionSelectForm);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('default state', () => {
+ describe('when "SaaS" radio option is selected (default state)', () => {
beforeEach(() => {
createComponent();
});
- it('selects saas radio option by default', () => {
+ it('selects "saas" radio option by default', () => {
expect(findFormRadioGroup().vm.$attrs.checked).toBe(VersionSelectForm.radioOptions.saas);
});
- it('does not render instance input', () => {
- expect(findInput().exists()).toBe(false);
+ it('renders submit button as "Save"', () => {
+ expect(findSubmitButton().text()).toBe(VersionSelectForm.i18n.buttonSave);
});
+ expectSelfManagedFlowAtStep(0);
+
describe('when form is submitted', () => {
it('emits "submit" event with gitlab.com as the payload', () => {
submitForm();
@@ -43,26 +67,61 @@ describe('VersionSelectForm', () => {
});
});
- describe('when "self-managed" radio option is selected', () => {
- beforeEach(async () => {
+ describe('when "self-managed" radio option is selected (step 1 of 3)', () => {
+ beforeEach(() => {
createComponent();
findFormRadioGroup().vm.$emit('input', VersionSelectForm.radioOptions.selfManaged);
- await nextTick();
});
- it('reveals the self-managed input field', () => {
- expect(findInput().exists()).toBe(true);
+ it('renders submit button as "Next"', () => {
+ expect(findSubmitButton().text()).toBe(VersionSelectForm.i18n.buttonNext);
});
- describe('when form is submitted', () => {
- it('emits "submit" event with the input field value as the payload', () => {
- const mockInstanceUrl = 'https://gitlab.example.com';
+ expectSelfManagedFlowAtStep(1);
- findInput().vm.$emit('input', mockInstanceUrl);
+ describe('when user clicks "Next" button (next to step 2 of 3)', () => {
+ beforeEach(() => {
submitForm();
+ });
+
+ expectSelfManagedFlowAtStep(2);
+
+ describe('when SetupInstructions emits `next` event (next to step 3 of 3)', () => {
+ beforeEach(() => {
+ findSetupInstructions().vm.$emit('next');
+ });
+
+ expectSelfManagedFlowAtStep(3);
+
+ describe('when form is submitted', () => {
+ it('emits "submit" event with the input field value as the payload', () => {
+ const mockInstanceUrl = 'https://gitlab.example.com';
+
+ findInput().vm.$emit('input', mockInstanceUrl);
+ submitForm();
+
+ expect(wrapper.emitted('submit')[0][0]).toBe(mockInstanceUrl);
+ });
+ });
+
+ describe('when back button is clicked', () => {
+ beforeEach(() => {
+ findBackButton().vm.$emit('click', {
+ preventDefault: jest.fn(), // preventDefault is needed to prevent form submission
+ });
+ });
+
+ expectSelfManagedFlowAtStep(1);
+ });
+ });
+
+ describe('when SetupInstructions emits `back` event (back to step 1 of 3)', () => {
+ beforeEach(() => {
+ findSetupInstructions().vm.$emit('back');
+ });
- expect(wrapper.emitted('submit')[0][0]).toBe(mockInstanceUrl);
+ expectSelfManagedFlowAtStep(1);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
index b27eba6b040..36e78ff309e 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
@@ -12,20 +12,11 @@ describe('SignInPage', () => {
const findSignInGitlabCom = () => wrapper.findComponent(SignInGitlabCom);
const findSignInGitabMultiversion = () => wrapper.findComponent(SignInGitlabMultiversion);
- const createComponent = ({
- props = {},
- jiraConnectOauthEnabled,
- publicKeyStorageEnabled,
- } = {}) => {
+ const createComponent = ({ props = {}, publicKeyStorageEnabled } = {}) => {
store = createStore();
wrapper = shallowMount(SignInPage, {
store,
- provide: {
- glFeatures: {
- jiraConnectOauth: jiraConnectOauthEnabled,
- },
- },
propsData: {
hasSubscriptions: false,
publicKeyStorageEnabled,
@@ -34,25 +25,14 @@ describe('SignInPage', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
- jiraConnectOauthEnabled | publicKeyStorageEnabled | shouldRenderDotCom | shouldRenderMultiversion
- ${false} | ${true} | ${true} | ${false}
- ${false} | ${false} | ${true} | ${false}
- ${true} | ${true} | ${false} | ${true}
- ${true} | ${false} | ${true} | ${false}
+ publicKeyStorageEnabled | shouldRenderDotCom | shouldRenderMultiversion
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${false}
`(
- 'renders correct component when jiraConnectOauth is $jiraConnectOauthEnabled',
- ({
- jiraConnectOauthEnabled,
- publicKeyStorageEnabled,
- shouldRenderDotCom,
- shouldRenderMultiversion,
- }) => {
- createComponent({ jiraConnectOauthEnabled, publicKeyStorageEnabled });
+ 'renders correct component when publicKeyStorageEnabled is $publicKeyStorageEnabled',
+ ({ publicKeyStorageEnabled, shouldRenderDotCom, shouldRenderMultiversion }) => {
+ createComponent({ publicKeyStorageEnabled });
expect(findSignInGitlabCom().exists()).toBe(shouldRenderDotCom);
expect(findSignInGitabMultiversion().exists()).toBe(shouldRenderMultiversion);
diff --git a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js
index 4956af76ead..d262f4b2735 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/subscriptions_page_spec.js
@@ -26,10 +26,6 @@ describe('SubscriptionsPage', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
describe.each`
scenario | subscriptionsLoading | hasSubscriptions | expectSubscriptionsList | expectEmptyState
diff --git a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js
index 5e3c30269b5..e53c3e766d2 100644
--- a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js
@@ -117,7 +117,7 @@ describe('JiraConnect actions', () => {
});
describe('when API request succeeds', () => {
- it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => {
+ it('commits the SET_ALERT mutation', async () => {
jest.spyOn(api, 'addJiraConnectSubscription').mockResolvedValue({ success: true });
await testAction(
@@ -125,7 +125,6 @@ describe('JiraConnect actions', () => {
{ namespacePath: mockNamespace, subscriptionsPath: mockSubscriptionsPath },
mockedState,
[
- { type: types.ADD_SUBSCRIPTION_LOADING, payload: true },
{
type: types.SET_ALERT,
payload: {
@@ -135,7 +134,6 @@ describe('JiraConnect actions', () => {
variant: 'success',
},
},
- { type: types.ADD_SUBSCRIPTION_LOADING, payload: false },
],
[{ type: 'fetchSubscriptions', payload: mockSubscriptionsPath }],
);
@@ -148,20 +146,18 @@ describe('JiraConnect actions', () => {
});
describe('when API request fails', () => {
- it('commits the SET_CURRENT_USER_ERROR mutation', async () => {
+ it('does not commit the SET_ALERT mutation', () => {
jest.spyOn(api, 'addJiraConnectSubscription').mockRejectedValue();
- await testAction(
+ // We need the empty catch(), since we are testing rejecting the promise,
+ // which would otherwise cause the test to fail.
+ testAction(
addSubscription,
- mockNamespace,
+ { namespacePath: mockNamespace, subscriptionsPath: mockSubscriptionsPath },
mockedState,
- [
- { type: types.ADD_SUBSCRIPTION_LOADING, payload: true },
- { type: types.ADD_SUBSCRIPTION_ERROR },
- { type: types.ADD_SUBSCRIPTION_LOADING, payload: false },
- ],
[],
- );
+ [],
+ ).catch(() => {});
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js b/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js
index aeb136a76b9..e41bcec19b7 100644
--- a/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/store/mutations_spec.js
@@ -51,22 +51,6 @@ describe('JiraConnect store mutations', () => {
});
});
- describe('ADD_SUBSCRIPTION_LOADING', () => {
- it('sets addSubscriptionLoading', () => {
- mutations.ADD_SUBSCRIPTION_LOADING(localState, true);
-
- expect(localState.addSubscriptionLoading).toBe(true);
- });
- });
-
- describe('ADD_SUBSCRIPTION_ERROR', () => {
- it('sets addSubscriptionError', () => {
- mutations.ADD_SUBSCRIPTION_ERROR(localState, true);
-
- expect(localState.addSubscriptionError).toBe(true);
- });
- });
-
describe('SET_CURRENT_USER', () => {
it('sets currentUser', () => {
const mockUser = { name: 'root' };
diff --git a/spec/frontend/jira_connect/subscriptions/utils_spec.js b/spec/frontend/jira_connect/subscriptions/utils_spec.js
index 762d9eb3443..d1588dbad2c 100644
--- a/spec/frontend/jira_connect/subscriptions/utils_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/utils_spec.js
@@ -5,10 +5,8 @@ import {
persistAlert,
retrieveAlert,
getJwt,
- getLocation,
reloadPage,
sizeToParent,
- getGitlabSignInURL,
} from '~/jira_connect/subscriptions/utils';
describe('JiraConnect utils', () => {
@@ -69,29 +67,6 @@ describe('JiraConnect utils', () => {
});
});
- describe('getLocation', () => {
- const mockLocation = 'test/location';
- const getLocationSpy = jest.fn((callback) => callback(mockLocation));
-
- it('resolves to the function call when AP.getLocation is a function', async () => {
- global.AP = {
- getLocation: getLocationSpy,
- };
-
- const location = await getLocation();
-
- expect(getLocationSpy).toHaveBeenCalled();
- expect(location).toBe(mockLocation);
- });
-
- it('resolves to undefined when AP.getLocation is not a function', async () => {
- const location = await getLocation();
-
- expect(getLocationSpy).not.toHaveBeenCalled();
- expect(location).toBeUndefined();
- });
- });
-
describe('reloadPage', () => {
const reloadSpy = jest.fn();
@@ -138,25 +113,4 @@ describe('JiraConnect utils', () => {
});
});
});
-
- describe('getGitlabSignInURL', () => {
- const mockSignInURL = 'https://gitlab.com/sign_in';
-
- it.each`
- returnTo | expectResult
- ${undefined} | ${mockSignInURL}
- ${''} | ${mockSignInURL}
- ${'/test/location'} | ${`${mockSignInURL}?return_to=${encodeURIComponent('/test/location')}`}
- `(
- 'returns `$expectResult` when `AP.getLocation` resolves to `$returnTo`',
- async ({ returnTo, expectResult }) => {
- global.AP = {
- getLocation: jest.fn().mockImplementation((cb) => cb(returnTo)),
- };
-
- const url = await getGitlabSignInURL(mockSignInURL);
- expect(url).toBe(expectResult);
- },
- );
- });
});
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 40e627262db..abd849b387e 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
@@ -2,7 +2,7 @@
exports[`JiraImportForm table body shows correct information in each cell 1`] = `
<table
- aria-busy="false"
+ aria-busy=""
aria-colcount="3"
class="table b-table gl-table b-table-fixed"
role="table"
@@ -76,7 +76,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#arrow-right"
+ href="file-mock#arrow-right"
/>
</svg>
</td>
@@ -92,7 +92,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<button
aria-expanded="false"
- aria-haspopup="true"
+ aria-haspopup="menu"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
type="button"
>
@@ -113,7 +113,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#chevron-down"
+ href="file-mock#chevron-down"
/>
</svg>
</button>
@@ -144,7 +144,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#search"
+ href="file-mock#search"
/>
</svg>
@@ -201,7 +201,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#arrow-right"
+ href="file-mock#arrow-right"
/>
</svg>
</td>
@@ -217,7 +217,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<!---->
<button
aria-expanded="false"
- aria-haspopup="true"
+ aria-haspopup="menu"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
type="button"
>
@@ -238,7 +238,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#chevron-down"
+ href="file-mock#chevron-down"
/>
</svg>
</button>
@@ -269,7 +269,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
role="img"
>
<use
- href="#search"
+ href="file-mock#search"
/>
</svg>
diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js
index 022a0f81aaa..dc1b75f5d9e 100644
--- a/spec/frontend/jira_import/components/jira_import_app_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_app_spec.js
@@ -67,11 +67,6 @@ describe('JiraImportApp', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when Jira integration is not configured', () => {
beforeEach(() => {
wrapper = mountComponent({ isJiraConfigured: false });
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index d43a9f8a145..7fd6398aaa4 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -106,7 +106,6 @@ describe('JiraImportForm', () => {
axiosMock.restore();
mutateSpy.mockRestore();
querySpy.mockRestore();
- wrapper.destroy();
});
describe('select dropdown project selection', () => {
@@ -305,7 +304,7 @@ describe('JiraImportForm', () => {
expect(getContinueButton().text()).toBe('Continue');
});
- it('is in loading state when the form is submitting', async () => {
+ it('is in loading state when the form is submitting', () => {
wrapper = mountComponent({ isSubmitting: true });
expect(getContinueButton().props('loading')).toBe(true);
@@ -417,7 +416,7 @@ describe('JiraImportForm', () => {
wrapper = mountComponent({ hasMoreUsers: true });
});
- it('calls the GraphQL user mapping mutation', async () => {
+ it('calls the GraphQL user mapping mutation', () => {
const mutationArguments = {
mutation: getJiraUserMappingMutation,
variables: {
diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js
index 42356763492..c0d415a2130 100644
--- a/spec/frontend/jira_import/components/jira_import_progress_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js
@@ -25,11 +25,6 @@ describe('JiraImportProgress', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('empty state', () => {
beforeEach(() => {
wrapper = mountComponent();
diff --git a/spec/frontend/jira_import/components/jira_import_setup_spec.js b/spec/frontend/jira_import/components/jira_import_setup_spec.js
index 0085a2b5572..5331467d669 100644
--- a/spec/frontend/jira_import/components/jira_import_setup_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js
@@ -17,11 +17,6 @@ describe('JiraImportSetup', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('contains illustration', () => {
expect(getGlEmptyStateProp('svgPath')).toBe(illustration);
});
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
index 14613775791..5ecddc7efd6 100644
--- a/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/jobs_filtered_search_spec.js
@@ -27,10 +27,6 @@ describe('Jobs filtered search', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays filtered search', () => {
createComponent();
diff --git a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
index fbe5f6a2e11..6755b854f01 100644
--- a/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
+++ b/spec/frontend/jobs/components/filtered_search/tokens/job_status_token_spec.js
@@ -45,10 +45,6 @@ describe('Job Status Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
diff --git a/spec/frontend/jobs/components/job/artifacts_block_spec.js b/spec/frontend/jobs/components/job/artifacts_block_spec.js
index c75deb64d84..ea5d727bd08 100644
--- a/spec/frontend/jobs/components/job/artifacts_block_spec.js
+++ b/spec/frontend/jobs/components/job/artifacts_block_spec.js
@@ -55,11 +55,6 @@ describe('Artifacts block', () => {
locked: true,
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with expired artifacts that are not locked', () => {
beforeEach(() => {
wrapper = createWrapper({
diff --git a/spec/frontend/jobs/components/job/commit_block_spec.js b/spec/frontend/jobs/components/job/commit_block_spec.js
index 4fcc754c82c..1c28b5079d7 100644
--- a/spec/frontend/jobs/components/job/commit_block_spec.js
+++ b/spec/frontend/jobs/components/job/commit_block_spec.js
@@ -32,10 +32,6 @@ describe('Commit block', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without merge request', () => {
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/jobs/components/job/empty_state_spec.js b/spec/frontend/jobs/components/job/empty_state_spec.js
index c6ab259bf46..970c2591795 100644
--- a/spec/frontend/jobs/components/job/empty_state_spec.js
+++ b/spec/frontend/jobs/components/job/empty_state_spec.js
@@ -35,13 +35,6 @@ describe('Empty State', () => {
const findAction = () => wrapper.findByTestId('job-empty-state-action');
const findManualVarsForm = () => wrapper.findComponent(ManualVariablesForm);
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
describe('renders image and title', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/jobs/components/job/environments_block_spec.js b/spec/frontend/jobs/components/job/environments_block_spec.js
index 134533e2af8..ab36f79ea5e 100644
--- a/spec/frontend/jobs/components/job/environments_block_spec.js
+++ b/spec/frontend/jobs/components/job/environments_block_spec.js
@@ -51,11 +51,6 @@ describe('Environments block', () => {
const findEnvironmentLink = () => wrapper.find('[data-testid="job-environment-link"]');
const findClusterLink = () => wrapper.find('[data-testid="job-cluster-link"]');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with last deployment', () => {
it('renders info for most recent deployment', () => {
createComponent({
diff --git a/spec/frontend/jobs/components/job/erased_block_spec.js b/spec/frontend/jobs/components/job/erased_block_spec.js
index c6aba01fa53..aeab676fc7e 100644
--- a/spec/frontend/jobs/components/job/erased_block_spec.js
+++ b/spec/frontend/jobs/components/job/erased_block_spec.js
@@ -18,10 +18,6 @@ describe('Erased block', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with job erased by user', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index cefedcd82fb..394fc8ad43c 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -83,8 +83,9 @@ describe('Job App', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
});
describe('while loading', () => {
diff --git a/spec/frontend/jobs/components/job/job_container_item_spec.js b/spec/frontend/jobs/components/job/job_container_item_spec.js
index 05c38dd74b7..8121aa1172f 100644
--- a/spec/frontend/jobs/components/job/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job/job_container_item_spec.js
@@ -24,11 +24,6 @@ describe('JobContainerItem', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when a job is not active and not retried', () => {
beforeEach(() => {
createComponent(job);
diff --git a/spec/frontend/jobs/components/job/job_log_controllers_spec.js b/spec/frontend/jobs/components/job/job_log_controllers_spec.js
index 5e9a73b4387..218096b9745 100644
--- a/spec/frontend/jobs/components/job/job_log_controllers_spec.js
+++ b/spec/frontend/jobs/components/job/job_log_controllers_spec.js
@@ -16,9 +16,6 @@ describe('Job log controllers', () => {
});
afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
commonUtils.backOff.mockReset();
});
@@ -133,7 +130,7 @@ describe('Job log controllers', () => {
});
it('renders disabled scroll top button', () => {
- expect(findScrollTop().attributes('disabled')).toBe('disabled');
+ expect(findScrollTop().attributes('disabled')).toBeDefined();
});
it('does not emit scrollJobLogTop event on click', async () => {
@@ -285,6 +282,18 @@ describe('Job log controllers', () => {
expect(findScrollFailure().props('disabled')).toBe(false);
});
});
+
+ describe('on error', () => {
+ beforeEach(() => {
+ jest.spyOn(commonUtils, 'backOff').mockRejectedValueOnce();
+
+ createWrapper({}, { jobLogJumpToFailures: true });
+ });
+
+ it('stays disabled', () => {
+ expect(findScrollFailure().props('disabled')).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
index d60043f33f7..a44a13259aa 100644
--- a/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
+++ b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js
@@ -27,13 +27,6 @@ describe('Job Retry Forward Deployment Modal', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
beforeEach(createWrapper);
describe('Modal configuration', () => {
@@ -64,13 +57,11 @@ describe('Job Retry Forward Deployment Modal', () => {
beforeEach(createWrapper);
it('should correctly configure the primary action', () => {
- expect(findModal().props('actionPrimary').attributes).toMatchObject([
- {
- 'data-method': 'post',
- href: job.retry_path,
- variant: 'danger',
- },
- ]);
+ expect(findModal().props('actionPrimary').attributes).toMatchObject({
+ 'data-method': 'post',
+ href: job.retry_path,
+ variant: 'danger',
+ });
});
});
});
diff --git a/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js b/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js
index 4da17ed8366..c1028f3929d 100644
--- a/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js
+++ b/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js
@@ -26,13 +26,6 @@ describe('Job Sidebar Details Container', () => {
);
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
describe('when no details are available', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
index 91821a38a78..8a63bfdc3d6 100644
--- a/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
+++ b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js
@@ -24,12 +24,6 @@ describe('Job Sidebar Retry Button', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
beforeEach(createWrapper);
it.each([
diff --git a/spec/frontend/jobs/components/job/jobs_container_spec.js b/spec/frontend/jobs/components/job/jobs_container_spec.js
index 2fde4d3020b..05660880751 100644
--- a/spec/frontend/jobs/components/job/jobs_container_spec.js
+++ b/spec/frontend/jobs/components/job/jobs_container_spec.js
@@ -68,10 +68,6 @@ describe('Jobs List block', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a list of jobs', () => {
createComponent({
jobs: [job, retried, active],
diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
index a5b3b0e3b47..a48155d93ac 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -2,24 +2,28 @@ import { GlSprintf, GlLink } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
+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 { convertToGraphQLId } from '~/graphql_shared/utils';
import waitForPromises from 'helpers/wait_for_promises';
-import { redirectTo } from '~/lib/utils/url_utility';
+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 retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql';
+import playJobMutation from '~/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql';
import {
mockFullPath,
mockId,
mockJobResponse,
mockJobWithVariablesResponse,
- mockJobMutationData,
+ mockJobPlayMutationData,
+ mockJobRetryMutationData,
} from './mock_data';
const localVue = createLocalVue();
+jest.mock('~/alert');
localVue.use(VueApollo);
jest.mock('~/lib/utils/url_utility', () => ({
@@ -39,9 +43,9 @@ describe('Manual Variables Form', () => {
const createComponent = ({ options = {}, props = {} } = {}) => {
wrapper = mountExtended(ManualVariablesForm, {
propsData: {
- ...props,
jobId: mockId,
- isRetryable: true,
+ isRetryable: false,
+ ...props,
},
provide: {
...defaultProvide,
@@ -50,7 +54,7 @@ describe('Manual Variables Form', () => {
});
};
- const createComponentWithApollo = async ({ props = {} } = {}) => {
+ const createComponentWithApollo = ({ props = {} } = {}) => {
const requestHandlers = [[getJobQuery, getJobQueryResponse]];
mockApollo = createMockApollo(requestHandlers);
@@ -71,7 +75,7 @@ describe('Manual Variables Form', () => {
const findHelpText = () => wrapper.findComponent(GlSprintf);
const findHelpLink = () => wrapper.findComponent(GlLink);
const findCancelBtn = () => wrapper.findByTestId('cancel-btn');
- const findRerunBtn = () => wrapper.findByTestId('run-manual-job-btn');
+ const findRunBtn = () => wrapper.findByTestId('run-manual-job-btn');
const findDeleteVarBtn = () => wrapper.findByTestId('delete-variable-btn');
const findAllDeleteVarBtns = () => wrapper.findAllByTestId('delete-variable-btn');
const findDeleteVarBtnPlaceholder = () => wrapper.findByTestId('delete-variable-btn-placeholder');
@@ -97,7 +101,7 @@ describe('Manual Variables Form', () => {
});
afterEach(() => {
- wrapper.destroy();
+ createAlert.mockClear();
});
describe('when page renders', () => {
@@ -112,10 +116,30 @@ describe('Manual Variables Form', () => {
'/help/ci/variables/index#add-a-cicd-variable-to-a-project',
);
});
+ });
- it('renders buttons', () => {
- expect(findCancelBtn().exists()).toBe(true);
- expect(findRerunBtn().exists()).toBe(true);
+ describe('when query is unsuccessful', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockRejectedValue({});
+ await createComponentWithApollo();
+ });
+
+ it('shows an alert with error', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText,
+ });
+ });
+ });
+
+ describe('when job has not been retried', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobWithVariablesResponse);
+ await createComponentWithApollo();
+ });
+
+ it('does not render the cancel button', () => {
+ expect(findCancelBtn().exists()).toBe(false);
+ expect(findRunBtn().exists()).toBe(true);
});
});
@@ -135,10 +159,10 @@ describe('Manual Variables Form', () => {
});
});
- describe('when mutation fires', () => {
+ describe('when play mutation fires', () => {
beforeEach(async () => {
await createComponentWithApollo();
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobMutationData);
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobPlayMutationData);
});
it('passes variables in correct format', async () => {
@@ -146,11 +170,11 @@ describe('Manual Variables Form', () => {
await findCiVariableValue().setValue('new value');
- await findRerunBtn().vm.$emit('click');
+ await findRunBtn().vm.$emit('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: retryJobMutation,
+ mutation: playJobMutation,
variables: {
id: convertToGraphQLId(TYPENAME_CI_BUILD, mockId),
variables: [
@@ -163,13 +187,63 @@ describe('Manual Variables Form', () => {
});
});
- // redirect to job after initial trigger assertion will be added in https://gitlab.com/gitlab-org/gitlab/-/issues/377268
+ it('redirects to job properly after job is run', async () => {
+ findRunBtn().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(mockJobPlayMutationData.data.jobPlay.job.webPath); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('when play mutation is unsuccessful', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
+ await createComponentWithApollo();
+ });
+
+ it('shows an alert with error', async () => {
+ findRunBtn().vm.$emit('click');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText,
+ });
+ });
+ });
+
+ describe('when job is retryable', () => {
+ beforeEach(async () => {
+ await createComponentWithApollo({ props: { isRetryable: true } });
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockJobRetryMutationData);
+ });
+
+ it('renders cancel button', () => {
+ expect(findCancelBtn().exists()).toBe(true);
+ });
+
it('redirects to job properly after rerun', async () => {
- findRerunBtn().vm.$emit('click');
+ findRunBtn().vm.$emit('click');
await waitForPromises();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(redirectTo).toHaveBeenCalledWith(mockJobMutationData.data.jobRetry.job.webPath);
+ expect(redirectTo).toHaveBeenCalledWith(mockJobRetryMutationData.data.jobRetry.job.webPath); // eslint-disable-line import/no-deprecated
+ });
+ });
+
+ describe('when retry mutation is unsuccessful', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
+ await createComponentWithApollo({ props: { isRetryable: true } });
+ });
+
+ it('shows an alert with error', async () => {
+ findRunBtn().vm.$emit('click');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText,
+ });
});
});
@@ -235,7 +309,7 @@ describe('Manual Variables Form', () => {
await createComponentWithApollo();
});
- it('delete variable button placeholder should only exist when a user cannot remove', async () => {
+ it('delete variable button placeholder should only exist when a user cannot remove', () => {
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
});
diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js
index 8a838acca7a..fb3a361c9c9 100644
--- a/spec/frontend/jobs/components/job/mock_data.js
+++ b/spec/frontend/jobs/components/job/mock_data.js
@@ -50,7 +50,32 @@ export const mockJobWithVariablesResponse = {
},
};
-export const mockJobMutationData = {
+export const mockJobPlayMutationData = {
+ data: {
+ jobPlay: {
+ job: {
+ id: 'gid://gitlab/Ci::Build/401',
+ manualVariables: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::JobVariable/151',
+ key: 'new key',
+ value: 'new value',
+ __typename: 'CiManualVariable',
+ },
+ ],
+ __typename: 'CiManualVariableConnection',
+ },
+ webPath: '/Commit451/lab-coat/-/jobs/401',
+ __typename: 'CiJob',
+ },
+ errors: [],
+ __typename: 'JobPlayPayload',
+ },
+ },
+};
+
+export const mockJobRetryMutationData = {
data: {
jobRetry: {
job: {
diff --git a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
index 5c9c011b4ab..fd27004816a 100644
--- a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js
@@ -1,5 +1,4 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SidebarDetailRow from '~/jobs/components/job/sidebar/sidebar_detail_row.vue';
describe('Sidebar detail row', () => {
@@ -8,23 +7,20 @@ describe('Sidebar detail row', () => {
const title = 'this is the title';
const value = 'this is the value';
const helpUrl = 'https://docs.gitlab.com/runner/register/index.html';
+ const path = 'path/to/value';
- const findHelpLink = () => wrapper.findComponent(GlLink);
+ const findHelpLink = () => wrapper.findByTestId('job-sidebar-help-link');
+ const findValueLink = () => wrapper.findByTestId('job-sidebar-value-link');
const createComponent = (props) => {
- wrapper = shallowMount(SidebarDetailRow, {
+ wrapper = shallowMountExtended(SidebarDetailRow, {
propsData: {
...props,
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('with title/value and without helpUrl', () => {
+ describe('with title/value and without helpUrl/path', () => {
beforeEach(() => {
createComponent({ title, value });
});
@@ -36,6 +32,10 @@ describe('Sidebar detail row', () => {
it('should not render the help link', () => {
expect(findHelpLink().exists()).toBe(false);
});
+
+ it('should not render the value link', () => {
+ expect(findValueLink().exists()).toBe(false);
+ });
});
describe('when helpUrl provided', () => {
@@ -52,4 +52,16 @@ describe('Sidebar detail row', () => {
expect(findHelpLink().attributes('href')).toBe(helpUrl);
});
});
+
+ describe('when path is provided', () => {
+ it('should render link to value', () => {
+ createComponent({
+ path,
+ title,
+ value,
+ });
+
+ expect(findValueLink().attributes('href')).toBe(path);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js
index da97945f9bf..cf182330578 100644
--- a/spec/frontend/jobs/components/job/sidebar_header_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js
@@ -31,7 +31,7 @@ describe('Sidebar Header', () => {
});
};
- const createComponentWithApollo = async ({ props = {}, restJob = {} } = {}) => {
+ const createComponentWithApollo = ({ props = {}, restJob = {} } = {}) => {
const getJobQueryResponse = jest.fn().mockResolvedValue(mockJobResponse);
const requestHandlers = [[getJobQuery, getJobQueryResponse]];
diff --git a/spec/frontend/jobs/components/job/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js
index aa9ca932023..fbff64b4d78 100644
--- a/spec/frontend/jobs/components/job/sidebar_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_spec.js
@@ -48,10 +48,6 @@ describe('Sidebar details block', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without terminal path', () => {
it('does not render terminal link', async () => {
createWrapper();
@@ -143,7 +139,7 @@ describe('Sidebar details block', () => {
return store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
});
- it('renders list of jobs', async () => {
+ it('renders list of jobs', () => {
expect(findJobsContainer().exists()).toBe(true);
});
});
@@ -151,7 +147,7 @@ describe('Sidebar details block', () => {
describe('when job data changes', () => {
const stageArg = job.pipeline.details.stages.find((stage) => stage.name === job.stage);
- beforeEach(async () => {
+ beforeEach(() => {
jest.spyOn(store, 'dispatch');
});
diff --git a/spec/frontend/jobs/components/job/stages_dropdown_spec.js b/spec/frontend/jobs/components/job/stages_dropdown_spec.js
index 61dec585e82..9d01dc50e96 100644
--- a/spec/frontend/jobs/components/job/stages_dropdown_spec.js
+++ b/spec/frontend/jobs/components/job/stages_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlDropdown, GlDropdownItem, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Mousetrap from 'mousetrap';
+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';
@@ -37,10 +37,6 @@ describe('Stages Dropdown', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without a merge request pipeline', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/jobs/components/job/stuck_block_spec.js b/spec/frontend/jobs/components/job/stuck_block_spec.js
index 8dc570cce27..0f014a9222b 100644
--- a/spec/frontend/jobs/components/job/stuck_block_spec.js
+++ b/spec/frontend/jobs/components/job/stuck_block_spec.js
@@ -5,13 +5,6 @@ import StuckBlock from '~/jobs/components/job/stuck_block.vue';
describe('Stuck Block Job component', () => {
let wrapper;
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const createWrapper = (props) => {
wrapper = shallowMount(StuckBlock, {
propsData: {
diff --git a/spec/frontend/jobs/components/job/trigger_block_spec.js b/spec/frontend/jobs/components/job/trigger_block_spec.js
index a1de8fd143f..8bb2c1f3ad8 100644
--- a/spec/frontend/jobs/components/job/trigger_block_spec.js
+++ b/spec/frontend/jobs/components/job/trigger_block_spec.js
@@ -20,10 +20,6 @@ describe('Trigger block', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with short token and no variables', () => {
it('renders short token', () => {
createComponent({
diff --git a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
index fb7d389c4d6..1072cdd6781 100644
--- a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
+++ b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js
@@ -18,10 +18,6 @@ describe('Unmet Prerequisites Block Job component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders an alert with the correct message', () => {
const container = wrapper.findComponent(GlAlert);
const alertMessage =
diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js
index 646935568b1..5adedea28a5 100644
--- a/spec/frontend/jobs/components/log/collapsible_section_spec.js
+++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js
@@ -19,10 +19,6 @@ describe('Job Log Collapsible Section', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with closed section', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/jobs/components/log/duration_badge_spec.js b/spec/frontend/jobs/components/log/duration_badge_spec.js
index 84dae386bdb..644d05366a0 100644
--- a/spec/frontend/jobs/components/log/duration_badge_spec.js
+++ b/spec/frontend/jobs/components/log/duration_badge_spec.js
@@ -20,10 +20,6 @@ describe('Job Log Duration Badge', () => {
createComponent(data);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders provided duration', () => {
expect(wrapper.text()).toBe(data.duration);
});
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index ec8e79bba13..16fe753e08a 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -29,10 +29,6 @@ describe('Job Log Header Line', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('line', () => {
beforeEach(() => {
createComponent(data);
diff --git a/spec/frontend/jobs/components/log/line_number_spec.js b/spec/frontend/jobs/components/log/line_number_spec.js
index 96aa31baab9..4130c124a30 100644
--- a/spec/frontend/jobs/components/log/line_number_spec.js
+++ b/spec/frontend/jobs/components/log/line_number_spec.js
@@ -21,10 +21,6 @@ describe('Job Log Line Number', () => {
createComponent(data);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders incremented lineNunber by 1', () => {
expect(wrapper.text()).toBe('1');
});
diff --git a/spec/frontend/jobs/components/log/log_spec.js b/spec/frontend/jobs/components/log/log_spec.js
index c933ed5c3e1..20638b13169 100644
--- a/spec/frontend/jobs/components/log/log_spec.js
+++ b/spec/frontend/jobs/components/log/log_spec.js
@@ -1,15 +1,24 @@
import { mount } from '@vue/test-utils';
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 { jobLog } from './mock_data';
+jest.mock('~/lib/utils/common_utils', () => ({
+ ...jest.requireActual('~/lib/utils/common_utils'),
+ scrollToElement: jest.fn(),
+}));
+
describe('Job Log', () => {
let wrapper;
let actions;
let state;
let store;
+ let toggleCollapsibleLineMock;
Vue.use(Vuex);
@@ -20,8 +29,9 @@ describe('Job Log', () => {
};
beforeEach(() => {
+ toggleCollapsibleLineMock = jest.fn();
actions = {
- toggleCollapsibleLine: () => {},
+ toggleCollapsibleLine: toggleCollapsibleLineMock,
};
state = {
@@ -33,17 +43,15 @@ describe('Job Log', () => {
actions,
state,
});
-
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
});
- const findCollapsibleLine = () => wrapper.find('.collapsible-line');
+ const findCollapsibleLine = () => wrapper.findComponent(LogLineHeader);
describe('line numbers', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('renders a line number for each open line', () => {
expect(wrapper.find('#L1').text()).toBe('1');
expect(wrapper.find('#L2').text()).toBe('2');
@@ -56,6 +64,10 @@ describe('Job Log', () => {
});
describe('collapsible sections', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('renders a clickable header section', () => {
expect(findCollapsibleLine().attributes('role')).toBe('button');
});
@@ -68,11 +80,54 @@ describe('Job Log', () => {
describe('on click header section', () => {
it('calls toggleCollapsibleLine', () => {
- jest.spyOn(wrapper.vm, 'toggleCollapsibleLine');
-
findCollapsibleLine().trigger('click');
- expect(wrapper.vm.toggleCollapsibleLine).toHaveBeenCalled();
+ expect(toggleCollapsibleLineMock).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('anchor scrolling', () => {
+ afterEach(() => {
+ window.location.hash = '';
+ });
+
+ describe('when hash is not present', () => {
+ it('does not scroll to line number', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.find('#L6').exists()).toBe(false);
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when hash is present', () => {
+ beforeEach(() => {
+ window.location.hash = '#L6';
+ });
+
+ it('scrolls to line number', async () => {
+ createComponent();
+
+ state.jobLog = logLinesParser(jobLog, [], '#L6');
+ await waitForPromises();
+
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+
+ state.jobLog = logLinesParser(jobLog, [], '#L7');
+ await waitForPromises();
+
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ });
+
+ it('line number within collapsed section is visible', () => {
+ state.jobLog = logLinesParser(jobLog, [], '#L6');
+
+ createComponent();
+
+ expect(wrapper.find('#L6').exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js
index eb8c4fe8bc9..fa51b92a044 100644
--- a/spec/frontend/jobs/components/log/mock_data.js
+++ b/spec/frontend/jobs/components/log/mock_data.js
@@ -22,6 +22,30 @@ export const jobLog = [
content: [{ text: 'Starting service postgres:9.6.14 ...', style: 'text-green' }],
section: 'prepare-executor',
},
+ {
+ offset: 1004,
+ content: [
+ {
+ text: 'Restore cache',
+ style: 'term-fg-l-cyan term-bold',
+ },
+ ],
+ section: 'restore-cache',
+ section_header: true,
+ section_options: {
+ collapsed: 'true',
+ },
+ },
+ {
+ offset: 1005,
+ content: [
+ {
+ text: 'Checking cache for ruby-gems-debian-bullseye-ruby-3.0-16...',
+ style: 'term-fg-l-green term-bold',
+ },
+ ],
+ section: 'restore-cache',
+ },
];
export const utilsMockData = [
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
index 55fe534aa3b..f2d249b6014 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -4,7 +4,7 @@ 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 { redirectTo } from '~/lib/utils/url_utility';
+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';
@@ -122,7 +122,7 @@ describe('Job actions cell', () => {
${findPlayButton} | ${'play'} | ${playableJob} | ${JobPlayMutation} | ${playMutationHandler} | ${playableJob.id}
${findRetryButton} | ${'retry'} | ${retryableJob} | ${JobRetryMutation} | ${retryMutationHandler} | ${retryableJob.id}
${findCancelButton} | ${'cancel'} | ${cancelableJob} | ${JobCancelMutation} | ${cancelMutationHandler} | ${cancelableJob.id}
- `('performs the $action mutation', async ({ button, jobType, mutationFile, handler, jobId }) => {
+ `('performs the $action mutation', ({ button, jobType, mutationFile, handler, jobId }) => {
createComponent(jobType, [[mutationFile, handler]]);
button().vm.$emit('click');
@@ -146,7 +146,7 @@ describe('Job actions cell', () => {
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith('jobActionPerformed');
- expect(redirectTo).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
},
);
@@ -165,7 +165,7 @@ describe('Job actions cell', () => {
await waitForPromises();
- expect(redirectTo).toHaveBeenCalledWith(redirectLink);
+ expect(redirectTo).toHaveBeenCalledWith(redirectLink); // eslint-disable-line import/no-deprecated
expect(eventHub.$emit).not.toHaveBeenCalled();
},
);
diff --git a/spec/frontend/jobs/components/table/cells/duration_cell_spec.js b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
index 763a4b0eaa2..d015edb0e91 100644
--- a/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/duration_cell_spec.js
@@ -22,10 +22,6 @@ describe('Duration Cell', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not display duration or finished time when no properties are present', () => {
createComponent();
diff --git a/spec/frontend/jobs/components/table/cells/job_cell_spec.js b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
index ddc196129a7..73e37eed5f1 100644
--- a/spec/frontend/jobs/components/table/cells/job_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/job_cell_spec.js
@@ -39,10 +39,6 @@ describe('Job Cell', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Job Id', () => {
it('displays the job id and links to the job', () => {
createComponent();
diff --git a/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
index 1f5e0a7aa21..3d424b20964 100644
--- a/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/pipeline_cell_spec.js
@@ -42,10 +42,6 @@ describe('Pipeline Cell', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Pipeline Id', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
index 88c97285b85..e3b1ca1cce3 100644
--- a/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
+++ b/spec/frontend/jobs/components/table/graphql/cache_config_spec.js
@@ -84,4 +84,23 @@ describe('jobs/components/table/graphql/cache_config', () => {
expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length);
});
});
+
+ describe('when incoming data has no nodes', () => {
+ it('should return existing cache', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ { __typename: 'CiJobConnection', count: 500 },
+ {
+ args: { statuses: 'SUCCESS' },
+ },
+ );
+
+ const expectedResponse = {
+ ...CIJobConnectionExistingCache,
+ statuses: 'SUCCESS',
+ };
+
+ expect(res).toEqual(expectedResponse);
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 109cef6f817..0e59e9ab5b6 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -1,10 +1,4 @@
-import {
- GlSkeletonLoader,
- GlAlert,
- GlEmptyState,
- GlIntersectionObserver,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlAlert, GlEmptyState, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -12,23 +6,26 @@ import { s__ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
-import { createAlert } from '~/flash';
+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 * as urlUtils from '~/lib/utils/url_utility';
import {
mockJobsResponsePaginated,
mockJobsResponseEmpty,
mockFailedSearchToken,
+ mockJobsCountResponse,
} from '../../mock_data';
const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Job table app', () => {
let wrapper;
@@ -37,7 +34,9 @@ describe('Job table app', () => {
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const emptyHandler = jest.fn().mockResolvedValue(mockJobsResponseEmpty);
- const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const countSuccessHandler = jest.fn().mockResolvedValue(mockJobsCountResponse);
+
+ const findSkeletonLoader = () => wrapper.findComponent(JobsSkeletonLoader);
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findTable = () => wrapper.findComponent(JobsTable);
const findTabs = () => wrapper.findComponent(JobsTableTabs);
@@ -48,14 +47,18 @@ describe('Job table app', () => {
const triggerInfiniteScroll = () =>
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
- const createMockApolloProvider = (handler) => {
- const requestHandlers = [[getJobsQuery, handler]];
+ const createMockApolloProvider = (handler, countHandler) => {
+ const requestHandlers = [
+ [getJobsQuery, handler],
+ [getJobsCountQuery, countHandler],
+ ];
return createMockApollo(requestHandlers);
};
const createComponent = ({
handler = successHandler,
+ countHandler = countSuccessHandler,
mountFn = shallowMount,
data = {},
} = {}) => {
@@ -68,14 +71,10 @@ describe('Job table app', () => {
provide: {
fullPath: projectPath,
},
- apolloProvider: createMockApolloProvider(handler),
+ apolloProvider: createMockApolloProvider(handler, countHandler),
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('should display skeleton loader when loading', () => {
createComponent();
@@ -118,6 +117,35 @@ describe('Job table app', () => {
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
});
+ it('avoids refetch jobs query when scope has not changed', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findTabs().vm.$emit('fetchJobsByStatus', null);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ });
+
+ it('should refetch jobs count query when the amount jobs and count do not match', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0);
+
+ // after applying filter a new count is fetched
+ findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1);
+
+ // 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(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(2);
+ });
+
describe('when infinite scrolling is triggered', () => {
it('does not display a skeleton loader', () => {
triggerInfiniteScroll();
@@ -148,12 +176,39 @@ describe('Job table app', () => {
});
describe('error state', () => {
- it('should show an alert if there is an error fetching the data', async () => {
+ it('should show an alert if there is an error fetching the jobs data', async () => {
createComponent({ handler: failedHandler });
await waitForPromises();
- expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe('There was an error fetching the jobs for your project.');
+ 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(
+ 'There was an error fetching the number of jobs for your project.',
+ );
+ });
+
+ 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 count should be zero if count query fails', async () => {
+ createComponent({ handler: successHandler, countHandler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findTabs().props('allJobsCount')).toBe(0);
});
});
@@ -215,6 +270,18 @@ describe('Job table app', () => {
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
});
+ it('refetches jobs count query when filtering', async () => {
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1);
+ });
+
it('shows raw text warning when user inputs raw text', async () => {
const expectedWarning = {
message: s__(
@@ -226,11 +293,13 @@ describe('Job table app', () => {
createComponent();
jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+ jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn());
await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']);
expect(createAlert).toHaveBeenCalledWith(expectedWarning);
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0);
});
it('updates URL query string when filtering jobs by status', async () => {
@@ -244,5 +313,42 @@ describe('Job table app', () => {
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: 30,
+ fullPath: 'gitlab-org/gitlab',
+ statuses: 'FAILED',
+ });
+ expect(countSuccessHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab',
+ statuses: 'FAILED',
+ });
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?statuses=FAILED`,
+ });
+
+ findFilteredSearch().vm.$emit('filterJobsBySearch', []);
+
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/`,
+ });
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: 30,
+ fullPath: 'gitlab-org/gitlab',
+ statuses: null,
+ });
+ expect(countSuccessHandler).toHaveBeenCalledWith({
+ fullPath: 'gitlab-org/gitlab',
+ statuses: null,
+ });
+ });
});
});
diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js
index 3c4f2d624fe..654b6d1c130 100644
--- a/spec/frontend/jobs/components/table/jobs_table_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_spec.js
@@ -3,7 +3,10 @@ import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import { mockJobsNodes } from '../../mock_data';
+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';
describe('Jobs Table', () => {
let wrapper;
@@ -13,52 +16,83 @@ describe('Jobs Table', () => {
const findTableRows = () => wrapper.findAllByTestId('jobs-table-row');
const findJobStage = () => wrapper.findByTestId('job-stage-name');
const findJobName = () => wrapper.findByTestId('job-name');
+ const findJobProject = () => wrapper.findComponent(ProjectCell);
+ const findJobRunner = () => wrapper.findComponent(RunnerCell);
const findAllCoverageJobs = () => wrapper.findAllByTestId('job-coverage');
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(JobsTable, {
propsData: {
- jobs: mockJobsNodes,
...props,
},
}),
);
};
- beforeEach(() => {
- createComponent();
- });
+ describe('jobs table', () => {
+ beforeEach(() => {
+ createComponent({ jobs: mockJobsNodes });
+ });
- afterEach(() => {
- wrapper.destroy();
- });
+ it('displays the jobs table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
- it('displays the jobs table', () => {
- expect(findTable().exists()).toBe(true);
- });
+ it('displays correct number of job rows', () => {
+ expect(findTableRows()).toHaveLength(mockJobsNodes.length);
+ });
- it('displays correct number of job rows', () => {
- expect(findTableRows()).toHaveLength(mockJobsNodes.length);
- });
+ it('displays job status', () => {
+ expect(findCiBadgeLink().exists()).toBe(true);
+ });
+
+ it('displays the job stage and name', () => {
+ const [firstJob] = mockJobsNodes;
+
+ expect(findJobStage().text()).toBe(firstJob.stage.name);
+ expect(findJobName().text()).toBe(firstJob.name);
+ });
- it('displays job status', () => {
- expect(findCiBadgeLink().exists()).toBe(true);
+ it('displays the coverage for only jobs that have coverage', () => {
+ const jobsThatHaveCoverage = mockJobsNodes.filter((job) => job.coverage !== null);
+
+ jobsThatHaveCoverage.forEach((job, index) => {
+ expect(findAllCoverageJobs().at(index).text()).toBe(`${job.coverage}%`);
+ });
+ expect(findAllCoverageJobs()).toHaveLength(jobsThatHaveCoverage.length);
+ });
});
- it('displays the job stage and name', () => {
- const firstJob = mockJobsNodes[0];
+ describe('regular user', () => {
+ beforeEach(() => {
+ createComponent({ jobs: mockJobsNodes });
+ });
+
+ it('hides the job runner', () => {
+ expect(findJobRunner().exists()).toBe(false);
+ });
- expect(findJobStage().text()).toBe(firstJob.stage.name);
- expect(findJobName().text()).toBe(firstJob.name);
+ it('hides the job project link', () => {
+ expect(findJobProject().exists()).toBe(false);
+ });
});
- it('displays the coverage for only jobs that have coverage', () => {
- const jobsThatHaveCoverage = mockJobsNodes.filter((job) => job.coverage !== null);
+ describe('admin mode', () => {
+ beforeEach(() => {
+ createComponent({ jobs: mockAllJobsNodes, tableFields: DEFAULT_FIELDS_ADMIN, admin: true });
+ });
+
+ it('displays the runner cell', () => {
+ expect(findJobRunner().exists()).toBe(true);
+ });
+
+ it('displays the project cell', () => {
+ expect(findJobProject().exists()).toBe(true);
+ });
- jobsThatHaveCoverage.forEach((job, index) => {
- expect(findAllCoverageJobs().at(index).text()).toBe(`${job.coverage}%`);
+ it('displays correct number of job rows', () => {
+ expect(findTableRows()).toHaveLength(mockAllJobsNodes.length);
});
- expect(findAllCoverageJobs()).toHaveLength(jobsThatHaveCoverage.length);
});
});
diff --git a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
index 23632001060..d20a732508a 100644
--- a/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_tabs_spec.js
@@ -3,6 +3,7 @@ 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';
describe('Jobs Table Tabs', () => {
let wrapper;
@@ -12,6 +13,11 @@ describe('Jobs Table Tabs', () => {
loading: false,
};
+ const adminProps = {
+ ...defaultProps,
+ showCancelAllJobsButton: true,
+ };
+
const statuses = {
success: 'SUCCESS',
failed: 'FAILED',
@@ -20,6 +26,7 @@ describe('Jobs Table Tabs', () => {
const findAllTab = () => wrapper.findByTestId('jobs-all-tab');
const findFinishedTab = () => wrapper.findByTestId('jobs-finished-tab');
+ const findCancelJobsButton = () => wrapper.findAllComponents(CancelJobs);
const triggerTabChange = (index) => wrapper.findAllComponents(GlTab).at(index).vm.$emit('click');
@@ -42,10 +49,6 @@ describe('Jobs Table Tabs', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays All tab with count', () => {
expect(trimText(findAllTab().text())).toBe(`All ${defaultProps.allJobsCount}`);
});
@@ -63,4 +66,16 @@ describe('Jobs Table Tabs', () => {
expect(wrapper.emitted()).toEqual({ fetchJobsByStatus: [[expectedScope]] });
});
+
+ it('does not displays cancel all jobs button', () => {
+ expect(findCancelJobsButton().exists()).toBe(false);
+ });
+
+ describe('admin mode', () => {
+ it('displays cancel all jobs button', () => {
+ createComponent(adminProps);
+
+ expect(findCancelJobsButton().exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
index 1d3845b19bb..098a63719fe 100644
--- a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
+++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
@@ -16,11 +16,6 @@ describe('DelayedJobMixin', () => {
template: '<div>{{remainingTime}}</div>',
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('if job is empty object', () => {
beforeEach(() => {
wrapper = shallowMount(dummyComponent, {
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 9abd610c26d..253e669e889 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1,7 +1,13 @@
+import mockJobsCount from 'test_fixtures/graphql/jobs/get_jobs_count.query.graphql.json';
+import mockAllJobsCount from 'test_fixtures/graphql/jobs/get_all_jobs_count.query.graphql.json';
import mockJobsEmpty from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.empty.json';
+import mockAllJobsEmpty from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.empty.json';
import mockJobsPaginated from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.paginated.json';
+import mockAllJobsPaginated from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.paginated.json';
import mockJobs from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.json';
+import mockAllJobs from 'test_fixtures/graphql/jobs/get_all_jobs.query.graphql.json';
import mockJobsAsGuest from 'test_fixtures/graphql/jobs/get_jobs.query.graphql.as_guest.json';
+import mockCancelableJobsCount from 'test_fixtures/graphql/jobs/get_cancelable_jobs_count.query.graphql.json';
import { TEST_HOST } from 'spec/test_constants';
import { TOKEN_TYPE_STATUS } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -10,9 +16,15 @@ threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
// Fixtures generated at spec/frontend/fixtures/jobs.rb
export const mockJobsResponsePaginated = mockJobsPaginated;
+export const mockAllJobsResponsePaginated = mockAllJobsPaginated;
export const mockJobsResponseEmpty = mockJobsEmpty;
+export const mockAllJobsResponseEmpty = mockAllJobsEmpty;
export const mockJobsNodes = mockJobs.data.project.jobs.nodes;
+export const mockAllJobsNodes = mockAllJobs.data.jobs.nodes;
export const mockJobsNodesAsGuest = mockJobsAsGuest.data.project.jobs.nodes;
+export const mockJobsCountResponse = mockJobsCount;
+export const mockAllJobsCountResponse = mockAllJobsCount;
+export const mockCancelableJobsCountResponse = mockCancelableJobsCount;
export const stages = [
{
@@ -920,6 +932,14 @@ export const stages = [
},
];
+export const statuses = {
+ success: 'SUCCESS',
+ failed: 'FAILED',
+ canceled: 'CANCELED',
+ pending: 'PENDING',
+ running: 'RUNNING',
+};
+
export default {
id: 4757,
artifact: {
diff --git a/spec/frontend/jobs/store/utils_spec.js b/spec/frontend/jobs/store/utils_spec.js
index 9458c2184f5..37a6722c555 100644
--- a/spec/frontend/jobs/store/utils_spec.js
+++ b/spec/frontend/jobs/store/utils_spec.js
@@ -43,6 +43,14 @@ describe('Jobs Store Utils', () => {
expect(parsedHeaderLine.isClosed).toBe(true);
});
+
+ it('expands all pre-closed sections if hash is present', () => {
+ const headerLine = { content: [{ text: 'foo' }], section_options: { collapsed: 'true' } };
+
+ const parsedHeaderLine = parseHeaderLine(headerLine, 2, '#L33');
+
+ expect(parsedHeaderLine.isClosed).toBe(false);
+ });
});
describe('parseLine', () => {
diff --git a/spec/frontend/labels/components/delete_label_modal_spec.js b/spec/frontend/labels/components/delete_label_modal_spec.js
index 24a803d3f16..19aef42528a 100644
--- a/spec/frontend/labels/components/delete_label_modal_spec.js
+++ b/spec/frontend/labels/components/delete_label_modal_spec.js
@@ -1,64 +1,55 @@
import { GlModal } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import DeleteLabelModal from '~/labels/components/delete_label_modal.vue';
-const MOCK_MODAL_DATA = {
- labelName: 'label 1',
- subjectName: 'GitLab Org',
- destroyPath: `${TEST_HOST}/1`,
-};
-
describe('~/labels/components/delete_label_modal', () => {
let wrapper;
- const createComponent = () => {
- wrapper = extendedWrapper(
- mount(DeleteLabelModal, {
- propsData: {
- selector: '.js-test-btn',
- },
- stubs: {
- GlModal: stubComponent(GlModal, {
- template:
- '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
- }),
- },
- }),
- );
- };
+ const mountComponent = () => {
+ const button = document.createElement('button');
+ button.classList.add('js-test-btn');
+ button.dataset.destroyPath = `${TEST_HOST}/1`;
+ button.dataset.labelName = 'label 1';
+ button.dataset.subjectName = 'GitLab Org';
+ document.body.append(button);
+
+ wrapper = mountExtended(DeleteLabelModal, {
+ propsData: {
+ selector: '.js-test-btn',
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ },
+ });
- afterEach(() => {
- wrapper.destroy();
- });
+ button.click();
+ };
const findModal = () => wrapper.findComponent(GlModal);
- const findPrimaryModalButton = () => wrapper.findByTestId('delete-button');
+ const findDeleteButton = () => wrapper.findByRole('link', { name: 'Delete label' });
- describe('template', () => {
- describe('when modal data is set', () => {
- beforeEach(() => {
- createComponent();
- wrapper.vm.labelName = MOCK_MODAL_DATA.labelName;
- wrapper.vm.subjectName = MOCK_MODAL_DATA.subjectName;
- wrapper.vm.destroyPath = MOCK_MODAL_DATA.destroyPath;
- });
+ describe('when modal data is set', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
- it('renders GlModal', () => {
- expect(findModal().exists()).toBe(true);
- });
+ it('renders GlModal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
- it('displays the label name and subject name', () => {
- expect(findModal().text()).toContain(
- `${MOCK_MODAL_DATA.labelName} will be permanently deleted from ${MOCK_MODAL_DATA.subjectName}. This cannot be undone`,
- );
- });
+ it('displays the label name and subject name', () => {
+ expect(findModal().text()).toContain(
+ `label 1 will be permanently deleted from GitLab Org. This cannot be undone`,
+ );
+ });
- it('passes the destroyPath to the button', () => {
- expect(findPrimaryModalButton().attributes('href')).toBe(MOCK_MODAL_DATA.destroyPath);
- });
+ it('passes the destroyPath to the button', () => {
+ expect(findDeleteButton().attributes('href')).toBe('http://test.host/1');
});
});
});
diff --git a/spec/frontend/labels/components/promote_label_modal_spec.js b/spec/frontend/labels/components/promote_label_modal_spec.js
index 97913c20229..5983c16a9d1 100644
--- a/spec/frontend/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/labels/components/promote_label_modal_spec.js
@@ -41,7 +41,6 @@ describe('Promote label modal', () => {
afterEach(() => {
axiosMock.reset();
- wrapper.destroy();
});
describe('Modal title and description', () => {
diff --git a/spec/frontend/language_switcher/components/app_spec.js b/spec/frontend/language_switcher/components/app_spec.js
index 7f6fb138d89..036ff55fef7 100644
--- a/spec/frontend/language_switcher/components/app_spec.js
+++ b/spec/frontend/language_switcher/components/app_spec.js
@@ -24,10 +24,6 @@ describe('<LanguageSwitcher />', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const getPreferredLanguage = () => wrapper.find('.gl-new-dropdown-button-text').text();
const findLanguageDropdownItem = (code) => wrapper.findByTestId(`language_switcher_lang_${code}`);
const findFooter = () => wrapper.findByTestId('footer');
diff --git a/spec/frontend/lib/apollo/indexed_db_persistent_storage_spec.js b/spec/frontend/lib/apollo/indexed_db_persistent_storage_spec.js
new file mode 100644
index 00000000000..f96364a918e
--- /dev/null
+++ b/spec/frontend/lib/apollo/indexed_db_persistent_storage_spec.js
@@ -0,0 +1,90 @@
+import { IndexedDBPersistentStorage } from '~/lib/apollo/indexed_db_persistent_storage';
+import { db } from '~/lib/apollo/local_db';
+import CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS from './mock_data/cache_with_persist_directive_and_field.json';
+
+describe('IndexedDBPersistentStorage', () => {
+ let subject;
+
+ const seedData = async (cacheKey, data = CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS) => {
+ const { ROOT_QUERY, ...rest } = data;
+
+ await db.table('queries').put(ROOT_QUERY, cacheKey);
+
+ const asyncPuts = Object.entries(rest).map(async ([key, value]) => {
+ const {
+ groups: { type, gid },
+ } = /^(?<type>.+?):(?<gid>.+)$/.exec(key);
+ const tableName = type.toLowerCase();
+
+ if (tableName !== 'projectmember' && tableName !== 'groupmember') {
+ await db.table(tableName).put(value, gid);
+ }
+ });
+
+ await Promise.all(asyncPuts);
+ };
+
+ beforeEach(async () => {
+ subject = await IndexedDBPersistentStorage.create();
+ });
+
+ afterEach(() => {
+ db.close();
+ });
+
+ it('returns empty response if there is nothing stored in the DB', async () => {
+ const result = await subject.getItem('some-query');
+
+ expect(result).toEqual({});
+ });
+
+ it('returns stored cache if cache was persisted in IndexedDB', async () => {
+ await seedData('issues_list', CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS);
+
+ const result = await subject.getItem('issues_list');
+ expect(result).toEqual(CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS);
+ });
+
+ it('puts the results in database on `setItem` call', async () => {
+ await subject.setItem(
+ 'issues_list',
+ JSON.stringify({
+ ROOT_QUERY: 'ROOT_QUERY_KEY',
+ 'Project:gid://gitlab/Project/6': {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/6',
+ },
+ }),
+ );
+
+ await expect(db.table('queries').get('issues_list')).resolves.toEqual('ROOT_QUERY_KEY');
+ await expect(db.table('project').get('gid://gitlab/Project/6')).resolves.toEqual({
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/6',
+ });
+ });
+
+ it('does not put results into non-existent table', async () => {
+ const queryId = 'issues_list';
+
+ await subject.setItem(
+ queryId,
+ JSON.stringify({
+ ROOT_QUERY: 'ROOT_QUERY_KEY',
+ 'DNE:gid://gitlab/DNE/1': {},
+ }),
+ );
+
+ expect(db.tables.map((x) => x.name)).not.toContain('dne');
+ });
+
+ it('when removeItem is called, clears all data', async () => {
+ await seedData('issues_list', CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS);
+
+ await subject.removeItem();
+
+ const actual = await Promise.all(db.tables.map((x) => x.toArray()));
+
+ expect(actual).toEqual(db.tables.map(() => []));
+ });
+});
diff --git a/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json
index c0651517986..0dfc2240cc3 100644
--- a/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json
+++ b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json
@@ -171,38 +171,6 @@
}
]
},
- "projectMembers({\"relations\":[\"DIRECT\",\"INHERITED\",\"INVITED_GROUPS\"],\"search\":\"\"})": {
- "__typename": "MemberInterfaceConnection",
- "nodes": [
- {
- "__ref": "ProjectMember:gid://gitlab/ProjectMember/54"
- },
- {
- "__ref": "ProjectMember:gid://gitlab/ProjectMember/53"
- },
- {
- "__ref": "ProjectMember:gid://gitlab/ProjectMember/52"
- },
- {
- "__ref": "GroupMember:gid://gitlab/GroupMember/26"
- },
- {
- "__ref": "GroupMember:gid://gitlab/GroupMember/25"
- },
- {
- "__ref": "GroupMember:gid://gitlab/GroupMember/11"
- },
- {
- "__ref": "GroupMember:gid://gitlab/GroupMember/10"
- },
- {
- "__ref": "GroupMember:gid://gitlab/GroupMember/9"
- },
- {
- "__ref": "GroupMember:gid://gitlab/GroupMember/1"
- }
- ]
- },
"milestones({\"includeAncestors\":true,\"searchTitle\":\"\",\"sort\":\"EXPIRED_LAST_DUE_DATE_ASC\",\"state\":\"active\"})": {
"__typename": "MilestoneConnection",
"nodes": [
@@ -1999,125 +1967,6 @@
"healthStatus": null,
"weight": null
},
- "UserCore:gid://gitlab/User/9": {
- "__typename": "UserCore",
- "id": "gid://gitlab/User/9",
- "avatarUrl": "https://secure.gravatar.com/avatar/175e76e391370beeb21914ab74c2efd4?s=80&d=identicon",
- "name": "Kiyoko Bahringer",
- "username": "jamie"
- },
- "ProjectMember:gid://gitlab/ProjectMember/54": {
- "__typename": "ProjectMember",
- "id": "gid://gitlab/ProjectMember/54",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/9"
- }
- },
- "UserCore:gid://gitlab/User/19": {
- "__typename": "UserCore",
- "id": "gid://gitlab/User/19",
- "avatarUrl": "https://secure.gravatar.com/avatar/3126153e3301ebf7cc8f7c99e57007f2?s=80&d=identicon",
- "name": "Cecile Hermann",
- "username": "jeannetta_breitenberg"
- },
- "ProjectMember:gid://gitlab/ProjectMember/53": {
- "__typename": "ProjectMember",
- "id": "gid://gitlab/ProjectMember/53",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/19"
- }
- },
- "UserCore:gid://gitlab/User/2": {
- "__typename": "UserCore",
- "id": "gid://gitlab/User/2",
- "avatarUrl": "https://secure.gravatar.com/avatar/a138e401136c90561f949297387a3bb9?s=80&d=identicon",
- "name": "Tish Treutel",
- "username": "liana.larkin"
- },
- "ProjectMember:gid://gitlab/ProjectMember/52": {
- "__typename": "ProjectMember",
- "id": "gid://gitlab/ProjectMember/52",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/2"
- }
- },
- "UserCore:gid://gitlab/User/13": {
- "__typename": "UserCore",
- "id": "gid://gitlab/User/13",
- "avatarUrl": "https://secure.gravatar.com/avatar/0ce8057f452296a13b5620bb2d9ede57?s=80&d=identicon",
- "name": "Tammy Gusikowski",
- "username": "xuan_oreilly"
- },
- "GroupMember:gid://gitlab/GroupMember/26": {
- "__typename": "GroupMember",
- "id": "gid://gitlab/GroupMember/26",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/13"
- }
- },
- "UserCore:gid://gitlab/User/21": {
- "__typename": "UserCore",
- "id": "gid://gitlab/User/21",
- "avatarUrl": "https://secure.gravatar.com/avatar/415b09d256f26403384363d7948c4d77?s=80&d=identicon",
- "name": "Twanna Hegmann",
- "username": "jamaal"
- },
- "GroupMember:gid://gitlab/GroupMember/25": {
- "__typename": "GroupMember",
- "id": "gid://gitlab/GroupMember/25",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/21"
- }
- },
- "UserCore:gid://gitlab/User/14": {
- "__typename": "UserCore",
- "id": "gid://gitlab/User/14",
- "avatarUrl": "https://secure.gravatar.com/avatar/e99697c6664381b0351b7617717dd49b?s=80&d=identicon",
- "name": "Francie Cole",
- "username": "greg.wisoky"
- },
- "GroupMember:gid://gitlab/GroupMember/11": {
- "__typename": "GroupMember",
- "id": "gid://gitlab/GroupMember/11",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/14"
- }
- },
- "UserCore:gid://gitlab/User/7": {
- "__typename": "UserCore",
- "id": "gid://gitlab/User/7",
- "avatarUrl": "https://secure.gravatar.com/avatar/3a382857e362d6cce60d3806dd173444?s=80&d=identicon",
- "name": "Ivan Carter",
- "username": "ethyl"
- },
- "GroupMember:gid://gitlab/GroupMember/10": {
- "__typename": "GroupMember",
- "id": "gid://gitlab/GroupMember/10",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/7"
- }
- },
- "UserCore:gid://gitlab/User/15": {
- "__typename": "UserCore",
- "id": "gid://gitlab/User/15",
- "avatarUrl": "https://secure.gravatar.com/avatar/79653006ff557e081db02deaa4ca281c?s=80&d=identicon",
- "name": "Danuta Dare",
- "username": "maddie_hintz"
- },
- "GroupMember:gid://gitlab/GroupMember/9": {
- "__typename": "GroupMember",
- "id": "gid://gitlab/GroupMember/9",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/15"
- }
- },
- "GroupMember:gid://gitlab/GroupMember/1": {
- "__typename": "GroupMember",
- "id": "gid://gitlab/GroupMember/1",
- "user": {
- "__ref": "UserCore:gid://gitlab/User/1"
- }
- },
"Milestone:gid://gitlab/Milestone/30": {
"__typename": "Milestone",
"id": "gid://gitlab/Milestone/30",
diff --git a/spec/frontend/lib/apollo/persist_link_spec.js b/spec/frontend/lib/apollo/persist_link_spec.js
index ddb861bcee0..f3afc4ba8cd 100644
--- a/spec/frontend/lib/apollo/persist_link_spec.js
+++ b/spec/frontend/lib/apollo/persist_link_spec.js
@@ -56,7 +56,7 @@ describe('~/lib/apollo/persist_link', () => {
expect(childFields.some((field) => field.name.value === '__persist')).toBe(false);
});
- it('decorates the response with `__persist: true` is there is `__persist` field in the query', async () => {
+ it('decorates the response with `__persist: true` is there is `__persist` field in the query', () => {
const link = getPersistLink().concat(terminatingLink);
subscription = execute(link, { query: QUERY_WITH_PERSIST_FIELD }).subscribe(({ data }) => {
@@ -64,7 +64,7 @@ describe('~/lib/apollo/persist_link', () => {
});
});
- it('does not decorate the response with `__persist: true` is there if query is not persistent', async () => {
+ it('does not decorate the response with `__persist: true` is there if query is not persistent', () => {
const link = getPersistLink().concat(terminatingLink);
subscription = execute(link, { query: DEFAULT_QUERY }).subscribe(({ data }) => {
diff --git a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
index 5ac7a7985a8..b8847f0fca3 100644
--- a/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
+++ b/spec/frontend/lib/apollo/suppress_network_errors_during_navigation_link_spec.js
@@ -6,13 +6,8 @@ import { isNavigatingAway } from '~/lib/utils/is_navigating_away';
jest.mock('~/lib/utils/is_navigating_away');
describe('getSuppressNetworkErrorsDuringNavigationLink', () => {
- const originalGon = window.gon;
let subscription;
- beforeEach(() => {
- window.gon = originalGon;
- });
-
afterEach(() => {
if (subscription) {
subscription.unsubscribe();
diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js
index f767a673553..fdc8789c1a8 100644
--- a/spec/frontend/lib/dompurify_spec.js
+++ b/spec/frontend/lib/dompurify_spec.js
@@ -49,8 +49,6 @@ const forbiddenDataAttrs = defaultConfig.FORBID_ATTR;
const acceptedDataAttrs = ['data-random', 'data-custom'];
describe('~/lib/dompurify', () => {
- let originalGon;
-
it('uses local configuration when given', () => {
// As dompurify uses a "Persistent Configuration", it might
// ignore config, this check verifies we respect
@@ -104,15 +102,10 @@ describe('~/lib/dompurify', () => {
${'root'} | ${rootGon}
${'absolute'} | ${absoluteGon}
`('when gon contains $type icon urls', ({ type, gon }) => {
- beforeAll(() => {
- originalGon = window.gon;
+ beforeEach(() => {
window.gon = gon;
});
- afterAll(() => {
- window.gon = originalGon;
- });
-
it('allows no href attrs', () => {
const htmlHref = `<svg><use></use></svg>`;
expect(sanitize(htmlHref)).toBe(htmlHref);
@@ -137,14 +130,9 @@ describe('~/lib/dompurify', () => {
describe('when gon does not contain icon urls', () => {
beforeAll(() => {
- originalGon = window.gon;
window.gon = {};
});
- afterAll(() => {
- window.gon = originalGon;
- });
-
it.each([...safeUrls.root, ...safeUrls.absolute, ...unsafeUrls])('sanitizes URL %s', (url) => {
const htmlHref = `<svg><use href="${url}"></use></svg>`;
const htmlXlink = `<svg><use xlink:href="${url}"></use></svg>`;
diff --git a/spec/frontend/lib/mousetrap_spec.js b/spec/frontend/lib/mousetrap_spec.js
new file mode 100644
index 00000000000..0ea221300a9
--- /dev/null
+++ b/spec/frontend/lib/mousetrap_spec.js
@@ -0,0 +1,113 @@
+// eslint-disable-next-line no-restricted-imports
+import Mousetrap from 'mousetrap';
+
+const originalMethodReturnValue = {};
+// Create a mock stopCallback method before ~/lib/utils/mousetrap overwrites
+// it. This allows us to spy on calls to it.
+const mockOriginalStopCallbackMethod = jest.fn().mockReturnValue(originalMethodReturnValue);
+Mousetrap.prototype.stopCallback = mockOriginalStopCallbackMethod;
+
+describe('mousetrap utils', () => {
+ describe('addStopCallback', () => {
+ let addStopCallback;
+ let clearStopCallbacksForTests;
+ const mockMousetrapInstance = { isMockMousetrap: true };
+ const mockKeyboardEvent = { type: 'keydown', key: 'Enter' };
+ const mockCombo = 'enter';
+
+ const mockKeydown = ({
+ instance = mockMousetrapInstance,
+ event = mockKeyboardEvent,
+ element = document,
+ combo = mockCombo,
+ } = {}) => Mousetrap.prototype.stopCallback.call(instance, event, element, combo);
+
+ beforeEach(async () => {
+ // Import async since it mutates the Mousetrap instance, by design.
+ ({ addStopCallback, clearStopCallbacksForTests } = await import('~/lib/mousetrap'));
+ clearStopCallbacksForTests();
+ });
+
+ it('delegates to the original stopCallback method when no additional callbacks added', () => {
+ const returnValue = mockKeydown();
+
+ expect(mockOriginalStopCallbackMethod).toHaveBeenCalledTimes(1);
+
+ const [thisArg] = mockOriginalStopCallbackMethod.mock.contexts;
+ const [eventArg, element, combo] = mockOriginalStopCallbackMethod.mock.calls[0];
+
+ expect(thisArg).toBe(mockMousetrapInstance);
+ expect(eventArg).toBe(mockKeyboardEvent);
+ expect(element).toBe(document);
+ expect(combo).toBe(mockCombo);
+
+ expect(returnValue).toBe(originalMethodReturnValue);
+ });
+
+ it('passes the expected arguments to the given stop callback', () => {
+ const callback = jest.fn();
+
+ addStopCallback(callback);
+
+ mockKeydown();
+
+ expect(callback).toHaveBeenCalledTimes(1);
+
+ const [thisArg] = callback.mock.contexts;
+ const [eventArg, element, combo] = callback.mock.calls[0];
+
+ expect(thisArg).toBe(mockMousetrapInstance);
+ expect(eventArg).toBe(mockKeyboardEvent);
+ expect(element).toBe(document);
+ expect(combo).toBe(mockCombo);
+ });
+
+ describe.each([true, false])('when a stop handler returns %p', (stopCallbackReturnValue) => {
+ let methodReturnValue;
+ const stopCallback = jest.fn().mockReturnValue(stopCallbackReturnValue);
+
+ beforeEach(() => {
+ addStopCallback(stopCallback);
+
+ methodReturnValue = mockKeydown();
+ });
+
+ it(`returns ${stopCallbackReturnValue}`, () => {
+ expect(methodReturnValue).toBe(stopCallbackReturnValue);
+ });
+
+ it('calls stop callback', () => {
+ expect(stopCallback).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not call mockOriginalStopCallbackMethod', () => {
+ expect(mockOriginalStopCallbackMethod).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when a stop handler returns undefined', () => {
+ let methodReturnValue;
+ const stopCallback = jest.fn().mockReturnValue(undefined);
+
+ beforeEach(() => {
+ addStopCallback(stopCallback);
+
+ methodReturnValue = mockKeydown();
+ });
+
+ it('returns originalMethodReturnValue', () => {
+ expect(methodReturnValue).toBe(originalMethodReturnValue);
+ });
+
+ it('calls stop callback', () => {
+ expect(stopCallback).toHaveBeenCalledTimes(1);
+ });
+
+ // Because this is the only registered stop callback, the next callback
+ // is the original method.
+ it('does call original stopCallback method', () => {
+ expect(mockOriginalStopCallbackMethod).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/axios_startup_calls_spec.js b/spec/frontend/lib/utils/axios_startup_calls_spec.js
index 4471b781446..3d063ff9b46 100644
--- a/spec/frontend/lib/utils/axios_startup_calls_spec.js
+++ b/spec/frontend/lib/utils/axios_startup_calls_spec.js
@@ -113,17 +113,10 @@ describe('setupAxiosStartupCalls', () => {
});
describe('startup call', () => {
- let oldGon;
-
beforeEach(() => {
- oldGon = window.gon;
window.gon = { gitlab_url: 'https://example.org/gitlab' };
});
- afterEach(() => {
- window.gon = oldGon;
- });
-
it('removes GitLab Base URL from startup call', async () => {
window.gl.startup_calls = {
'/startup': {
diff --git a/spec/frontend/lib/utils/chart_utils_spec.js b/spec/frontend/lib/utils/chart_utils_spec.js
index 65bb68c5017..3b34b0ef672 100644
--- a/spec/frontend/lib/utils/chart_utils_spec.js
+++ b/spec/frontend/lib/utils/chart_utils_spec.js
@@ -1,4 +1,8 @@
-import { firstAndLastY } from '~/lib/utils/chart_utils';
+import { firstAndLastY, getToolboxOptions } from '~/lib/utils/chart_utils';
+import { __ } from '~/locale';
+import * as iconUtils from '~/lib/utils/icon_utils';
+
+jest.mock('~/lib/utils/icon_utils');
describe('Chart utils', () => {
describe('firstAndLastY', () => {
@@ -12,4 +16,53 @@ describe('Chart utils', () => {
expect(firstAndLastY(data)).toEqual([1, 3]);
});
});
+
+ describe('getToolboxOptions', () => {
+ describe('when icons are successfully fetched', () => {
+ beforeEach(() => {
+ iconUtils.getSvgIconPathContent.mockImplementation((name) =>
+ Promise.resolve(`${name}-svg-path-mock`),
+ );
+ });
+
+ it('returns toolbox config', async () => {
+ await expect(getToolboxOptions()).resolves.toEqual({
+ toolbox: {
+ feature: {
+ dataZoom: {
+ icon: {
+ zoom: 'path://marquee-selection-svg-path-mock',
+ back: 'path://redo-svg-path-mock',
+ },
+ },
+ restore: {
+ icon: 'path://repeat-svg-path-mock',
+ },
+ saveAsImage: {
+ icon: 'path://download-svg-path-mock',
+ },
+ },
+ },
+ });
+ });
+ });
+
+ describe('when icons are not successfully fetched', () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ iconUtils.getSvgIconPathContent.mockRejectedValue(error);
+ jest.spyOn(console, 'warn').mockImplementation();
+ });
+
+ it('returns empty object and calls `console.warn`', async () => {
+ await expect(getToolboxOptions()).resolves.toEqual({});
+ // eslint-disable-next-line no-console
+ expect(console.warn).toHaveBeenCalledWith(
+ __('SVG could not be rendered correctly: '),
+ error,
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index 87966cf9fba..92ac66c19f0 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -1,44 +1,6 @@
-import {
- isValidColorExpression,
- textColorForBackground,
- hexToRgb,
- validateHexColor,
- darkModeEnabled,
-} from '~/lib/utils/color_utils';
+import { isValidColorExpression, validateHexColor, darkModeEnabled } from '~/lib/utils/color_utils';
describe('Color utils', () => {
- describe('Converting hex code to rgb', () => {
- it('convert hex code to rgb', () => {
- expect(hexToRgb('#000000')).toEqual([0, 0, 0]);
- expect(hexToRgb('#ffffff')).toEqual([255, 255, 255]);
- });
-
- it('convert short hex code to rgb', () => {
- expect(hexToRgb('#000')).toEqual([0, 0, 0]);
- expect(hexToRgb('#fff')).toEqual([255, 255, 255]);
- });
-
- it('handle conversion regardless of the characters case', () => {
- expect(hexToRgb('#f0F')).toEqual([255, 0, 255]);
- });
- });
-
- describe('Getting text color for given background', () => {
- // following tests are being ported from `text_color_for_bg` section in labels_helper_spec.rb
- it('uses light text on dark backgrounds', () => {
- expect(textColorForBackground('#222E2E')).toEqual('#FFFFFF');
- });
-
- it('uses dark text on light backgrounds', () => {
- expect(textColorForBackground('#EEEEEE')).toEqual('#333333');
- });
-
- it('supports RGB triplets', () => {
- expect(textColorForBackground('#FFF')).toEqual('#333333');
- expect(textColorForBackground('#000')).toEqual('#FFFFFF');
- });
- });
-
describe('Validate hex color', () => {
it.each`
color | output
@@ -63,7 +25,7 @@ describe('Color utils', () => {
${'groups:issues:index'} | ${'gl-dark'} | ${'monokai-light'} | ${true}
`(
'is $expected on $page with $bodyClass body class and $ideTheme IDE theme',
- async ({ page, bodyClass, ideTheme, expected }) => {
+ ({ page, bodyClass, ideTheme, expected }) => {
document.body.outerHTML = `<body class="${bodyClass}" data-page="${page}"></body>`;
window.gon = {
user_color_scheme: ideTheme,
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 7b068f7d248..b4ec00ab766 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -534,18 +534,10 @@ describe('common_utils', () => {
});
describe('spriteIcon', () => {
- let beforeGon;
-
beforeEach(() => {
- window.gon = window.gon || {};
- beforeGon = { ...window.gon };
window.gon.sprite_icons = 'icons.svg';
});
- afterEach(() => {
- window.gon = beforeGon;
- });
-
it('should return the svg for a linked icon', () => {
expect(commonUtils.spriteIcon('test')).toEqual(
'<svg ><use xlink:href="icons.svg#test" /></svg>',
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
index 142c76f7bc0..fab5a7b8844 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js
@@ -44,7 +44,6 @@ describe('confirmAction', () => {
resetHTMLFixture();
Vue.prototype.$mount.mockRestore();
modalWrapper?.destroy();
- modalWrapper = null;
modal?.destroy();
modal = null;
});
@@ -67,6 +66,7 @@ describe('confirmAction', () => {
modalHtmlMessage: '<strong>Hello</strong>',
title: 'title',
hideCancel: true,
+ size: 'md',
};
await renderRootComponent('', options);
expect(modal.props()).toEqual(
@@ -80,6 +80,7 @@ describe('confirmAction', () => {
modalHtmlMessage: options.modalHtmlMessage,
title: options.title,
hideCancel: options.hideCancel,
+ size: 'md',
}),
);
});
diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
index 313e028d861..9dcb850076c 100644
--- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
+++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js
@@ -14,6 +14,7 @@ describe('Confirm Modal', () => {
secondaryText,
secondaryVariant,
title,
+ size,
hideCancel = false,
} = {}) => {
wrapper = mount(ConfirmModal, {
@@ -24,14 +25,11 @@ describe('Confirm Modal', () => {
secondaryVariant,
hideCancel,
title,
+ size,
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlModal = () => wrapper.findComponent(GlModal);
describe('Modal events', () => {
@@ -95,5 +93,17 @@ describe('Confirm Modal', () => {
expect(findGlModal().props().title).toBe(title);
});
+
+ it('should set modal size to `sm` by default', () => {
+ createComponent();
+
+ expect(findGlModal().props('size')).toBe('sm');
+ });
+
+ it('should set modal size when `size` prop is set', () => {
+ createComponent({ size: 'md' });
+
+ expect(findGlModal().props('size')).toBe('md');
+ });
});
});
diff --git a/spec/frontend/lib/utils/css_utils_spec.js b/spec/frontend/lib/utils/css_utils_spec.js
new file mode 100644
index 00000000000..dcaeb075c93
--- /dev/null
+++ b/spec/frontend/lib/utils/css_utils_spec.js
@@ -0,0 +1,22 @@
+import { getCssClassDimensions } from '~/lib/utils/css_utils';
+
+describe('getCssClassDimensions', () => {
+ const mockDimensions = { width: 1, height: 2 };
+ let actual;
+
+ beforeEach(() => {
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockReturnValue(mockDimensions);
+ actual = getCssClassDimensions('foo bar');
+ });
+
+ it('returns the measured width and height', () => {
+ expect(actual).toEqual(mockDimensions);
+ });
+
+ it('measures an element with the given classes', () => {
+ expect(Element.prototype.getBoundingClientRect).toHaveBeenCalledTimes(1);
+
+ const [tempElement] = Element.prototype.getBoundingClientRect.mock.contexts;
+ expect([...tempElement.classList]).toEqual(['foo', 'bar']);
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
index a83b0ed9fbe..e7a6367eeac 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -134,18 +134,6 @@ describe('formatTimeAsSummary', () => {
});
});
-describe('durationTimeFormatted', () => {
- it.each`
- duration | expectedOutput
- ${87} | ${'00:01:27'}
- ${141} | ${'00:02:21'}
- ${12} | ${'00:00:12'}
- ${60} | ${'00:01:00'}
- `('returns $expectedOutput when provided $duration', ({ duration, expectedOutput }) => {
- expect(utils.durationTimeFormatted(duration)).toBe(expectedOutput);
- });
-});
-
describe('formatUtcOffset', () => {
it.each`
offset | expected
diff --git a/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js b/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js
new file mode 100644
index 00000000000..15e056e45d0
--- /dev/null
+++ b/spec/frontend/lib/utils/datetime/time_spent_utility_spec.js
@@ -0,0 +1,25 @@
+import { formatTimeSpent } from '~/lib/utils/datetime/time_spent_utility';
+
+describe('Time spent utils', () => {
+ describe('formatTimeSpent', () => {
+ describe('with limitToHours false', () => {
+ it('formats 34500 seconds to `1d 1h 35m`', () => {
+ expect(formatTimeSpent(34500)).toEqual('1d 1h 35m');
+ });
+
+ it('formats -34500 seconds to `- 1d 1h 35m`', () => {
+ expect(formatTimeSpent(-34500)).toEqual('- 1d 1h 35m');
+ });
+ });
+
+ describe('with limitToHours true', () => {
+ it('formats 34500 seconds to `9h 35m`', () => {
+ expect(formatTimeSpent(34500, true)).toEqual('9h 35m');
+ });
+
+ it('formats -34500 seconds to `- 9h 35m`', () => {
+ expect(formatTimeSpent(-34500, true)).toEqual('- 9h 35m');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
index 1ef7047d959..74ce8175357 100644
--- a/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/timeago_utility_spec.js
@@ -1,18 +1,9 @@
+import { DATE_ONLY_FORMAT } from '~/lib/utils/datetime/constants';
import { getTimeago, localTimeAgo, timeFor, duration } from '~/lib/utils/datetime/timeago_utility';
import { s__ } from '~/locale';
import '~/commons/bootstrap';
describe('TimeAgo utils', () => {
- let oldGon;
-
- afterEach(() => {
- window.gon = oldGon;
- });
-
- beforeEach(() => {
- oldGon = window.gon;
- });
-
describe('getTimeago', () => {
describe('with User Setting timeDisplayRelative: true', () => {
beforeEach(() => {
@@ -34,15 +25,37 @@ describe('TimeAgo utils', () => {
window.gon = { time_display_relative: false };
});
- it.each([
+ const defaultFormatExpectations = [
[new Date().toISOString(), 'Jul 6, 2020, 12:00 AM'],
[new Date(), 'Jul 6, 2020, 12:00 AM'],
[new Date().getTime(), 'Jul 6, 2020, 12:00 AM'],
// Slightly different behaviour when `null` is passed :see_no_evil`
[null, 'Jan 1, 1970, 12:00 AM'],
- ])('formats date `%p` as `%p`', (date, result) => {
+ ];
+
+ it.each(defaultFormatExpectations)('formats date `%p` as `%p`', (date, result) => {
expect(getTimeago().format(date)).toEqual(result);
});
+
+ describe('with unknown format', () => {
+ it.each(defaultFormatExpectations)(
+ 'uses default format and formats date `%p` as `%p`',
+ (date, result) => {
+ expect(getTimeago('non_existent').format(date)).toEqual(result);
+ },
+ );
+ });
+
+ describe('with DATE_ONLY_FORMAT', () => {
+ it.each([
+ [new Date().toISOString(), 'Jul 6, 2020'],
+ [new Date(), 'Jul 6, 2020'],
+ [new Date().getTime(), 'Jul 6, 2020'],
+ [null, 'Jan 1, 1970'],
+ ])('formats date `%p` as `%p`', (date, result) => {
+ expect(getTimeago(DATE_ONLY_FORMAT).format(date)).toEqual(result);
+ });
+ });
});
});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 8d989350173..330bfca7029 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -276,19 +276,35 @@ describe('getTimeframeWindowFrom', () => {
});
describe('formatTime', () => {
- const expectedTimestamps = [
- [0, '00:00:00'],
- [1000, '00:00:01'],
- [42000, '00:00:42'],
- [121000, '00:02:01'],
- [10921000, '03:02:01'],
- [108000000, '30:00:00'],
- ];
+ it.each`
+ milliseconds | expected
+ ${0} | ${'00:00:00'}
+ ${1} | ${'00:00:00'}
+ ${499} | ${'00:00:00'}
+ ${500} | ${'00:00:01'}
+ ${1000} | ${'00:00:01'}
+ ${42 * 1000} | ${'00:00:42'}
+ ${60 * 1000} | ${'00:01:00'}
+ ${(60 + 1) * 1000} | ${'00:01:01'}
+ ${(3 * 60 * 60 + 2 * 60 + 1) * 1000} | ${'03:02:01'}
+ ${(11 * 60 * 60 + 59 * 60 + 59) * 1000} | ${'11:59:59'}
+ ${30 * 60 * 60 * 1000} | ${'30:00:00'}
+ ${(35 * 60 * 60 + 3 * 60 + 7) * 1000} | ${'35:03:07'}
+ ${240 * 60 * 60 * 1000} | ${'240:00:00'}
+ ${1000 * 60 * 60 * 1000} | ${'1000:00:00'}
+ `(`formats $milliseconds ms as $expected`, ({ milliseconds, expected }) => {
+ expect(datetimeUtility.formatTime(milliseconds)).toBe(expected);
+ });
- expectedTimestamps.forEach(([milliseconds, expectedTimestamp]) => {
- it(`formats ${milliseconds}ms as ${expectedTimestamp}`, () => {
- expect(datetimeUtility.formatTime(milliseconds)).toBe(expectedTimestamp);
- });
+ it.each`
+ milliseconds | expected
+ ${-1} | ${'00:00:00'}
+ ${-499} | ${'00:00:00'}
+ ${-1000} | ${'-00:00:01'}
+ ${-60 * 1000} | ${'-00:01:00'}
+ ${-(35 * 60 * 60 + 3 * 60 + 7) * 1000} | ${'-35:03:07'}
+ `(`when negative, formats $milliseconds ms as $expected`, ({ milliseconds, expected }) => {
+ expect(datetimeUtility.formatTime(milliseconds)).toBe(expected);
});
});
diff --git a/spec/frontend/lib/utils/error_message_spec.js b/spec/frontend/lib/utils/error_message_spec.js
new file mode 100644
index 00000000000..d55a6de06c3
--- /dev/null
+++ b/spec/frontend/lib/utils/error_message_spec.js
@@ -0,0 +1,48 @@
+import { parseErrorMessage } from '~/lib/utils/error_message';
+
+const defaultErrorMessage = 'Default error message';
+const errorMessage = 'Returned error message';
+
+const generateErrorWithMessage = (message) => {
+ return {
+ message,
+ };
+};
+
+describe('parseErrorMessage', () => {
+ const ufErrorPrefix = 'Foo:';
+ beforeEach(() => {
+ gon.uf_error_prefix = ufErrorPrefix;
+ });
+
+ it.each`
+ error | expectedResult
+ ${`${ufErrorPrefix} ${errorMessage}`} | ${errorMessage}
+ ${`${errorMessage} ${ufErrorPrefix}`} | ${defaultErrorMessage}
+ ${errorMessage} | ${defaultErrorMessage}
+ ${undefined} | ${defaultErrorMessage}
+ ${''} | ${defaultErrorMessage}
+ `(
+ 'properly parses "$error" error object and returns "$expectedResult"',
+ ({ error, expectedResult }) => {
+ const errorObject = generateErrorWithMessage(error);
+ expect(parseErrorMessage(errorObject, defaultErrorMessage)).toEqual(expectedResult);
+ },
+ );
+
+ it.each`
+ error | defaultMessage | expectedResult
+ ${undefined} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${''} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${{}} | ${defaultErrorMessage} | ${defaultErrorMessage}
+ ${generateErrorWithMessage(errorMessage)} | ${undefined} | ${''}
+ ${generateErrorWithMessage(`${ufErrorPrefix} ${errorMessage}`)} | ${undefined} | ${errorMessage}
+ ${generateErrorWithMessage(errorMessage)} | ${''} | ${''}
+ ${generateErrorWithMessage(`${ufErrorPrefix} ${errorMessage}`)} | ${''} | ${errorMessage}
+ `(
+ 'properly handles the edge case of error="$error" and defaultMessage="$defaultMessage"',
+ ({ error, defaultMessage, expectedResult }) => {
+ expect(parseErrorMessage(error, defaultMessage)).toEqual(expectedResult);
+ },
+ );
+});
diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js
index f63af2fe0a4..509ddc7ce86 100644
--- a/spec/frontend/lib/utils/file_upload_spec.js
+++ b/spec/frontend/lib/utils/file_upload_spec.js
@@ -1,5 +1,9 @@
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload';
+import fileUpload, {
+ getFilename,
+ validateImageName,
+ validateFileFromAllowList,
+} from '~/lib/utils/file_upload';
describe('File upload', () => {
beforeEach(() => {
@@ -89,3 +93,19 @@ describe('file name validator', () => {
expect(validateImageName(file)).toBe('image.png');
});
});
+
+describe('validateFileFromAllowList', () => {
+ it('returns true if the file type is in the allowed list', () => {
+ const allowList = ['.foo', '.bar'];
+ const fileName = 'file.foo';
+
+ expect(validateFileFromAllowList(fileName, allowList)).toBe(true);
+ });
+
+ it('returns false if the file type is in the allowed list', () => {
+ const allowList = ['.foo', '.bar'];
+ const fileName = 'file.baz';
+
+ expect(validateFileFromAllowList(fileName, allowList)).toBe(false);
+ });
+});
diff --git a/spec/frontend/lib/utils/intersection_observer_spec.js b/spec/frontend/lib/utils/intersection_observer_spec.js
index 71b1daffe0d..8eef403f0ae 100644
--- a/spec/frontend/lib/utils/intersection_observer_spec.js
+++ b/spec/frontend/lib/utils/intersection_observer_spec.js
@@ -57,7 +57,7 @@ describe('IntersectionObserver Utility', () => {
${true} | ${'IntersectionAppear'}
`(
'should emit the correct event on the entry target based on the computed Intersection',
- async ({ isIntersecting, event }) => {
+ ({ isIntersecting, event }) => {
const target = document.createElement('div');
observer.addEntry({ target, isIntersecting });
diff --git a/spec/frontend/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index dc4aa0ea5ed..d2591cd2328 100644
--- a/spec/frontend/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -3,6 +3,7 @@ import {
bytesToKiB,
bytesToMiB,
bytesToGiB,
+ numberToHumanSizeSplit,
numberToHumanSize,
numberToMetricPrefix,
sum,
@@ -13,6 +14,12 @@ import {
isNumeric,
isPositiveInteger,
} from '~/lib/utils/number_utils';
+import {
+ BYTES_FORMAT_BYTES,
+ BYTES_FORMAT_KIB,
+ BYTES_FORMAT_MIB,
+ BYTES_FORMAT_GIB,
+} from '~/lib/utils/constants';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
@@ -78,6 +85,28 @@ describe('Number Utils', () => {
});
});
+ describe('numberToHumanSizeSplit', () => {
+ it('should return bytes', () => {
+ expect(numberToHumanSizeSplit(654)).toEqual(['654', BYTES_FORMAT_BYTES]);
+ expect(numberToHumanSizeSplit(-654)).toEqual(['-654', BYTES_FORMAT_BYTES]);
+ });
+
+ it('should return KiB', () => {
+ expect(numberToHumanSizeSplit(1079)).toEqual(['1.05', BYTES_FORMAT_KIB]);
+ expect(numberToHumanSizeSplit(-1079)).toEqual(['-1.05', BYTES_FORMAT_KIB]);
+ });
+
+ it('should return MiB', () => {
+ expect(numberToHumanSizeSplit(10485764)).toEqual(['10.00', BYTES_FORMAT_MIB]);
+ expect(numberToHumanSizeSplit(-10485764)).toEqual(['-10.00', BYTES_FORMAT_MIB]);
+ });
+
+ it('should return GiB', () => {
+ expect(numberToHumanSizeSplit(10737418240)).toEqual(['10.00', BYTES_FORMAT_GIB]);
+ expect(numberToHumanSizeSplit(-10737418240)).toEqual(['-10.00', BYTES_FORMAT_GIB]);
+ });
+ });
+
describe('numberToHumanSize', () => {
it('should return bytes', () => {
expect(numberToHumanSize(654)).toEqual('654 bytes');
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index 63eeb54e850..096a92305dc 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -121,7 +121,7 @@ describe('Poll', () => {
});
describe('with delayed initial request', () => {
- it('delays the first request', async () => {
+ it('delays the first request', () => {
mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
diff --git a/spec/frontend/lib/utils/ref_validator_spec.js b/spec/frontend/lib/utils/ref_validator_spec.js
new file mode 100644
index 00000000000..7185ebf0a24
--- /dev/null
+++ b/spec/frontend/lib/utils/ref_validator_spec.js
@@ -0,0 +1,79 @@
+import { validateTag, validationMessages } from '~/lib/utils/ref_validator';
+
+describe('~/lib/utils/ref_validator', () => {
+ describe('validateTag', () => {
+ describe.each([
+ ['foo'],
+ ['FOO'],
+ ['foo/a.lockx'],
+ ['foo.123'],
+ ['foo/123'],
+ ['foo/bar/123'],
+ ['foo.bar.123'],
+ ['foo-bar_baz'],
+ ['head'],
+ ['"foo"-'],
+ ['foo@bar'],
+ ['\ud83e\udd8a'],
+ ['ünicöde'],
+ ['\x80}'],
+ ])('tag with the name "%s"', (tagName) => {
+ it('is valid', () => {
+ const result = validateTag(tagName);
+ expect(result.isValid).toBe(true);
+ expect(result.validationErrors).toEqual([]);
+ });
+ });
+
+ describe.each([
+ [' ', validationMessages.EmptyNameValidationMessage],
+
+ ['refs/heads/tagName', validationMessages.DisallowedPrefixesValidationMessage],
+ ['/foo', validationMessages.DisallowedPrefixesValidationMessage],
+ ['-tagName', validationMessages.DisallowedPrefixesValidationMessage],
+
+ ['HEAD', validationMessages.DisallowedNameValidationMessage],
+ ['@', validationMessages.DisallowedNameValidationMessage],
+
+ ['tag name with spaces', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag\\name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag^name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag..name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['..', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag?name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag*name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag[name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag@{name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag:name', validationMessages.DisallowedSubstringsValidationMessage],
+ ['tag~name', validationMessages.DisallowedSubstringsValidationMessage],
+
+ ['/', validationMessages.DisallowedSequenceEmptyValidationMessage],
+ ['//', validationMessages.DisallowedSequenceEmptyValidationMessage],
+ ['foo//123', validationMessages.DisallowedSequenceEmptyValidationMessage],
+
+ ['.', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['/./', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['./.', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['.tagName', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['tag/.Name', validationMessages.DisallowedSequencePrefixesValidationMessage],
+ ['foo/.123/bar', validationMessages.DisallowedSequencePrefixesValidationMessage],
+
+ ['foo.', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['a.lock', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo/a.lock', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo/a.lock/b', validationMessages.DisallowedSequencePostfixesValidationMessage],
+ ['foo.123.', validationMessages.DisallowedSequencePostfixesValidationMessage],
+
+ ['foo/', validationMessages.DisallowedPostfixesValidationMessage],
+
+ ['control-character\x7f', validationMessages.ControlCharactersValidationMessage],
+ ['control-character\x15', validationMessages.ControlCharactersValidationMessage],
+ ])('tag with name "%s"', (tagName, validationMessage) => {
+ it(`should be invalid with validation message "${validationMessage}"`, () => {
+ const result = validateTag(tagName);
+ expect(result.isValid).toBe(false);
+ expect(result.validationErrors).toContain(validationMessage);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/secret_detection_spec.js b/spec/frontend/lib/utils/secret_detection_spec.js
new file mode 100644
index 00000000000..7bde6cc4a8e
--- /dev/null
+++ b/spec/frontend/lib/utils/secret_detection_spec.js
@@ -0,0 +1,68 @@
+import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
+const mockConfirmAction = ({ confirmed }) => {
+ confirmAction.mockResolvedValueOnce(confirmed);
+};
+
+describe('containsSensitiveToken', () => {
+ describe('when message does not contain sensitive tokens', () => {
+ const nonSensitiveMessages = [
+ 'This is a normal message',
+ '1234567890',
+ '!@#$%^&*()_+',
+ 'https://example.com',
+ ];
+
+ it.each(nonSensitiveMessages)('returns false for message: %s', (message) => {
+ expect(containsSensitiveToken(message)).toBe(false);
+ });
+ });
+
+ describe('when message contains sensitive tokens', () => {
+ const sensitiveMessages = [
+ 'token: glpat-cgyKc1k_AsnEpmP-5fRL',
+ 'token: GlPat-abcdefghijklmnopqrstuvwxyz',
+ 'token: feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ 'https://example.com/feed?feed_token=123456789_abcdefghij',
+ 'glpat-1234567890 and feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ ];
+
+ it.each(sensitiveMessages)('returns true for message: %s', (message) => {
+ expect(containsSensitiveToken(message)).toBe(true);
+ });
+ });
+});
+
+describe('confirmSensitiveAction', () => {
+ afterEach(() => {
+ confirmAction.mockReset();
+ });
+
+ it('should call confirmAction with correct parameters', async () => {
+ const prompt = 'Are you sure you want to delete this item?';
+ const expectedParams = {
+ primaryBtnVariant: 'danger',
+ primaryBtnText: i18n.primaryBtnText,
+ };
+ await confirmSensitiveAction(prompt);
+
+ expect(confirmAction).toHaveBeenCalledWith(prompt, expectedParams);
+ });
+
+ it('should return true when confirmed is true', async () => {
+ mockConfirmAction({ confirmed: true });
+
+ const result = await confirmSensitiveAction();
+ expect(result).toBe(true);
+ });
+
+ it('should return false when confirmed is false', async () => {
+ mockConfirmAction({ confirmed: false });
+
+ const result = await confirmSensitiveAction();
+ expect(result).toBe(false);
+ });
+});
diff --git a/spec/frontend/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js
deleted file mode 100644
index ec9e746c838..00000000000
--- a/spec/frontend/lib/utils/sticky_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { setHTMLFixture } from 'helpers/fixtures';
-import { isSticky } from '~/lib/utils/sticky';
-
-const TEST_OFFSET_TOP = 500;
-
-describe('sticky', () => {
- let el;
- let offsetTop;
-
- beforeEach(() => {
- setHTMLFixture(
- `
- <div class="parent">
- <div id="js-sticky"></div>
- </div>
- `,
- );
-
- offsetTop = TEST_OFFSET_TOP;
- el = document.getElementById('js-sticky');
- Object.defineProperty(el, 'offsetTop', {
- get() {
- return offsetTop;
- },
- });
- });
-
- afterEach(() => {
- el = null;
- });
-
- describe('when stuck', () => {
- it('does not remove is-stuck class', () => {
- isSticky(el, 0, el.offsetTop);
- isSticky(el, 0, el.offsetTop);
-
- expect(el.classList.contains('is-stuck')).toBe(true);
- });
-
- it('adds is-stuck class', () => {
- isSticky(el, 0, el.offsetTop);
-
- expect(el.classList.contains('is-stuck')).toBe(true);
- });
-
- it('inserts placeholder element', () => {
- isSticky(el, 0, el.offsetTop, true);
-
- expect(document.querySelector('.sticky-placeholder')).not.toBeNull();
- });
- });
-
- describe('when not stuck', () => {
- it('removes is-stuck class', () => {
- jest.spyOn(el.classList, 'remove');
-
- isSticky(el, 0, el.offsetTop);
- isSticky(el, 0, 0);
-
- expect(el.classList.remove).toHaveBeenCalledWith('is-stuck');
- expect(el.classList.contains('is-stuck')).toBe(false);
- });
-
- it('does not add is-stuck class', () => {
- isSticky(el, 0, 0);
-
- expect(el.classList.contains('is-stuck')).toBe(false);
- });
-
- it('removes placeholder', () => {
- isSticky(el, 0, el.offsetTop, true);
- isSticky(el, 0, 0, true);
-
- expect(document.querySelector('.sticky-placeholder')).toBeNull();
- });
- });
-});
diff --git a/spec/frontend/lib/utils/tappable_promise_spec.js b/spec/frontend/lib/utils/tappable_promise_spec.js
new file mode 100644
index 00000000000..654cd20a9de
--- /dev/null
+++ b/spec/frontend/lib/utils/tappable_promise_spec.js
@@ -0,0 +1,63 @@
+import TappablePromise from '~/lib/utils/tappable_promise';
+
+describe('TappablePromise', () => {
+ it('allows a promise to have a progress indicator', () => {
+ const pseudoProgress = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1];
+ const progressCallback = jest.fn();
+ const promise = new TappablePromise((tap, resolve) => {
+ pseudoProgress.forEach(tap);
+ resolve('done');
+
+ return 'returned value';
+ });
+
+ return promise
+ .tap(progressCallback)
+ .then((val) => {
+ expect(val).toBe('done');
+ expect(val).not.toBe('returned value');
+
+ expect(progressCallback).toHaveBeenCalledTimes(pseudoProgress.length);
+
+ pseudoProgress.forEach((progress, index) => {
+ expect(progressCallback).toHaveBeenNthCalledWith(index + 1, progress);
+ });
+ })
+ .catch(() => {});
+ });
+
+ it('resolves with the value returned by the callback', () => {
+ const promise = new TappablePromise((tap) => {
+ tap(0.5);
+ return 'test';
+ });
+
+ return promise
+ .tap((progress) => {
+ expect(progress).toBe(0.5);
+ })
+ .then((value) => {
+ expect(value).toBe('test');
+ });
+ });
+
+ it('allows a promise to be rejected', () => {
+ const promise = new TappablePromise((tap, resolve, reject) => {
+ reject(new Error('test error'));
+ });
+
+ return promise.catch((e) => {
+ expect(e.message).toBe('test error');
+ });
+ });
+
+ it('rejects the promise if the callback throws an error', () => {
+ const promise = new TappablePromise(() => {
+ throw new Error('test error');
+ });
+
+ return promise.catch((e) => {
+ expect(e.message).toBe('test error');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 7aab1013fc0..2180ea7e6c2 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -1,12 +1,16 @@
import $ from 'jquery';
+import AxiosMockAdapter from 'axios-mock-adapter';
import {
insertMarkdownText,
keypressNoteText,
compositionStartNoteText,
compositionEndNoteText,
updateTextForToolbarBtn,
+ resolveSelectedImage,
} from '~/lib/utils/text_markdown';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import '~/lib/utils/jquery_at_who';
+import axios from '~/lib/utils/axios_utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('init markdown', () => {
@@ -14,6 +18,7 @@ describe('init markdown', () => {
let textArea;
let indentButton;
let outdentButton;
+ let axiosMock;
beforeAll(() => {
setHTMLFixture(
@@ -34,6 +39,14 @@ describe('init markdown', () => {
document.execCommand = jest.fn(() => false);
});
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
afterAll(() => {
resetHTMLFixture();
});
@@ -707,6 +720,55 @@ describe('init markdown', () => {
});
});
+ describe('resolveSelectedImage', () => {
+ const markdownPreviewPath = '/markdown/preview';
+ const imageMarkdown = '![image](/uploads/image.png)';
+ const imageAbsoluteUrl = '/abs/uploads/image.png';
+
+ describe('when textarea cursor is positioned on an image', () => {
+ beforeEach(() => {
+ axiosMock.onPost(markdownPreviewPath, { text: imageMarkdown }).reply(HTTP_STATUS_OK, {
+ body: `
+ <p><a href="${imageAbsoluteUrl}"><img src="${imageAbsoluteUrl}"></a></p>
+ `,
+ });
+ });
+
+ it('returns the image absolute URL, markdown, and filename', async () => {
+ textArea.value = `image ${imageMarkdown}`;
+ textArea.setSelectionRange(8, 8);
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toEqual({
+ imageURL: imageAbsoluteUrl,
+ imageMarkdown,
+ filename: 'image.png',
+ });
+ });
+ });
+
+ describe('when textarea cursor is not positioned on an image', () => {
+ it.each`
+ markdown | selectionRange
+ ${`image ${imageMarkdown}`} | ${[4, 4]}
+ ${`!2 (issue)`} | ${[2, 2]}
+ `('returns null', async ({ markdown, selectionRange }) => {
+ textArea.value = markdown;
+ textArea.setSelectionRange(...selectionRange);
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null);
+ });
+ });
+
+ describe('when textarea cursor is positioned between images', () => {
+ it('returns null', async () => {
+ const position = imageMarkdown.length + 1;
+
+ textArea.value = `${imageMarkdown}\n\n${imageMarkdown}`;
+ textArea.setSelectionRange(position, position);
+
+ expect(await resolveSelectedImage(textArea, markdownPreviewPath)).toBe(null);
+ });
+ });
+ });
+
describe('Source Editor', () => {
let editor;
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index f2572ca0ad2..71a84d56791 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -398,4 +398,36 @@ describe('text_utility', () => {
expect(textUtils.base64DecodeUnicode('8J+YgA==')).toBe('😀');
});
});
+
+ describe('findInvalidBranchNameCharacters', () => {
+ const invalidChars = [' ', '~', '^', ':', '?', '*', '[', '..', '@{', '\\', '//'];
+ const badBranchName = 'branch-with all these ~ ^ : ? * [ ] \\ // .. @{ } //';
+ const goodBranch = 'branch-with-no-errrors';
+
+ it('returns an array of invalid characters in a branch name', () => {
+ const chars = textUtils.findInvalidBranchNameCharacters(badBranchName);
+ chars.forEach((char) => {
+ expect(invalidChars).toContain(char);
+ });
+ });
+
+ it('returns an empty array with no invalid characters', () => {
+ expect(textUtils.findInvalidBranchNameCharacters(goodBranch)).toEqual([]);
+ });
+ });
+
+ describe('humanizeBranchValidationErrors', () => {
+ it.each`
+ errors | message
+ ${[' ']} | ${"Can't contain spaces"}
+ ${['?', '//', ' ']} | ${"Can't contain spaces, ?, //"}
+ ${['\\', '[', '..']} | ${"Can't contain \\, [, .."}
+ `('returns an $message with $errors', ({ errors, message }) => {
+ expect(textUtils.humanizeBranchValidationErrors(errors)).toEqual(message);
+ });
+
+ it('returns an empty string with no invalid characters', () => {
+ expect(textUtils.humanizeBranchValidationErrors([])).toEqual('');
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 6afdab455a6..0799bc87c8c 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -45,10 +45,6 @@ describe('URL utility', () => {
});
describe('webIDEUrl', () => {
- afterEach(() => {
- gon.relative_url_root = '';
- });
-
it('escapes special characters', () => {
expect(urlUtils.webIDEUrl('/gitlab-org/gitlab-#-foss/merge_requests/1')).toBe(
'/-/ide/project/gitlab-org/gitlab-%23-foss/merge_requests/1',
@@ -505,10 +501,6 @@ describe('URL utility', () => {
gon.gitlab_url = gitlabUrl;
});
- afterEach(() => {
- gon.gitlab_url = '';
- });
-
it.each`
url | urlType | external
${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false}
@@ -796,18 +788,6 @@ describe('URL utility', () => {
});
});
- describe('stripFinalUrlSegment', () => {
- it.each`
- path | expected
- ${'http://fake.domain/twitter/typeahead-js/-/tags/v0.11.0'} | ${'http://fake.domain/twitter/typeahead-js/-/tags/'}
- ${'http://fake.domain/bar/cool/-/nested/content'} | ${'http://fake.domain/bar/cool/-/nested/'}
- ${'http://fake.domain/bar/cool?q="search"'} | ${'http://fake.domain/bar/'}
- ${'http://fake.domain/bar/cool#link-to-something'} | ${'http://fake.domain/bar/'}
- `('stripFinalUrlSegment $path => $expected', ({ path, expected }) => {
- expect(urlUtils.stripFinalUrlSegment(path)).toBe(expected);
- });
- });
-
describe('escapeFileUrl', () => {
it('encodes URL excluding the slashes', () => {
expect(urlUtils.escapeFileUrl('/foo-bar/file.md')).toBe('/foo-bar/file.md');
diff --git a/spec/frontend/lib/utils/vuex_module_mappers_spec.js b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
index d25a692dfea..abd5095c1d2 100644
--- a/spec/frontend/lib/utils/vuex_module_mappers_spec.js
+++ b/spec/frontend/lib/utils/vuex_module_mappers_spec.js
@@ -96,10 +96,6 @@ describe('~/lib/utils/vuex_module_mappers', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('from module defined by prop', () => {
it('maps state', () => {
expect(getMappedState()).toEqual({
diff --git a/spec/frontend/lib/utils/web_ide_navigator_spec.js b/spec/frontend/lib/utils/web_ide_navigator_spec.js
new file mode 100644
index 00000000000..0f5cd09d50e
--- /dev/null
+++ b/spec/frontend/lib/utils/web_ide_navigator_spec.js
@@ -0,0 +1,38 @@
+import { visitUrl, webIDEUrl } from '~/lib/utils/url_utility';
+import { openWebIDE } from '~/lib/utils/web_ide_navigator';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ webIDEUrl: jest.fn().mockImplementation((path) => `/-/ide/projects${path}`),
+}));
+
+describe('openWebIDE', () => {
+ it('when called without projectPath throws TypeError and does not call visitUrl', () => {
+ expect(() => {
+ openWebIDE();
+ }).toThrow(new TypeError('projectPath parameter is required'));
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+
+ it('when called with projectPath and without fileName calls visitUrl with correct path', () => {
+ const params = { projectPath: 'project-path' };
+ const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/`;
+ const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
+
+ openWebIDE(params.projectPath);
+
+ expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
+ expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
+ });
+
+ it('when called with projectPath and fileName calls visitUrl with correct path', () => {
+ const params = { projectPath: 'project-path', fileName: 'README' };
+ const expectedNonIDEPath = `/${params.projectPath}/edit/main/-/${params.fileName}/`;
+ const expectedIDEPath = `/-/ide/projects${expectedNonIDEPath}`;
+
+ openWebIDE(params.projectPath, params.fileName);
+
+ expect(webIDEUrl).toHaveBeenCalledWith(expectedNonIDEPath);
+ expect(visitUrl).toHaveBeenCalledWith(expectedIDEPath);
+ });
+});
diff --git a/spec/frontend/listbox/redirect_behavior_spec.js b/spec/frontend/listbox/redirect_behavior_spec.js
index 7b2a40b65ce..c2479e71e4a 100644
--- a/spec/frontend/listbox/redirect_behavior_spec.js
+++ b/spec/frontend/listbox/redirect_behavior_spec.js
@@ -1,6 +1,6 @@
import { initListbox } from '~/listbox';
import { initRedirectListboxBehavior } from '~/listbox/redirect_behavior';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { getFixture, setHTMLFixture } from 'helpers/fixtures';
jest.mock('~/lib/utils/url_utility');
@@ -42,10 +42,10 @@ describe('initRedirectListboxBehavior', () => {
const { onChange } = firstCallArgs[1];
const mockItem = { href: '/foo' };
- expect(redirectTo).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
onChange(mockItem);
- expect(redirectTo).toHaveBeenCalledWith(mockItem.href);
+ expect(redirectTo).toHaveBeenCalledWith(mockItem.href); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/locale/sprintf_spec.js b/spec/frontend/locale/sprintf_spec.js
index e0d0e117ea4..a7e245e2b78 100644
--- a/spec/frontend/locale/sprintf_spec.js
+++ b/spec/frontend/locale/sprintf_spec.js
@@ -84,5 +84,16 @@ describe('locale', () => {
expect(output).toBe('contains duplicated 15%');
});
});
+
+ describe('ignores special replacements in the input', () => {
+ it.each(['$$', '$&', '$`', `$'`])('replacement "%s" is ignored', (replacement) => {
+ const input = 'My odd %{replacement} is preserved';
+
+ const parameters = { replacement };
+
+ const output = sprintf(input, parameters, false);
+ expect(output).toBe(`My odd ${replacement} is preserved`);
+ });
+ });
});
});
diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index b94964dc482..c2e0e44f97f 100644
--- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -20,10 +20,6 @@ describe('AccessRequestActionButtons', () => {
const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
const findApproveButton = () => wrapper.findComponent(ApproveAccessRequestButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders remove member button', () => {
createComponent();
diff --git a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
index 15bb03480e1..7a4cd844425 100644
--- a/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/approve_access_request_button_spec.js
@@ -38,7 +38,7 @@ describe('ApproveAccessRequestButton', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -50,10 +50,6 @@ describe('ApproveAccessRequestButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a tooltip', () => {
const button = findButton();
diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
index 68009708c99..a852443844b 100644
--- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
@@ -19,10 +19,6 @@ describe('InviteActionButtons', () => {
const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
const findResendInviteButton = () => wrapper.findComponent(ResendInviteButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when user has `canRemove` permissions', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
index b511cebdf28..1d83a2e0e71 100644
--- a/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_group_link_button_spec.js
@@ -37,7 +37,7 @@ describe('RemoveGroupLinkButton', () => {
groupLink: group,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -48,11 +48,6 @@ describe('RemoveGroupLinkButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('displays a tooltip', () => {
const button = findButton();
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index cca340169b7..3879279b559 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -47,7 +47,7 @@ describe('RemoveMemberButton', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -58,10 +58,6 @@ describe('RemoveMemberButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('sets attributes on button', () => {
expect(wrapper.attributes()).toMatchObject({
'aria-label': 'Remove member',
diff --git a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
index 51cfd47ddf4..a6b5978b566 100644
--- a/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/resend_invite_button_spec.js
@@ -38,7 +38,7 @@ describe('ResendInviteButton', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -50,10 +50,6 @@ describe('ResendInviteButton', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a tooltip', () => {
expect(getBinding(findButton().element, 'gl-tooltip')).not.toBeUndefined();
expect(findButton().attributes('title')).toBe('Resend invite');
diff --git a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
index 90f5b217007..679ad7897ed 100644
--- a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
+++ b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
@@ -18,7 +18,7 @@ describe('LeaveGroupDropdownItem', () => {
...propsData,
},
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
slots: {
default: text,
@@ -32,10 +32,6 @@ describe('LeaveGroupDropdownItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a slot with red text', () => {
expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
});
diff --git a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
index e1c498249d7..125f1f8fff3 100644
--- a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
+++ b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
@@ -58,10 +58,6 @@ describe('RemoveMemberDropdownItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a slot with red text', () => {
expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
});
diff --git a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
index 5a2de1cac80..448c04bcb69 100644
--- a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
+++ b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
@@ -24,17 +24,13 @@ describe('UserActionDropdown', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
const findRemoveMemberDropdownItem = () => wrapper.findComponent(RemoveMemberDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when user has `canRemove` permissions', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index d105a4d9fde..b2147163233 100644
--- a/spec/frontend/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -49,7 +49,6 @@ describe('MembersApp', () => {
});
afterEach(() => {
- wrapper.destroy();
store = null;
});
diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index 13c50de9835..8e4263f88fe 100644
--- a/spec/frontend/members/components/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -25,10 +25,6 @@ describe('MemberList', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders link to group', () => {
const link = wrapper.findComponent(GlAvatarLink);
diff --git a/spec/frontend/members/components/avatars/invite_avatar_spec.js b/spec/frontend/members/components/avatars/invite_avatar_spec.js
index b197a46c0d1..84878fb9be2 100644
--- a/spec/frontend/members/components/avatars/invite_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/invite_avatar_spec.js
@@ -24,10 +24,6 @@ describe('MemberList', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders email as name', () => {
expect(getByText(invite.email).exists()).toBe(true);
});
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 9172876e76f..4808bcb9363 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -26,10 +26,6 @@ describe('UserAvatar', () => {
const findStatusEmoji = (emoji) => wrapper.find(`gl-emoji[data-name="${emoji}"]`);
- afterEach(() => {
- wrapper.destroy();
- });
-
it("renders link to user's profile", () => {
createComponent();
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index f346967121c..29b7ceae0e3 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import {
MEMBER_TYPES,
@@ -167,7 +167,7 @@ describe('MembersFilteredSearchBar', () => {
{ type: FILTERED_SEARCH_TOKEN_TWO_FACTOR.type, value: { data: 'enabled', operator: '=' } },
]);
- expect(redirectTo).toHaveBeenCalledWith('https://localhost/?two_factor=enabled');
+ expect(redirectTo).toHaveBeenCalledWith('https://localhost/?two_factor=enabled'); // eslint-disable-line import/no-deprecated
});
it('adds search query param', () => {
@@ -178,6 +178,7 @@ describe('MembersFilteredSearchBar', () => {
{ type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
+ // eslint-disable-next-line import/no-deprecated
expect(redirectTo).toHaveBeenCalledWith(
'https://localhost/?two_factor=enabled&search=foobar',
);
@@ -191,6 +192,7 @@ describe('MembersFilteredSearchBar', () => {
{ type: FILTERED_SEARCH_TERM, value: { data: 'foo bar baz' } },
]);
+ // eslint-disable-next-line import/no-deprecated
expect(redirectTo).toHaveBeenCalledWith(
'https://localhost/?two_factor=enabled&search=foo+bar+baz',
);
@@ -206,6 +208,7 @@ describe('MembersFilteredSearchBar', () => {
{ type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
+ // eslint-disable-next-line import/no-deprecated
expect(redirectTo).toHaveBeenCalledWith(
'https://localhost/?two_factor=enabled&search=foobar&sort=name_asc',
);
@@ -220,7 +223,7 @@ describe('MembersFilteredSearchBar', () => {
{ type: FILTERED_SEARCH_TERM, value: { data: 'foobar' } },
]);
- expect(redirectTo).toHaveBeenCalledWith('https://localhost/?search=foobar&tab=invited');
+ expect(redirectTo).toHaveBeenCalledWith('https://localhost/?search=foobar&tab=invited'); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
index ef3c8bde3cf..526f839ece8 100644
--- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -46,7 +46,7 @@ describe('SortDropdown', () => {
const findSortingComponent = () => wrapper.findComponent(GlSorting);
const findSortDirectionToggle = () =>
findSortingComponent().find('button[title^="Sort direction"]');
- const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
+ const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]');
const findDropdownItemByText = (text) =>
wrapper
.findAllComponents(GlSortingItem)
diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js
index 77af5e7293e..9078bd87d62 100644
--- a/spec/frontend/members/components/members_tabs_spec.js
+++ b/spec/frontend/members/components/members_tabs_spec.js
@@ -100,10 +100,6 @@ describe('MembersTabs', () => {
setWindowLocation('https://localhost');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders `GlTabs` with `syncActiveTabWithQueryParams` and `queryParamName` props set', async () => {
await createComponent();
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index ba587c6f0b3..cec5f192e59 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -60,10 +60,6 @@ describe('LeaveModal', () => {
const findForm = () => findModal().findComponent(GlForm);
const findUserDeletionObstaclesList = () => findModal().findComponent(UserDeletionObstaclesList);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('sets modal ID', async () => {
await createComponent();
diff --git a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
index af96396f09f..e4782ac7f2e 100644
--- a/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_group_link_modal_spec.js
@@ -52,11 +52,6 @@ describe('RemoveGroupLinkModal', () => {
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when modal is open', () => {
beforeEach(async () => {
createComponent();
diff --git a/spec/frontend/members/components/modals/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js
index 47a03b5083a..baef0b30b02 100644
--- a/spec/frontend/members/components/modals/remove_member_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js
@@ -54,10 +54,6 @@ describe('RemoveMemberModal', () => {
const findGlModal = () => wrapper.findComponent(GlModal);
const findUserDeletionObstaclesList = () => wrapper.findComponent(UserDeletionObstaclesList);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
state | memberModelType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall
${'removing a group member'} | ${MEMBER_MODEL_TYPE_GROUP_MEMBER} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false}
diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js
index fa31177564b..2c0493e7c59 100644
--- a/spec/frontend/members/components/table/created_at_spec.js
+++ b/spec/frontend/members/components/table/created_at_spec.js
@@ -20,10 +20,6 @@ describe('CreatedAt', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('created at text', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/members/components/table/expiration_datepicker_spec.js b/spec/frontend/members/components/table/expiration_datepicker_spec.js
index 9b8f053348b..9176a02a447 100644
--- a/spec/frontend/members/components/table/expiration_datepicker_spec.js
+++ b/spec/frontend/members/components/table/expiration_datepicker_spec.js
@@ -58,10 +58,6 @@ describe('ExpirationDatepicker', () => {
const findInput = () => wrapper.find('input');
const findDatepicker = () => wrapper.findComponent(GlDatepicker);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('datepicker input', () => {
it('sets `member.expiresAt` as initial date', async () => {
createComponent({ member: { ...member, expiresAt: '2020-03-17T00:00:00Z' } });
@@ -97,7 +93,7 @@ describe('ExpirationDatepicker', () => {
});
describe('when datepicker is changed', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
findDatepicker().vm.$emit('input', new Date('2020-03-17'));
diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index 95db30a3683..3a04d1dcb0a 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -23,10 +23,6 @@ describe('MemberActions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
memberType | member | expectedComponent | expectedComponentName
${MEMBER_TYPES.user} | ${memberMock} | ${UserActionDropdown} | ${'UserActionDropdown'}
diff --git a/spec/frontend/members/components/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js
index dc5c97f41df..369f8a06cfd 100644
--- a/spec/frontend/members/components/table/member_avatar_spec.js
+++ b/spec/frontend/members/components/table/member_avatar_spec.js
@@ -18,10 +18,6 @@ describe('MemberList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
memberType | member | expectedComponent | expectedComponentName
${MEMBER_TYPES.user} | ${memberMock} | ${UserAvatar} | ${'UserAvatar'}
diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index fbfd0ca7ae7..bbfbb19fd92 100644
--- a/spec/frontend/members/components/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -23,17 +23,13 @@ describe('MemberSource', () => {
...propsData,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('direct member', () => {
describe('when created by is available', () => {
it('displays "Direct member by <user name>"', () => {
diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js
index ac5d83d028d..1c6f1b086cf 100644
--- a/spec/frontend/members/components/table/members_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -97,11 +97,6 @@ describe('MembersTableCell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
member | expectedMemberType
${memberMock} | ${MEMBER_TYPES.user}
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index b8e0d73d8f6..e3c89bfed53 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -96,10 +96,6 @@ describe('MembersTable', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('fields', () => {
const memberCanUpdate = {
...directMember,
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index a11f67be8f5..1045e3f9849 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -1,5 +1,6 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import * as Sentry from '@sentry/browser';
import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -12,6 +13,7 @@ import { member } from '../../mock_data';
Vue.use(Vuex);
jest.mock('ee_else_ce/members/guest_overage_confirm_action');
+jest.mock('@sentry/browser');
describe('RoleDropdown', () => {
let wrapper;
@@ -20,9 +22,9 @@ describe('RoleDropdown', () => {
show: jest.fn(),
};
- const createStore = () => {
+ const createStore = ({ updateMemberRoleReturn = Promise.resolve() } = {}) => {
actions = {
- updateMemberRole: jest.fn(() => Promise.resolve()),
+ updateMemberRole: jest.fn(() => updateMemberRoleReturn),
};
return new Vuex.Store({
@@ -32,7 +34,7 @@ describe('RoleDropdown', () => {
});
};
- const createComponent = (propsData = {}) => {
+ const createComponent = (propsData = {}, store = createStore()) => {
wrapper = mount(RoleDropdown, {
provide: {
namespace: MEMBER_TYPES.user,
@@ -46,7 +48,7 @@ describe('RoleDropdown', () => {
permissions: {},
...propsData,
},
- store: createStore(),
+ store,
mocks: {
$toast,
},
@@ -67,27 +69,19 @@ describe('RoleDropdown', () => {
.findAllComponents(GlDropdownItem)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('isChecked'));
- const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
+ const findDropdownToggle = () => wrapper.find('button[aria-haspopup="menu"]');
const findDropdown = () => wrapper.findComponent(GlDropdown);
- let originalGon;
-
beforeEach(() => {
- originalGon = window.gon;
gon.features = { showOverageOnRolePromotion: true };
});
- afterEach(() => {
- window.gon = originalGon;
- wrapper.destroy();
- });
-
describe('when dropdown is open', () => {
- beforeEach(() => {
+ beforeEach(async () => {
guestOverageConfirmAction.mockReturnValue(true);
createComponent();
- return findDropdownToggle().trigger('click');
+ await findDropdownToggle().trigger('click');
});
it('renders all valid roles', () => {
@@ -121,26 +115,74 @@ describe('RoleDropdown', () => {
});
});
- it('displays toast when successful', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ describe('when updateMemberRole is successful', () => {
+ it('displays toast', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
- await nextTick();
+ await nextTick();
- expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
- });
+ expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
+ });
- it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
- expect(findDropdown().props('loading')).toBe(true);
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+
+ it('enables dropdown after `updateMemberRole` resolves', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await waitForPromises();
+
+ expect(findDropdown().props('disabled')).toBe(false);
+ });
+
+ it('does not log error to Sentry', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await waitForPromises();
+
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
});
- it('enables dropdown after `updateMemberRole` resolves', async () => {
- await getDropdownItemByText('Developer').trigger('click');
+ describe('when updateMemberRole is not successful', () => {
+ const reason = 'Rejected ☹️';
+
+ beforeEach(() => {
+ createComponent({}, createStore({ updateMemberRoleReturn: Promise.reject(reason) }));
+ });
+
+ it('does not display toast', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await nextTick();
+
+ expect($toast.show).not.toHaveBeenCalled();
+ });
+
+ it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
- await waitForPromises();
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+
+ it('enables dropdown after `updateMemberRole` resolves', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
+
+ await waitForPromises();
+
+ expect(findDropdown().props('disabled')).toBe(false);
+ });
+
+ it('logs error to Sentry', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
- expect(findDropdown().props('disabled')).toBe(false);
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(reason);
+ });
});
});
});
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index 5c813eb2a67..b1730cf3746 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -31,9 +31,6 @@ describe('initMembersApp', () => {
afterEach(() => {
el = null;
-
- wrapper.destroy();
- wrapper = null;
});
it('renders `MembersTabs`', () => {
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 4f276e8c9df..c4357e9c1f0 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -213,7 +213,7 @@ describe('Members Utils', () => {
${'recent_sign_in'} | ${{ sortByKey: 'lastSignIn', sortDesc: false }}
${'oldest_sign_in'} | ${{ sortByKey: 'lastSignIn', sortDesc: true }}
`('when `sort` query string param is `$sortParam`', ({ sortParam, expected }) => {
- it(`returns ${JSON.stringify(expected)}`, async () => {
+ it(`returns ${JSON.stringify(expected)}`, () => {
setWindowLocation(`?sort=${sortParam}`);
expect(parseSortParam(['account', 'granted', 'expires', 'maxRole', 'lastSignIn'])).toEqual(
diff --git a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
index 9b5641ef7b3..ab913b30f3c 100644
--- a/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
+++ b/spec/frontend/merge_conflicts/components/merge_conflict_resolver_app_spec.js
@@ -37,10 +37,6 @@ describe('Merge Conflict Resolver App', () => {
store.dispatch('setConflictsData', conflictsMock);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findLoadingSpinner = () => wrapper.findByTestId('loading-spinner');
const findConflictsCount = () => wrapper.findByTestId('conflicts-count');
const findFiles = () => wrapper.findAllByTestId('files');
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index 19ef4b7db25..d2c4c8b796c 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -4,13 +4,13 @@ import Cookies from '~/lib/utils/cookies';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { INTERACTIVE_RESOLVE_MODE, EDIT_RESOLVE_MODE } from '~/merge_conflicts/constants';
import * as actions from '~/merge_conflicts/store/actions';
import * as types from '~/merge_conflicts/store/mutation_types';
import { restoreFileLinesState, markLine, decorateFiles } from '~/merge_conflicts/utils';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/merge_conflicts/utils');
jest.mock('~/lib/utils/cookies');
@@ -114,7 +114,7 @@ describe('merge conflicts actions', () => {
expect(window.location.assign).toHaveBeenCalledWith('hrefPath');
});
- it('on errors shows flash', async () => {
+ it('on errors shows an alert', async () => {
mock.onPost(resolveConflictsPath).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.submitResolvedConflicts,
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index 579cee8c022..6f80f8e6aab 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -1,14 +1,16 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlMergeRequestWithTaskList from 'test_fixtures/merge_requests/merge_request_with_task_list.html';
+import htmlMergeRequestOfCurrentUser from 'test_fixtures/merge_requests/merge_request_of_current_user.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_CONFLICT, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MergeRequest from '~/merge_request';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('MergeRequest', () => {
const test = {};
@@ -16,7 +18,7 @@ describe('MergeRequest', () => {
let mock;
beforeEach(() => {
- loadHTMLFixture('merge_requests/merge_request_with_task_list.html');
+ setHTMLFixture(htmlMergeRequestWithTaskList);
jest.spyOn(axios, 'patch');
mock = new MockAdapter(axios);
@@ -112,7 +114,7 @@ describe('MergeRequest', () => {
describe('hideCloseButton', () => {
describe('merge request of current_user', () => {
beforeEach(() => {
- loadHTMLFixture('merge_requests/merge_request_of_current_user.html');
+ setHTMLFixture(htmlMergeRequestOfCurrentUser);
test.el = document.querySelector('.js-issuable-actions');
MergeRequest.hideCloseButton();
});
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 6d434d7e654..3b8c9dd3bf3 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlMergeRequestsWithTaskList from 'test_fixtures/merge_requests/merge_request_with_task_list.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initMrPage from 'helpers/init_vue_mr_page_helper';
import { stubPerformanceWebAPI } from 'helpers/performance';
import axios from '~/lib/utils/axios_utils';
@@ -40,6 +41,10 @@ describe('MergeRequestTabs', () => {
gl.mrWidget = {};
});
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
describe('clickTab', () => {
let params;
@@ -84,7 +89,7 @@ describe('MergeRequestTabs', () => {
let tabUrl;
beforeEach(() => {
- loadHTMLFixture('merge_requests/merge_request_with_task_list.html');
+ setHTMLFixture(htmlMergeRequestsWithTaskList);
tabUrl = $('.commits-tab a').attr('href');
@@ -268,32 +273,32 @@ describe('MergeRequestTabs', () => {
describe('expandViewContainer', () => {
beforeEach(() => {
- $('body').append(
- '<div class="content-wrapper"><div class="container-fluid container-limited"></div></div>',
- );
- });
-
- afterEach(() => {
- $('.content-wrapper').remove();
+ $('.content-wrapper .container-fluid').addClass('container-limited');
});
- it('removes container-limited from containers', () => {
+ it('removes `container-limited` class from content container', () => {
+ expect($('.content-wrapper .container-limited')).toHaveLength(1);
testContext.class.expandViewContainer();
-
expect($('.content-wrapper .container-limited')).toHaveLength(0);
});
+ });
- it('does not add container-limited when fluid layout is prefered', () => {
- $('.content-wrapper .container-fluid').removeClass('container-limited');
-
- testContext.class.expandViewContainer(false);
+ describe('resetViewContainer', () => {
+ it('does not add `container-limited` CSS class when fluid layout is preferred', () => {
+ testContext.class.resetViewContainer();
expect($('.content-wrapper .container-limited')).toHaveLength(0);
});
- it('does remove container-limited from breadcrumbs', () => {
- $('.container-limited').addClass('breadcrumbs');
- testContext.class.expandViewContainer();
+ it('adds `container-limited` CSS class back when fixed layout is preferred', () => {
+ document.body.innerHTML = '';
+ initMrPage();
+ $('.content-wrapper .container-fluid').addClass('container-limited');
+ // recreate the instance so that `isFixedLayoutPreferred` is re-evaluated
+ testContext.class = new MergeRequestTabs({ stubLocation });
+ $('.content-wrapper .container-fluid').removeClass('container-limited');
+
+ testContext.class.resetViewContainer();
expect($('.content-wrapper .container-limited')).toHaveLength(1);
});
@@ -354,8 +359,6 @@ describe('MergeRequestTabs', () => {
testContext.class.expandSidebar.forEach((el) => {
expect(el.classList.contains('gl-display-none!')).toBe(hides);
});
-
- window.gon = {};
});
describe('when switching tabs', () => {
@@ -381,12 +384,12 @@ describe('MergeRequestTabs', () => {
});
});
- it('scrolls to 0, if no position is stored', () => {
+ it('does not scroll if no position is stored', () => {
testContext.class.tabShown('unknownTab');
jest.advanceTimersByTime(250);
- expect(window.scrollTo.mock.calls[0][0]).toEqual({ top: 0, left: 0, behavior: 'auto' });
+ expect(window.scrollTo).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/merge_requests/components/compare_app_spec.js b/spec/frontend/merge_requests/components/compare_app_spec.js
index 8f84341b653..ba129363ffd 100644
--- a/spec/frontend/merge_requests/components/compare_app_spec.js
+++ b/spec/frontend/merge_requests/components/compare_app_spec.js
@@ -30,10 +30,6 @@ function factory(provideData = {}) {
}
describe('Merge requests compare app component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows commit box when selected branch is empty', () => {
factory({
currentBranch: {
diff --git a/spec/frontend/merge_requests/components/compare_dropdown_spec.js b/spec/frontend/merge_requests/components/compare_dropdown_spec.js
index ab5c315816c..ce03b80bdcb 100644
--- a/spec/frontend/merge_requests/components/compare_dropdown_spec.js
+++ b/spec/frontend/merge_requests/components/compare_dropdown_spec.js
@@ -47,7 +47,6 @@ describe('Merge requests compare dropdown component', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/milestones/components/delete_milestone_modal_spec.js b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
index 87235fa843a..ad6aedaa8ff 100644
--- a/spec/frontend/milestones/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
@@ -5,11 +5,11 @@ import axios from '~/lib/utils/axios_utils';
import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
import eventHub from '~/milestones/event_hub';
import { HTTP_STATUS_IM_A_TEAPOT, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
-import { redirectTo } from '~/lib/utils/url_utility';
-import { createAlert } from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import { createAlert } from '~/alert';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Delete milestone modal', () => {
let wrapper;
@@ -39,10 +39,6 @@ describe('Delete milestone modal', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('onSubmit', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
@@ -64,7 +60,7 @@ describe('Delete milestone modal', () => {
});
});
await findModal().vm.$emit('primary');
- expect(redirectTo).toHaveBeenCalledWith(responseURL);
+ expect(redirectTo).toHaveBeenCalledWith(responseURL); // eslint-disable-line import/no-deprecated
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
milestoneUrl: mockProps.milestoneUrl,
successful: true,
@@ -94,7 +90,7 @@ describe('Delete milestone modal', () => {
expect(createAlert).toHaveBeenCalledWith({
message: alertMessage,
});
- expect(redirectTo).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
milestoneUrl: mockProps.milestoneUrl,
successful: false,
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index f8ddca1a2ad..748e01d4291 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -85,11 +85,6 @@ describe('Milestone combobox component', () => {
mock.onGet(`/api/v4/projects/${projectId}/search`).reply((config) => searchApiCallSpy(config));
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
//
// Finders
//
diff --git a/spec/frontend/milestones/components/promote_milestone_modal_spec.js b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
index d7ad3d29d0a..e91e792afe8 100644
--- a/spec/frontend/milestones/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
@@ -3,14 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility';
import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Promote milestone modal', () => {
let wrapper;
@@ -33,10 +33,6 @@ describe('Promote milestone modal', () => {
wrapper = shallowMount(PromoteMilestoneModal);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Modal opener button', () => {
it('button gets disabled when the modal opens', () => {
expect(promoteButton().disabled).toBe(false);
diff --git a/spec/frontend/milestones/index_spec.js b/spec/frontend/milestones/index_spec.js
new file mode 100644
index 00000000000..477217fc10f
--- /dev/null
+++ b/spec/frontend/milestones/index_spec.js
@@ -0,0 +1,38 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { initShow, MILESTONE_DESCRIPTION_ELEMENT } from '~/milestones/index';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+jest.mock('~/behaviors/markdown/render_gfm');
+jest.mock('~/milestones/milestone');
+jest.mock('~/right_sidebar');
+jest.mock('~/sidebar/mount_milestone_sidebar');
+
+describe('#initShow', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <div class="detail-page-description milestone-detail">
+ <div class="description">
+ <div class="markdown-code-block">
+ <pre class="js-render-mermaid">
+ graph TD;
+ A-- > B;
+ A-- > C;
+ B-- > D;
+ C-- > D;
+ </pre>
+ </div>
+ </div>
+ </div>
+ `);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('calls `renderGFM` to ensure that all gitlab-flavoured markdown is rendered on the milestone details page', () => {
+ initShow();
+
+ expect(renderGFM).toHaveBeenCalledWith(document.querySelector(MILESTONE_DESCRIPTION_ELEMENT));
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
deleted file mode 100644
index 7d7eee2bc2c..00000000000
--- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
+++ /dev/null
@@ -1,268 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MlCandidate renders correctly 1`] = `
-<div>
- <div
- class="gl-alert gl-alert-warning"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16 gl-alert-icon"
- data-testid="warning-icon"
- role="img"
- >
- <use
- href="#warning"
- />
- </svg>
-
- <div
- aria-live="assertive"
- class="gl-alert-content"
- role="alert"
- >
- <h2
- class="gl-alert-title"
- >
- Machine learning experiment tracking is in incubating phase
- </h2>
-
- <div
- class="gl-alert-body"
- >
-
- GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited.
-
- <a
- class="gl-link"
- href="https://about.gitlab.com/handbook/engineering/incubation/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Learn more about incubating features
- </a>
- </div>
-
- <div
- class="gl-alert-actions"
- >
- <a
- class="btn gl-alert-action btn-confirm btn-md gl-button"
- href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Give feedback on this feature
-
- </span>
- </a>
- </div>
- </div>
-
- <button
- aria-label="Dismiss"
- class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="close-icon"
- role="img"
- >
- <use
- href="#close"
- />
- </svg>
-
- <!---->
- </button>
- </div>
-
- <h3>
-
- Model candidate details
-
- </h3>
-
- <table
- class="candidate-details"
- >
- <tbody>
- <tr
- class="divider"
- />
-
- <tr>
- <td
- class="gl-text-secondary gl-font-weight-bold"
- >
- Info
- </td>
-
- <td
- class="gl-font-weight-bold"
- >
- ID
- </td>
-
- <td>
- candidate_iid
- </td>
- </tr>
-
- <tr>
- <td />
-
- <td
- class="gl-font-weight-bold"
- >
- Status
- </td>
-
- <td>
- SUCCESS
- </td>
- </tr>
-
- <tr>
- <td />
-
- <td
- class="gl-font-weight-bold"
- >
- Experiment
- </td>
-
- <td>
- <a
- class="gl-link"
- href="#"
- >
- The Experiment
- </a>
- </td>
- </tr>
-
- <!---->
-
- <tr
- class="divider"
- />
-
- <tr>
- <td
- class="gl-text-secondary gl-font-weight-bold"
- >
-
- Parameters
-
- </td>
-
- <td
- class="gl-font-weight-bold"
- >
- Algorithm
- </td>
-
- <td>
- Decision Tree
- </td>
- </tr>
- <tr>
- <td />
-
- <td
- class="gl-font-weight-bold"
- >
- MaxDepth
- </td>
-
- <td>
- 3
- </td>
- </tr>
- <tr
- class="divider"
- />
-
- <tr>
- <td
- class="gl-text-secondary gl-font-weight-bold"
- >
-
- Metrics
-
- </td>
-
- <td
- class="gl-font-weight-bold"
- >
- AUC
- </td>
-
- <td>
- .55
- </td>
- </tr>
- <tr>
- <td />
-
- <td
- class="gl-font-weight-bold"
- >
- Accuracy
- </td>
-
- <td>
- .99
- </td>
- </tr>
- <tr
- class="divider"
- />
-
- <tr>
- <td
- class="gl-text-secondary gl-font-weight-bold"
- >
-
- Metadata
-
- </td>
-
- <td
- class="gl-font-weight-bold"
- >
- FileName
- </td>
-
- <td>
- test.py
- </td>
- </tr>
- <tr>
- <td />
-
- <td
- class="gl-font-weight-bold"
- >
- ExecutionTime
- </td>
-
- <td>
- .0856
- </td>
- </tr>
- </tbody>
- </table>
-</div>
-`;
diff --git a/spec/frontend/ml/experiment_tracking/components/delete_button_spec.js b/spec/frontend/ml/experiment_tracking/components/delete_button_spec.js
new file mode 100644
index 00000000000..f2a9e3ad9ee
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/delete_button_spec.js
@@ -0,0 +1,68 @@
+import { GlModal, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+
+const csrfToken = 'mock-csrf-token';
+jest.mock('~/lib/utils/csrf', () => ({ token: csrfToken }));
+
+const MODAL_BODY = 'MODAL_BODY';
+const MODAL_TITLE = 'MODAL_TITLE';
+
+describe('DeleteButton', () => {
+ let wrapper;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDeleteButton = () => wrapper.findComponent(GlDisclosureDropdownItem);
+ const findForm = () => wrapper.find('form');
+ const findModalText = () => wrapper.findByText(MODAL_BODY);
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(DeleteButton, {
+ propsData: {
+ deletePath: '/delete',
+ deleteConfirmationText: MODAL_BODY,
+ actionPrimaryText: 'Delete!',
+ modalTitle: MODAL_TITLE,
+ },
+ });
+ });
+
+ it('mounts the modal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('mounts the dropdown', () => {
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('mounts the button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ describe('when modal is opened', () => {
+ it('displays modal title', () => {
+ expect(findModal().props('title')).toBe(MODAL_TITLE);
+ });
+
+ it('displays modal body', () => {
+ expect(findModalText().exists()).toBe(true);
+ });
+
+ it('submits the form when primary action is clicked', () => {
+ const submitSpy = jest.spyOn(findForm().element, 'submit');
+
+ findModal().vm.$emit('primary');
+
+ expect(submitSpy).toHaveBeenCalled();
+ });
+
+ it('displays form with correct action and inputs', () => {
+ const form = findForm();
+
+ expect(form.attributes('action')).toBe('/delete');
+ expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
+ expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(csrfToken);
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
deleted file mode 100644
index 483e454d7d7..00000000000
--- a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { GlAlert } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue';
-
-describe('MlCandidate', () => {
- let wrapper;
-
- const createWrapper = () => {
- const candidate = {
- params: [
- { name: 'Algorithm', value: 'Decision Tree' },
- { name: 'MaxDepth', value: '3' },
- ],
- metrics: [
- { name: 'AUC', value: '.55' },
- { name: 'Accuracy', value: '.99' },
- ],
- metadata: [
- { name: 'FileName', value: 'test.py' },
- { name: 'ExecutionTime', value: '.0856' },
- ],
- info: {
- iid: 'candidate_iid',
- artifact_link: 'path_to_artifact',
- experiment_name: 'The Experiment',
- experiment_path: 'path/to/experiment',
- status: 'SUCCESS',
- },
- };
-
- return mountExtended(MlCandidate, { propsData: { candidate } });
- };
-
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- it('shows incubation warning', () => {
- wrapper = createWrapper();
-
- expect(findAlert().exists()).toBe(true);
- });
-
- it('renders correctly', () => {
- wrapper = createWrapper();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-});
diff --git a/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js b/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js
new file mode 100644
index 00000000000..0794d4747b3
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/components/model_experiments_header_spec.js
@@ -0,0 +1,35 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
+
+describe('ml/experiment_tracking/components/model_experiments_header.vue', () => {
+ let wrapper;
+
+ const createWrapper = () => {
+ wrapper = shallowMount(ModelExperimentsHeader, {
+ propsData: { pageTitle: 'Some Title' },
+ slots: {
+ default: 'Slot content',
+ },
+ });
+ };
+
+ beforeEach(createWrapper);
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findTitle = () => wrapper.find('h3');
+
+ it('renders title', () => {
+ expect(findTitle().text()).toBe('Some Title');
+ });
+
+ it('link points to documentation', () => {
+ expect(findBadge().attributes().href).toBe(
+ '/help/user/project/ml/experiment_tracking/index.md',
+ );
+ });
+
+ it('renders slots', () => {
+ expect(wrapper.html()).toContain('Slot content');
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js
new file mode 100644
index 00000000000..8a39c5de2b3
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js
@@ -0,0 +1,49 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import DetailRow from '~/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue';
+
+describe('CandidateDetailRow', () => {
+ const SECTION_LABEL_CELL = 0;
+ const ROW_LABEL_CELL = 1;
+ const ROW_VALUE_CELL = 2;
+
+ let wrapper;
+
+ const createWrapper = (href = '') => {
+ wrapper = shallowMount(DetailRow, {
+ propsData: { sectionLabel: 'Section', label: 'Item', text: 'Text', href },
+ });
+ };
+
+ const findCellAt = (index) => wrapper.findAll('td').at(index);
+ const findLink = () => findCellAt(ROW_VALUE_CELL).findComponent(GlLink);
+
+ beforeEach(() => createWrapper());
+
+ it('renders section label', () => {
+ expect(findCellAt(SECTION_LABEL_CELL).text()).toBe('Section');
+ });
+
+ it('renders row label', () => {
+ expect(findCellAt(ROW_LABEL_CELL).text()).toBe('Item');
+ });
+
+ describe('No href', () => {
+ it('Renders text', () => {
+ expect(findCellAt(ROW_VALUE_CELL).text()).toBe('Text');
+ });
+
+ it('Does not render as link', () => {
+ expect(findLink().exists()).toBe(false);
+ });
+ });
+
+ describe('With href', () => {
+ beforeEach(() => createWrapper('LINK'));
+
+ it('Renders link', () => {
+ expect(findLink().attributes().href).toBe('LINK');
+ expect(findLink().text()).toBe('Text');
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
new file mode 100644
index 00000000000..9d1c22faa8f
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
@@ -0,0 +1,119 @@
+import { shallowMount } from '@vue/test-utils';
+import MlCandidatesShow from '~/ml/experiment_tracking/routes/candidates/show';
+import DetailRow from '~/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue';
+import { TITLE_LABEL } from '~/ml/experiment_tracking/routes/candidates/show/translations';
+import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
+import { newCandidate } from './mock_data';
+
+describe('MlCandidatesShow', () => {
+ let wrapper;
+ const CANDIDATE = newCandidate();
+
+ const createWrapper = (createCandidate = () => CANDIDATE) => {
+ wrapper = shallowMount(MlCandidatesShow, {
+ propsData: { candidate: createCandidate() },
+ });
+ };
+
+ const findDeleteButton = () => wrapper.findComponent(DeleteButton);
+ const findHeader = () => wrapper.findComponent(ModelExperimentsHeader);
+ const findNthDetailRow = (index) => wrapper.findAllComponents(DetailRow).at(index);
+ const findSectionLabel = (label) => wrapper.find(`[sectionLabel='${label}']`);
+ const findLabel = (label) => wrapper.find(`[label='${label}']`);
+
+ describe('Header', () => {
+ beforeEach(() => createWrapper());
+
+ it('shows delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('passes the delete path to delete button', () => {
+ expect(findDeleteButton().props('deletePath')).toBe('path_to_candidate');
+ });
+
+ it('passes the right title', () => {
+ expect(findHeader().props('pageTitle')).toBe(TITLE_LABEL);
+ });
+ });
+
+ describe('Detail Table', () => {
+ describe('All info available', () => {
+ beforeEach(() => createWrapper());
+
+ const expectedTable = [
+ ['Info', 'ID', CANDIDATE.info.iid, ''],
+ ['', 'MLflow run ID', CANDIDATE.info.eid, ''],
+ ['', 'Status', CANDIDATE.info.status, ''],
+ ['', 'Experiment', CANDIDATE.info.experiment_name, CANDIDATE.info.path_to_experiment],
+ ['', 'Artifacts', 'Artifacts', CANDIDATE.info.path_to_artifact],
+ ['Parameters', CANDIDATE.params[0].name, CANDIDATE.params[0].value, ''],
+ ['', CANDIDATE.params[1].name, CANDIDATE.params[1].value, ''],
+ ['Metrics', CANDIDATE.metrics[0].name, CANDIDATE.metrics[0].value, ''],
+ ['', CANDIDATE.metrics[1].name, CANDIDATE.metrics[1].value, ''],
+ ['Metadata', CANDIDATE.metadata[0].name, CANDIDATE.metadata[0].value, ''],
+ ['', CANDIDATE.metadata[1].name, CANDIDATE.metadata[1].value, ''],
+ ].map((row, index) => [index, ...row]);
+
+ it.each(expectedTable)(
+ 'row %s is created correctly',
+ (index, sectionLabel, label, text, href) => {
+ const row = findNthDetailRow(index);
+
+ expect(row.props()).toMatchObject({ sectionLabel, label, text, href });
+ },
+ );
+ it('does not render params', () => {
+ expect(findSectionLabel('Parameters').exists()).toBe(true);
+ });
+
+ it('renders all conditional rows', () => {
+ // This is a bit of a duplicated test from the above table test, but having this makes sure that the
+ // tests that test the negatives are implemented correctly
+ expect(findLabel('Artifacts').exists()).toBe(true);
+ expect(findSectionLabel('Parameters').exists()).toBe(true);
+ expect(findSectionLabel('Metadata').exists()).toBe(true);
+ expect(findSectionLabel('Metrics').exists()).toBe(true);
+ });
+ });
+
+ describe('No artifact path', () => {
+ beforeEach(() =>
+ createWrapper(() => {
+ const candidate = newCandidate();
+ delete candidate.info.path_to_artifact;
+ return candidate;
+ }),
+ );
+
+ it('does not render artifact row', () => {
+ expect(findLabel('Artifacts').exists()).toBe(false);
+ });
+ });
+
+ describe('No params, metrics, ci or metadata available', () => {
+ beforeEach(() =>
+ createWrapper(() => {
+ const candidate = newCandidate();
+ delete candidate.params;
+ delete candidate.metrics;
+ delete candidate.metadata;
+ return candidate;
+ }),
+ );
+
+ it('does not render params', () => {
+ expect(findSectionLabel('Parameters').exists()).toBe(false);
+ });
+
+ it('does not render metadata', () => {
+ expect(findSectionLabel('Metadata').exists()).toBe(false);
+ });
+
+ it('does not render metrics', () => {
+ expect(findSectionLabel('Metrics').exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
new file mode 100644
index 00000000000..cad2c03fc93
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
@@ -0,0 +1,23 @@
+export const newCandidate = () => ({
+ params: [
+ { name: 'Algorithm', value: 'Decision Tree' },
+ { name: 'MaxDepth', value: '3' },
+ ],
+ metrics: [
+ { name: 'AUC', value: '.55' },
+ { name: 'Accuracy', value: '.99' },
+ ],
+ metadata: [
+ { name: 'FileName', value: 'test.py' },
+ { name: 'ExecutionTime', value: '.0856' },
+ ],
+ info: {
+ iid: 'candidate_iid',
+ eid: 'abcdefg',
+ path_to_artifact: 'path_to_artifact',
+ experiment_name: 'The Experiment',
+ path_to_experiment: 'path/to/experiment',
+ status: 'SUCCESS',
+ path: 'path_to_candidate',
+ },
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
index 017db647ac6..0c83be1822e 100644
--- a/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
@@ -1,8 +1,9 @@
import { GlEmptyState, GlLink, GlTableLite } from '@gitlab/ui';
import MlExperimentsIndexApp from '~/ml/experiment_tracking/routes/experiments/index';
-import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { TITLE_LABEL } from '~/ml/experiment_tracking/routes/experiments/index/translations';
import {
startCursor,
firstExperiment,
@@ -14,11 +15,10 @@ import {
let wrapper;
const createWrapper = (defaultExperiments = [], pageInfo = defaultPageInfo) => {
wrapper = mountExtended(MlExperimentsIndexApp, {
- propsData: { experiments: defaultExperiments, pageInfo },
+ propsData: { experiments: defaultExperiments, pageInfo, emptyStateSvgPath: 'path' },
});
};
-const findAlert = () => wrapper.findComponent(IncubationAlert);
const findPagination = () => wrapper.findComponent(Pagination);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findTable = () => wrapper.findComponent(GlTableLite);
@@ -28,6 +28,7 @@ const findNthTableRow = (idx) => findTableRows().at(idx);
const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col);
const hrefInRowAndColumn = (row, col) =>
findColumnInRow(row, col).findComponent(GlLink).attributes().href;
+const findTitleHeader = () => wrapper.findComponent(ModelExperimentsHeader);
describe('MlExperimentsIndex', () => {
describe('empty state', () => {
@@ -44,12 +45,18 @@ describe('MlExperimentsIndex', () => {
it('does not show pagination', () => {
expect(findPagination().exists()).toBe(false);
});
+
+ it('does not render header', () => {
+ expect(findTitleHeader().exists()).toBe(false);
+ });
});
- it('displays IncubationAlert', () => {
- createWrapper(experiments);
+ describe('Title header', () => {
+ beforeEach(() => createWrapper(experiments));
- expect(findAlert().exists()).toBe(true);
+ it('has the right title', () => {
+ expect(findTitleHeader().props('pageTitle')).toBe(TITLE_LABEL);
+ });
});
describe('experiments table', () => {
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
index f307d2c5a58..2dd17888305 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js
@@ -1,105 +1,54 @@
-import { GlAlert, GlTable, GlLink } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
+import { GlTableLite, GlLink, GlEmptyState, GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import MlExperimentsShow from '~/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue';
+import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
+import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlHelpers from '~/lib/utils/url_utility';
+import { MOCK_START_CURSOR, MOCK_PAGE_INFO, MOCK_CANDIDATES, MOCK_EXPERIMENT } from './mock_data';
-describe('MlExperiment', () => {
+describe('MlExperimentsShow', () => {
let wrapper;
- const startCursor = 'eyJpZCI6IjE2In0';
- const defaultPageInfo = {
- startCursor,
- endCursor: 'eyJpZCI6IjIifQ',
- hasNextPage: true,
- hasPreviousPage: true,
- };
-
const createWrapper = (
candidates = [],
metricNames = [],
paramNames = [],
- pageInfo = defaultPageInfo,
+ pageInfo = MOCK_PAGE_INFO,
+ experiment = MOCK_EXPERIMENT,
+ emptyStateSvgPath = 'path',
) => {
- wrapper = mountExtended(MlExperiment, {
- provide: { candidates, metricNames, paramNames, pageInfo },
+ wrapper = mount(MlExperimentsShow, {
+ propsData: { experiment, candidates, metricNames, paramNames, pageInfo, emptyStateSvgPath },
});
};
- const candidates = [
- {
- rmse: 1,
- l1_ratio: 0.4,
- details: 'link_to_candidate1',
- artifact: 'link_to_artifact',
- name: 'aCandidate',
- created_at: '2023-01-05T14:07:02.975Z',
- user: { username: 'root', path: '/root' },
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate2',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate3',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate4',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- {
- auc: 0.3,
- l1_ratio: 0.5,
- details: 'link_to_candidate5',
- created_at: '2023-01-05T14:07:02.975Z',
- name: null,
- user: null,
- },
- ];
-
- const createWrapperWithCandidates = (pageInfo = defaultPageInfo) => {
- createWrapper(candidates, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo);
+ const createWrapperWithCandidates = (pageInfo = MOCK_PAGE_INFO) => {
+ createWrapper(MOCK_CANDIDATES, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo);
};
- const findAlert = () => wrapper.findComponent(GlAlert);
const findPagination = () => wrapper.findComponent(Pagination);
- const findEmptyState = () => wrapper.findByText('No candidates to display');
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
- const findTable = () => wrapper.findComponent(GlTable);
+ const findTable = () => wrapper.findComponent(GlTableLite);
const findTableHeaders = () => findTable().findAll('th');
const findTableRows = () => findTable().findAll('tbody > tr');
const findNthTableRow = (idx) => findTableRows().at(idx);
const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col);
+ const findExperimentHeader = () => wrapper.findComponent(ModelExperimentsHeader);
+ const findDeleteButton = () => wrapper.findComponent(DeleteButton);
+ const findDownloadButton = () => findExperimentHeader().findComponent(GlButton);
+
const hrefInRowAndColumn = (row, col) =>
findColumnInRow(row, col).findComponent(GlLink).attributes().href;
-
- it('shows incubation warning', () => {
- createWrapper();
-
- expect(findAlert().exists()).toBe(true);
- });
+ const linkTextInRowAndColumn = (row, col) =>
+ findColumnInRow(row, col).findComponent(GlLink).text();
describe('default inputs', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createWrapper();
-
- await nextTick();
});
it('shows empty state', () => {
@@ -110,8 +59,16 @@ describe('MlExperiment', () => {
expect(findPagination().exists()).toBe(false);
});
- it('there are no columns', () => {
- expect(findTable().findAll('th')).toHaveLength(0);
+ it('shows experiment header', () => {
+ expect(findExperimentHeader().exists()).toBe(true);
+ });
+
+ it('passes the correct title to experiment header', () => {
+ expect(findExperimentHeader().props('pageTitle')).toBe(MOCK_EXPERIMENT.name);
+ });
+
+ it('does not show table', () => {
+ expect(findTable().exists()).toBe(false);
});
it('initializes sorting correctly', () => {
@@ -126,6 +83,40 @@ describe('MlExperiment', () => {
});
});
+ describe('Delete', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows delete button', () => {
+ expect(findDeleteButton().exists()).toBe(true);
+ });
+
+ it('passes the right props', () => {
+ expect(findDeleteButton().props('deletePath')).toBe(MOCK_EXPERIMENT.path);
+ });
+ });
+
+ describe('CSV download', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows download CSV button', () => {
+ expect(findDownloadButton().exists()).toBe(true);
+ });
+
+ it('calls the action to download the CSV', () => {
+ setWindowLocation('https://blah.com/something/1?name=query&orderBy=name');
+ jest.spyOn(urlHelpers, 'visitUrl').mockImplementation(() => {});
+
+ findDownloadButton().vm.$emit('click');
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith('/something/1.csv?name=query&orderBy=name');
+ });
+ });
+
describe('Search', () => {
it('shows search box', () => {
createWrapper();
@@ -227,34 +218,34 @@ describe('MlExperiment', () => {
it('Passes pagination to pagination component', () => {
createWrapperWithCandidates();
- expect(findPagination().props('startCursor')).toBe(startCursor);
+ expect(findPagination().props('startCursor')).toBe(MOCK_START_CURSOR);
});
});
describe('Candidate table', () => {
const firstCandidateIndex = 0;
const secondCandidateIndex = 1;
- const firstCandidate = candidates[firstCandidateIndex];
+ const firstCandidate = MOCK_CANDIDATES[firstCandidateIndex];
beforeEach(() => {
createWrapperWithCandidates();
});
it('renders all rows', () => {
- expect(findTableRows()).toHaveLength(candidates.length);
+ expect(findTableRows()).toHaveLength(MOCK_CANDIDATES.length);
});
it('sets the correct columns in the table', () => {
const expectedColumnNames = [
'Name',
'Created at',
- 'User',
+ 'Author',
'L1 Ratio',
'Rmse',
'Auc',
'Mae',
- '',
- '',
+ 'CI Job',
+ 'Artifacts',
];
expect(findTableHeaders().wrappers.map((h) => h.text())).toEqual(expectedColumnNames);
@@ -270,7 +261,29 @@ describe('MlExperiment', () => {
});
it('shows empty state when no artifact', () => {
- expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe('-');
+ expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe(
+ 'No artifacts',
+ );
+ });
+ });
+
+ describe('CI Job column', () => {
+ const jobColumnIndex = -2;
+
+ it('has a link to the job', () => {
+ expect(hrefInRowAndColumn(firstCandidateIndex, jobColumnIndex)).toBe(
+ firstCandidate.ci_job.path,
+ );
+ });
+
+ it('shows the name of the job', () => {
+ expect(linkTextInRowAndColumn(firstCandidateIndex, jobColumnIndex)).toBe(
+ firstCandidate.ci_job.name,
+ );
+ });
+
+ it('shows empty state when there is no job', () => {
+ expect(findColumnInRow(secondCandidateIndex, jobColumnIndex).text()).toBe('-');
});
});
@@ -301,15 +314,7 @@ describe('MlExperiment', () => {
});
it('when there is no user shows nothing', () => {
- expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe('');
- });
- });
-
- describe('Detail column', () => {
- const detailColumn = -2;
-
- it('is a link to details', () => {
- expect(hrefInRowAndColumn(firstCandidateIndex, detailColumn)).toBe(firstCandidate.details);
+ expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe('No name');
});
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
new file mode 100644
index 00000000000..4a606be8da6
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js
@@ -0,0 +1,58 @@
+export const MOCK_START_CURSOR = 'eyJpZCI6IjE2In0';
+
+export const MOCK_PAGE_INFO = {
+ startCursor: MOCK_START_CURSOR,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+};
+
+export const MOCK_EXPERIMENT = { name: 'experiment', path: '/path/to/experiment' };
+
+export const MOCK_CANDIDATES = [
+ {
+ rmse: 1,
+ l1_ratio: 0.4,
+ details: 'link_to_candidate1',
+ artifact: 'link_to_artifact',
+ ci_job: {
+ path: 'link_to_job',
+ name: 'a job',
+ },
+ name: 'aCandidate',
+ created_at: '2023-01-05T14:07:02.975Z',
+ user: { username: 'root', path: '/root' },
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate2',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate3',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate4',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate5',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+];
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
index 0158966997f..cc38a3fd8a1 100644
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ b/spec/frontend/monitoring/components/charts/column_spec.js
@@ -51,10 +51,6 @@ describe('Column component', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('xAxisLabel', () => {
const mockDate = Date.UTC(2020, 4, 26, 20); // 8:00 PM in GMT
diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js
index 484199698ea..33ea5e83598 100644
--- a/spec/frontend/monitoring/components/charts/gauge_spec.js
+++ b/spec/frontend/monitoring/components/charts/gauge_spec.js
@@ -21,11 +21,6 @@ describe('Gauge Chart component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('chart component', () => {
it('is rendered when props are passed', () => {
createWrapper();
diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js
index e163d4e73a0..54245cbdbc1 100644
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js
@@ -28,10 +28,6 @@ describe('Heatmap component', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should display a label on the x axis', () => {
expect(wrapper.vm.xAxisName).toBe(graphData.xLabel);
});
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index 62a0b7e6ad3..fa31b479296 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -21,10 +21,6 @@ describe('Single Stat Chart component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('statValue', () => {
it('should display the correct value', () => {
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 503dee7b937..c1b51f71a7e 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -58,10 +58,6 @@ describe('Time series component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('With a single time series', () => {
describe('general functions', () => {
const findChart = () => wrapper.findComponent({ ref: 'chart' });
diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
index 88de3467580..eb05b1f184a 100644
--- a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
+++ b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
@@ -29,10 +29,6 @@ describe('Create dashboard modal', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has button that links to the project url', async () => {
findRepoButton().trigger('click');
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index bb57420d406..4d290922707 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -2,7 +2,7 @@ import { GlDropdownItem, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
import { createStore } from '~/monitoring/stores';
@@ -55,11 +55,6 @@ describe('Actions menu', () => {
store = createStore();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('add metric item', () => {
it('is rendered when custom metrics are available', async () => {
createShallowWrapper();
@@ -297,8 +292,8 @@ describe('Actions menu', () => {
findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
await nextTick();
- expect(redirectTo).toHaveBeenCalled();
- expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
+ expect(redirectTo).toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl); // eslint-disable-line import/no-deprecated
});
});
});
@@ -324,7 +319,7 @@ describe('Actions menu', () => {
await nextTick();
expect(findStarDashboardItem().exists()).toBe(true);
- expect(findStarDashboardItem().attributes('disabled')).toBe('true');
+ expect(findStarDashboardItem().attributes('disabled')).toBeDefined();
});
it('on click it dispatches a toggle star action', async () => {
@@ -370,7 +365,7 @@ describe('Actions menu', () => {
});
it('is rendered by default but it is disabled', () => {
- expect(findCreateDashboardItem().attributes('disabled')).toBe('true');
+ expect(findCreateDashboardItem().attributes('disabled')).toBeDefined();
});
describe('when project path is set', () => {
@@ -415,7 +410,7 @@ describe('Actions menu', () => {
});
it('is disabled', () => {
- expect(findCreateDashboardItem().attributes('disabled')).toBe('true');
+ expect(findCreateDashboardItem().attributes('disabled')).toBeDefined();
});
it('does not render a modal for creating a dashboard', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 18ccda2c41c..091e05ab271 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -1,7 +1,7 @@
import { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
@@ -9,12 +9,7 @@ import RefreshButton from '~/monitoring/components/refresh_button.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import {
- environmentData,
- dashboardGitResponse,
- selfMonitoringDashboardGitResponse,
- dashboardHeaderProps,
-} from '../mock_data';
+import { environmentData, dashboardGitResponse, dashboardHeaderProps } from '../mock_data';
import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
const mockProjectPath = 'https://path/to/project';
@@ -59,10 +54,6 @@ describe('Dashboard header', () => {
store = createStore();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('dashboards dropdown', () => {
beforeEach(() => {
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
@@ -83,6 +74,7 @@ describe('Dashboard header', () => {
display_name: 'A display name',
});
+ // eslint-disable-next-line import/no-deprecated
expect(redirectTo).toHaveBeenCalledWith(
`${mockProjectPath}/-/metrics/.gitlab%2Fdashboards%2Fdashboard%26copy.yml`,
);
@@ -94,6 +86,7 @@ describe('Dashboard header', () => {
display_name: 'dashboard&copy.yml',
});
+ // eslint-disable-next-line import/no-deprecated
expect(redirectTo).toHaveBeenCalledWith(`${mockProjectPath}/-/metrics/dashboard%26copy.yml`);
});
});
@@ -271,14 +264,8 @@ describe('Dashboard header', () => {
});
describe('actions menu', () => {
- const ootbDashboards = [
- dashboardGitResponse[0].path,
- selfMonitoringDashboardGitResponse[0].path,
- ];
- const customDashboards = [
- dashboardGitResponse[1].path,
- selfMonitoringDashboardGitResponse[1].path,
- ];
+ const ootbDashboards = [dashboardGitResponse[0].path];
+ const customDashboards = [dashboardGitResponse[1].path];
it('is rendered', () => {
createShallowWrapper();
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index d71f6374967..1cfd132b123 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -49,8 +49,6 @@ describe('dashboard invalid url parameters', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue();
});
- afterEach(() => {});
-
it('is mounted', () => {
expect(wrapper.exists()).toBe(true);
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index 339c1710a9e..491649e5b96 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -106,10 +106,6 @@ describe('Dashboard Panel', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the chart title', () => {
expect(findTitle().text()).toBe(graphDataEmpty.title);
});
@@ -134,10 +130,6 @@ describe('Dashboard Panel', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders no chart title', () => {
expect(findTitle().text()).toBe('');
});
@@ -160,10 +152,6 @@ describe('Dashboard Panel', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the chart title', () => {
expect(findTitle().text()).toBe(graphData.title);
});
@@ -377,10 +365,6 @@ describe('Dashboard Panel', () => {
await nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('csvText', () => {
it('converts metrics data from json to csv', () => {
const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].label}"`;
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 1d17a9116df..1f995965003 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -4,7 +4,7 @@ import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { ESC_KEY } from '~/lib/utils/keys';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -33,7 +33,7 @@ import {
setupStoreWithLinks,
} from '../store_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Dashboard', () => {
let store;
@@ -75,7 +75,6 @@ describe('Dashboard', () => {
if (store.dispatch.mockReset) {
store.dispatch.mockReset();
}
- wrapper.destroy();
});
describe('request information to the server', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 9873654bdda..b123d1e7d79 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -1,11 +1,11 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
queryToObject,
- redirectTo,
+ redirectTo, // eslint-disable-line import/no-deprecated
removeParams,
mergeUrlParams,
updateHistory,
@@ -18,7 +18,7 @@ import { defaultTimeRange } from '~/vue_shared/constants';
import { dashboardProps } from '../fixture_data';
import { mockProjectDir } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('dashboard invalid url parameters', () => {
@@ -46,9 +46,6 @@ describe('dashboard invalid url parameters', () => {
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
mock.restore();
queryToObject.mockReset();
});
@@ -139,7 +136,7 @@ describe('dashboard invalid url parameters', () => {
// redirect to with new parameters
expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
- expect(redirectTo).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledTimes(1); // eslint-disable-line import/no-deprecated
});
it('changes the url when a panel moves the time slider', async () => {
diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
index 6695353bdb5..beb698c838f 100644
--- a/spec/frontend/monitoring/components/embeds/embed_group_spec.js
+++ b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
@@ -48,9 +48,6 @@ describe('Embed Group', () => {
afterEach(() => {
metricsWithDataGetter.mockReset();
- if (wrapper) {
- wrapper.destroy();
- }
});
describe('interactivity', () => {
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
index beff3da2baf..db25d524592 100644
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
@@ -52,9 +52,6 @@ describe('MetricEmbed', () => {
afterEach(() => {
metricsWithDataGetter.mockClear();
- if (wrapper) {
- wrapper.destroy();
- }
});
describe('no metrics are available yet', () => {
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index 104263e73e0..593d832f297 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -18,10 +18,6 @@ describe('Graph group component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('When group is not collapsed', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js
index e3cd26b0e48..d3a48be7939 100644
--- a/spec/frontend/monitoring/components/group_empty_state_spec.js
+++ b/spec/frontend/monitoring/components/group_empty_state_spec.js
@@ -23,10 +23,6 @@ function createComponent(props) {
describe('GroupEmptyState', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each([
metricStates.NO_DATA,
metricStates.TIMEOUT,
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index cb300870689..f6cc6789b1f 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -40,6 +40,7 @@ describe('RefreshButton', () => {
afterEach(() => {
dispatch.mockReset();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
});
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index 012e2e9c3e2..e6c5569fa19 100644
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -1,6 +1,5 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
describe('Custom variable component', () => {
@@ -54,13 +53,10 @@ describe('Custom variable component', () => {
expect(findDropdown().exists()).toBe(true);
});
- it('changing dropdown items triggers update', async () => {
+ it('changing dropdown items triggers update', () => {
createShallowWrapper();
- jest.spyOn(wrapper.vm, '$emit');
-
findDropdownItems().at(1).vm.$emit('click');
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary');
+ expect(wrapper.emitted('input')).toEqual([['canary']]);
});
});
diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
index 3073b3968aa..20e1937c5ac 100644
--- a/spec/frontend/monitoring/components/variables/text_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/text_field_spec.js
@@ -33,25 +33,23 @@ describe('Text variable component', () => {
it('triggers keyup enter', async () => {
createShallowWrapper();
- jest.spyOn(wrapper.vm, '$emit');
findInput().element.value = 'prod-pod';
findInput().trigger('input');
findInput().trigger('keyup.enter');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'prod-pod');
+ expect(wrapper.emitted('input')).toEqual([['prod-pod']]);
});
it('triggers blur enter', async () => {
createShallowWrapper();
- jest.spyOn(wrapper.vm, '$emit');
findInput().element.value = 'canary-pod';
findInput().trigger('input');
findInput().trigger('blur');
await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', 'canary-pod');
+ expect(wrapper.emitted('input')).toEqual([['canary-pod']]);
});
});
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 00be5868ba3..1d23190e586 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -206,32 +206,6 @@ export const dashboardGitResponse = [
...customDashboardsData,
];
-export const selfMonitoringDashboardGitResponse = [
- {
- default: true,
- display_name: 'Default',
- can_edit: false,
- system_dashboard: true,
- out_of_the_box_dashboard: true,
- project_blob_path: null,
- path: 'config/prometheus/self_monitoring_default.yml',
- starred: false,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/self_monitoring_default.yml`,
- },
- {
- default: false,
- display_name: 'dashboard.yml',
- can_edit: true,
- system_dashboard: false,
- out_of_the_box_dashboard: false,
- project_blob_path: `${mockProjectDir}/-/blob/main/.gitlab/dashboards/dashboard.yml`,
- path: '.gitlab/dashboards/dashboard.yml',
- starred: true,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
- },
- ...customDashboardsData,
-];
-
// Metrics mocks
export const metricsResult = [
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
index c5a8b50ee60..7fcb7607772 100644
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ b/spec/frontend/monitoring/pages/dashboard_page_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
import { createStore } from '~/monitoring/stores';
+import { assertProps } from 'helpers/assert_props';
import { dashboardProps } from '../fixture_data';
describe('monitoring/pages/dashboard_page', () => {
@@ -37,15 +38,8 @@ describe('monitoring/pages/dashboard_page', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
it('throws errors if dashboard props are not passed', () => {
- expect(() => buildWrapper()).toThrow('Missing required prop: "dashboardProps"');
+ expect(() => assertProps(DashboardPage, {})).toThrow('Missing required prop: "dashboardProps"');
});
it('renders the dashboard page with dashboard component', () => {
diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js
index fa112fca2db..98ee6c1cb29 100644
--- a/spec/frontend/monitoring/pages/panel_new_page_spec.js
+++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js
@@ -49,10 +49,6 @@ describe('monitoring/pages/panel_new_page', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('back to dashboard button', () => {
it('is rendered', () => {
expect(findBackButton().exists()).toBe(true);
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 8eda46a2ff1..b3b198d6b51 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import {
@@ -61,7 +61,7 @@ import {
mockDashboardsErrorResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Monitoring store actions', () => {
const { convertObjectPropsToCamelCase } = commonUtils;
@@ -177,7 +177,6 @@ describe('Monitoring store actions', () => {
});
it('dispatches when feature metricsDashboardAnnotations is on', () => {
- const origGon = window.gon;
window.gon = { features: { metricsDashboardAnnotations: true } };
return testAction(
@@ -190,9 +189,7 @@ describe('Monitoring store actions', () => {
{ type: 'fetchDashboard' },
{ type: 'fetchAnnotations' },
],
- ).then(() => {
- window.gon = origGon;
- });
+ );
});
});
@@ -263,7 +260,7 @@ describe('Monitoring store actions', () => {
});
});
- it('does not show a flash error when showErrorBanner is disabled', async () => {
+ it('does not show an alert when showErrorBanner is disabled', async () => {
state.showErrorBanner = false;
await result();
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
index bad24345f9d..cf8e59d6522 100644
--- a/spec/frontend/nav/components/new_nav_toggle_spec.js
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -1,16 +1,17 @@
import { mount, createWrapper } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { getByText as getByTextHelper } from '@testing-library/dom';
-import { GlToggle } from '@gitlab/ui';
+import { GlDisclosureDropdownItem, GlToggle } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { s__ } from '~/locale';
+import { mockTracking } from 'helpers/tracking_helper';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_ENDPONT = 'https://example.com/toggle';
@@ -18,8 +19,10 @@ describe('NewNavToggle', () => {
useMockLocationHelper();
let wrapper;
+ let trackingSpy;
const findToggle = () => wrapper.findComponent(GlToggle);
+ const findDisclosureItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const createComponent = (propsData = { enabled: false }) => {
wrapper = mount(NewNavToggle, {
@@ -28,85 +31,184 @@ describe('NewNavToggle', () => {
...propsData,
},
});
- };
- afterEach(() => {
- wrapper.destroy();
- });
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ };
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
- it('renders its title', () => {
- createComponent();
- expect(getByText('Navigation redesign').exists()).toBe(true);
- });
-
- describe('when user preference is enabled', () => {
- beforeEach(() => {
- createComponent({ enabled: true });
- });
-
- it('renders the toggle as enabled', () => {
- expect(findToggle().props('value')).toBe(true);
+ describe('When rendered in scope of the new navigation', () => {
+ it('renders the disclosure item', () => {
+ createComponent({ newNavigation: true, enabled: true });
+ expect(findDisclosureItem().exists()).toBe(true);
});
- });
- describe('when user preference is disabled', () => {
- beforeEach(() => {
- createComponent({ enabled: false });
- });
+ describe('when user preference is enabled', () => {
+ beforeEach(() => {
+ createComponent({ newNavigation: true, enabled: true });
+ });
- it('renders the toggle as disabled', () => {
- expect(findToggle().props('value')).toBe(false);
+ it('renders the toggle as enabled', () => {
+ expect(findToggle().props('value')).toBe(true);
+ });
});
- });
- describe.each`
- desc | actFn
- ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
- ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')}
- `('$desc', ({ actFn }) => {
- let mock;
+ describe('when user preference is disabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: false });
+ });
- beforeEach(() => {
- mock = new MockAdapter(axios);
- createComponent({ enabled: false });
+ it('renders the toggle as disabled', () => {
+ expect(findToggle().props('value')).toBe(false);
+ });
});
- it('reloads the page on success', async () => {
- mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
-
- actFn();
- await waitForPromises();
-
- expect(window.location.reload).toHaveBeenCalled();
+ describe.each`
+ desc | actFn | toggleValue | trackingLabel | trackingProperty
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ `('$desc', ({ actFn, toggleValue, trackingLabel, trackingProperty }) => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent({ enabled: toggleValue });
+ });
+
+ it('reloads the page on success', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+
+ actFn();
+ await waitForPromises();
+
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+
+ it('shows an alert on error', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ actFn();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ }),
+ );
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+
+ it('changes the toggle', async () => {
+ await actFn();
+
+ expect(findToggle().props('value')).toBe(!toggleValue);
+ });
+
+ it('tracks the Snowplow event', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+ await actFn();
+ await waitForPromises();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: trackingLabel,
+ property: trackingProperty,
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
});
+ });
- it('shows an alert on error', async () => {
- mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ describe('When rendered in scope of the current navigation', () => {
+ it('renders its title', () => {
+ createComponent();
+ expect(getByText('Navigation redesign').exists()).toBe(true);
+ });
- actFn();
- await waitForPromises();
+ describe('when user preference is enabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: true });
+ });
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: s__(
- 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
- ),
- }),
- );
- expect(window.location.reload).not.toHaveBeenCalled();
+ it('renders the toggle as enabled', () => {
+ expect(findToggle().props('value')).toBe(true);
+ });
});
- it('changes the toggle', async () => {
- await actFn();
+ describe('when user preference is disabled', () => {
+ beforeEach(() => {
+ createComponent({ enabled: false });
+ });
- expect(findToggle().props('value')).toBe(true);
+ it('renders the toggle as disabled', () => {
+ expect(findToggle().props('value')).toBe(false);
+ });
});
- afterEach(() => {
- mock.restore();
+ describe.each`
+ desc | actFn | toggleValue | trackingLabel | trackingProperty
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${false} | ${'enable_new_nav_beta'} | ${'navigation_top'}
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')} | ${true} | ${'disable_new_nav_beta'} | ${'nav_user_menu'}
+ `('$desc', ({ actFn, toggleValue, trackingLabel, trackingProperty }) => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ createComponent({ enabled: toggleValue });
+ });
+
+ it('reloads the page on success', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+
+ actFn();
+ await waitForPromises();
+
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+
+ it('shows an alert on error', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ actFn();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ }),
+ );
+ expect(window.location.reload).not.toHaveBeenCalled();
+ });
+
+ it('changes the toggle', async () => {
+ await actFn();
+
+ expect(findToggle().props('value')).toBe(!toggleValue);
+ });
+
+ it('tracks the Snowplow event', async () => {
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
+ await actFn();
+ await waitForPromises();
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: trackingLabel,
+ property: trackingProperty,
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
});
});
});
diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js
index 76b8ebdc92f..9d3b43520ec 100644
--- a/spec/frontend/nav/components/responsive_app_spec.js
+++ b/spec/frontend/nav/components/responsive_app_spec.js
@@ -33,10 +33,6 @@ describe('~/nav/components/responsive_app.vue', () => {
document.body.className = 'test-class';
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/nav/components/responsive_header_spec.js b/spec/frontend/nav/components/responsive_header_spec.js
index f87de0afb14..2514035270a 100644
--- a/spec/frontend/nav/components/responsive_header_spec.js
+++ b/spec/frontend/nav/components/responsive_header_spec.js
@@ -14,7 +14,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
default: TEST_SLOT_CONTENT,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -25,10 +25,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders slot', () => {
expect(wrapper.text()).toBe(TEST_SLOT_CONTENT);
});
diff --git a/spec/frontend/nav/components/responsive_home_spec.js b/spec/frontend/nav/components/responsive_home_spec.js
index 8f198d92747..5a5cfc93607 100644
--- a/spec/frontend/nav/components/responsive_home_spec.js
+++ b/spec/frontend/nav/components/responsive_home_spec.js
@@ -29,7 +29,7 @@ describe('~/nav/components/responsive_home.vue', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
listeners: {
'menu-item-click': menuItemClickListener,
@@ -45,10 +45,6 @@ describe('~/nav/components/responsive_home.vue', () => {
menuItemClickListener = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
index e70f70afc97..7f39552eb42 100644
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ b/spec/frontend/nav/components/top_nav_app_spec.js
@@ -28,10 +28,6 @@ describe('~/nav/components/top_nav_app.vue', () => {
const findNavItemDropdowToggle = () => findNavItemDropdown().find('.js-top-nav-dropdown-toggle');
const findMenu = () => wrapper.findComponent(TopNavDropdownMenu);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponentShallow();
diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js
index 293fe361fa9..388ac243648 100644
--- a/spec/frontend/nav/components/top_nav_container_view_spec.js
+++ b/spec/frontend/nav/components/top_nav_container_view_spec.js
@@ -48,10 +48,6 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
};
const findFrequentItemsContainer = () => wrapper.find('[data-testid="frequent-items-container"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each(['projects', 'groups'])(
'emits frequent items event to event hub (%s)',
async (frequentItemsDropdownType) => {
diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
index 8a0340087ec..08d6650b5bb 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -36,10 +36,6 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
active: idx === activeIndex,
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation();
});
diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
index 7a5a8475ab7..7a3e58fd964 100644
--- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
@@ -54,10 +54,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
menuItems: findMenuItemModels(x),
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
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 18210658b89..2cd65307b0b 100644
--- a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
+++ b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
@@ -1,6 +1,8 @@
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
+import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants';
const TEST_VIEW_MODEL = {
title: 'Dropdown',
@@ -18,6 +20,16 @@ const TEST_VIEW_MODEL = {
menu_items: [
{ id: 'bar-1', title: 'Bar 1', href: '/bar/1' },
{ id: 'bar-2', title: 'Bar 2', href: '/bar/2' },
+ {
+ id: 'invite',
+ title: '_invite members title_',
+ component: TOP_NAV_INVITE_MEMBERS_COMPONENT,
+ icon: '_icon_',
+ data: {
+ trigger_element: '_trigger_element_',
+ trigger_source: '_trigger_source_',
+ },
+ },
],
},
],
@@ -36,6 +48,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
const findDropdownContents = () =>
findDropdown()
.findAll('[data-testid]')
@@ -55,10 +68,6 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
@@ -73,6 +82,10 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
});
it('renders dropdown content', () => {
+ const hrefItems = TEST_VIEW_MODEL.menu_sections[1].menu_items.filter((item) =>
+ Boolean(item.href),
+ );
+
expect(findDropdownContents()).toEqual([
{
type: 'header',
@@ -90,12 +103,18 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
type: 'header',
text: TEST_VIEW_MODEL.menu_sections[1].title,
},
- ...TEST_VIEW_MODEL.menu_sections[1].menu_items.map(({ title, href }) => ({
+ ...hrefItems.map(({ title, href }) => ({
type: 'item',
href,
text: title,
})),
]);
+ expect(findInviteMembersTrigger().props()).toMatchObject({
+ displayText: '_invite members title_',
+ icon: '_icon_',
+ triggerElement: 'dropdown-_trigger_element_',
+ triggerSource: '_trigger_source_',
+ });
});
});
diff --git a/spec/frontend/new_branch_spec.js b/spec/frontend/new_branch_spec.js
index 5a09598059d..baff5ebfdb8 100644
--- a/spec/frontend/new_branch_spec.js
+++ b/spec/frontend/new_branch_spec.js
@@ -1,4 +1,5 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlBranchesNewBranch from 'test_fixtures/branches/new_branch.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import NewBranchForm from '~/new_branch_form';
describe('Branch', () => {
@@ -11,7 +12,7 @@ describe('Branch', () => {
describe('create a new branch', () => {
function fillNameWith(value) {
document.querySelector('.js-branch-name').value = value;
- const event = new CustomEvent('blur');
+ const event = new CustomEvent('change');
document.querySelector('.js-branch-name').dispatchEvent(event);
}
@@ -20,7 +21,7 @@ describe('Branch', () => {
}
beforeEach(() => {
- loadHTMLFixture('branches/new_branch.html');
+ setHTMLFixture(htmlBranchesNewBranch);
document.querySelector('form').addEventListener('submit', (e) => e.preventDefault());
testContext.form = new NewBranchForm(document.querySelector('.js-create-branch-form'), []);
});
diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js
index 10762a1c3a2..9836400a366 100644
--- a/spec/frontend/notebook/cells/code_spec.js
+++ b/spec/frontend/notebook/cells/code_spec.js
@@ -13,10 +13,6 @@ describe('Code component', () => {
json = JSON.parse(JSON.stringify(fixture));
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without output', () => {
beforeEach(() => {
wrapper = mountComponent(json.cells[0]);
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index a7776bd5b69..f226776212a 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -1,18 +1,16 @@
import { mount } from '@vue/test-utils';
import katex from 'katex';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import markdownTableJson from 'test_fixtures/blob/notebook/markdown-table.json';
import basicJson from 'test_fixtures/blob/notebook/basic.json';
import mathJson from 'test_fixtures/blob/notebook/math.json';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
import Prompt from '~/notebook/cells/prompt.vue';
-const Component = Vue.extend(MarkdownComponent);
-
window.katex = katex;
function buildCellComponent(cell, relativePath = '', hidePrompt) {
- return mount(Component, {
+ return mount(MarkdownComponent, {
propsData: {
cell,
hidePrompt,
diff --git a/spec/frontend/notebook/cells/output/dataframe_spec.js b/spec/frontend/notebook/cells/output/dataframe_spec.js
new file mode 100644
index 00000000000..bf90497a36b
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/dataframe_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import DataframeOutput from '~/notebook/cells/output/dataframe.vue';
+import JSONTable from '~/behaviors/components/json_table.vue';
+import { outputWithDataframe } from '../../mock_data';
+
+describe('~/notebook/cells/output/DataframeOutput', () => {
+ let wrapper;
+
+ function createComponent(rawCode) {
+ wrapper = shallowMount(DataframeOutput, {
+ propsData: {
+ rawCode,
+ count: 0,
+ index: 0,
+ },
+ });
+ }
+
+ const findTable = () => wrapper.findComponent(JSONTable);
+
+ describe('with valid dataframe', () => {
+ beforeEach(() => createComponent(outputWithDataframe.data['text/html'].join('')));
+
+ it('mounts the table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('table caption is empty', () => {
+ expect(findTable().props().caption).toEqual('');
+ });
+
+ it('allows filtering', () => {
+ expect(findTable().props().hasFilter).toBe(true);
+ });
+
+ it('sets the correct fields', () => {
+ expect(findTable().props().fields).toEqual([
+ { key: 'index0', label: '', sortable: true, class: 'gl-font-weight-bold' },
+ { key: 'column0', label: 'column_1', sortable: true, class: '' },
+ { key: 'column1', label: 'column_2', sortable: true, class: '' },
+ ]);
+ });
+
+ it('sets the correct items', () => {
+ expect(findTable().props().items).toEqual([
+ { index0: '0', column0: 'abc de f', column1: 'a' },
+ { index0: '1', column0: 'True', column1: '0.1' },
+ ]);
+ });
+ });
+
+ describe('invalid dataframe', () => {
+ it('still displays the table', () => {
+ createComponent('dataframe');
+
+ expect(findTable().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/notebook/cells/output/dataframe_util_spec.js b/spec/frontend/notebook/cells/output/dataframe_util_spec.js
new file mode 100644
index 00000000000..37dee5429e4
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/dataframe_util_spec.js
@@ -0,0 +1,133 @@
+import { isDataframe, convertHtmlTableToJson } from '~/notebook/cells/output/dataframe_util';
+import { outputWithDataframeContent, outputWithMultiIndexDataFrame } from '../../mock_data';
+import sanitizeTests from './html_sanitize_fixtures';
+
+describe('notebook/cells/output/dataframe_utils', () => {
+ describe('isDataframe', () => {
+ describe('when output data has no text/html', () => {
+ it('is is not a dataframe', () => {
+ const input = { data: { 'image/png': ['blah'] } };
+
+ expect(isDataframe(input)).toBe(false);
+ });
+ });
+
+ describe('when output data has no text/html, but no mention of dataframe', () => {
+ it('is is not a dataframe', () => {
+ const input = { data: { 'text/html': ['blah'] } };
+
+ expect(isDataframe(input)).toBe(false);
+ });
+ });
+
+ describe('when output data has text/html, but no mention of dataframe in the first 20 lines', () => {
+ it('is is not a dataframe', () => {
+ const input = { data: { 'text/html': [...new Array(20).fill('a'), 'dataframe'] } };
+
+ expect(isDataframe(input)).toBe(false);
+ });
+ });
+
+ describe('when output data has text/html, and includes "dataframe" within the first 20 lines', () => {
+ it('is is not a dataframe', () => {
+ const input = { data: { 'text/html': ['dataframe'] } };
+
+ expect(isDataframe(input)).toBe(true);
+ });
+ });
+ });
+
+ describe('convertHtmlTableToJson', () => {
+ it('converts table correctly', () => {
+ const input = outputWithDataframeContent;
+
+ const output = {
+ fields: [
+ { key: 'index0', label: '', sortable: true, class: 'gl-font-weight-bold' },
+ { key: 'column0', label: 'column_1', sortable: true, class: '' },
+ { key: 'column1', label: 'column_2', sortable: true, class: '' },
+ ],
+ items: [
+ { index0: '0', column0: 'abc de f', column1: 'a' },
+ { index0: '1', column0: 'True', column1: '0.1' },
+ ],
+ };
+
+ expect(convertHtmlTableToJson(input)).toEqual(output);
+ });
+
+ it('converts multi-index table correctly', () => {
+ const input = outputWithMultiIndexDataFrame;
+
+ const output = {
+ fields: [
+ { key: 'index0', label: 'first', sortable: true, class: 'gl-font-weight-bold' },
+ { key: 'index1', label: 'second', sortable: true, class: 'gl-font-weight-bold' },
+ { key: 'column0', label: '0', sortable: true, class: '' },
+ ],
+ items: [
+ { index0: 'bar', index1: 'one', column0: '1' },
+ { index0: 'bar', index1: 'two', column0: '2' },
+ { index0: 'baz', index1: 'one', column0: '3' },
+ { index0: 'baz', index1: 'two', column0: '4' },
+ ],
+ };
+
+ expect(convertHtmlTableToJson(input)).toEqual(output);
+ });
+
+ describe('sanitizes input before parsing table', () => {
+ it('sanitizes input html', () => {
+ const parser = new DOMParser();
+ const spy = jest.spyOn(parser, 'parseFromString');
+ const input = 'hello<style>p {width:50%;}</style><script>alert(1)</script>';
+
+ convertHtmlTableToJson(input, parser);
+
+ expect(spy).toHaveBeenCalledWith('hello', 'text/html');
+ });
+ });
+
+ describe('does not include harmful html', () => {
+ const makeDataframeWithHtml = (html) => {
+ return [
+ '<table border="1" class="dataframe">\n',
+ ' <thead>\n',
+ ' <tr style="text-align: right;">\n',
+ ' <th></th>\n',
+ ' <th>column_1</th>\n',
+ ' </tr>\n',
+ ' </thead>\n',
+ ' <tbody>\n',
+ ' <tr>\n',
+ ' <th>0</th>\n',
+ ` <td>${html}</td>\n`,
+ ' </tr>\n',
+ ' </tbody>\n',
+ '</table>\n',
+ '</div>',
+ ];
+ };
+
+ it.each([
+ ['table', 0],
+ ['style', 1],
+ ['iframe', 2],
+ ['svg', 3],
+ ])('sanitizes output for: %p', (tag, index) => {
+ const inputHtml = makeDataframeWithHtml(sanitizeTests[index][1].input);
+ const convertedHtml = convertHtmlTableToJson(inputHtml).items[0].column0;
+
+ expect(convertedHtml).not.toContain(tag);
+ });
+ });
+
+ describe('when dataframe is invalid', () => {
+ it('returns empty', () => {
+ const input = [' dataframe', ' blah'];
+
+ expect(convertHtmlTableToJson(input)).toEqual({ fields: [], items: [] });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/notebook/cells/output/error_spec.js b/spec/frontend/notebook/cells/output/error_spec.js
new file mode 100644
index 00000000000..2e4ca8c1761
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/error_spec.js
@@ -0,0 +1,48 @@
+import { mount } from '@vue/test-utils';
+import ErrorOutput from '~/notebook/cells/output/error.vue';
+import Prompt from '~/notebook/cells/prompt.vue';
+import Markdown from '~/notebook/cells/markdown.vue';
+import { errorOutputContent, relativeRawPath } from '../../mock_data';
+
+describe('notebook/cells/output/error.vue', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mount(ErrorOutput, {
+ propsData: {
+ rawCode: errorOutputContent,
+ index: 1,
+ count: 2,
+ },
+ provide: { relativeRawPath },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findPrompt = () => wrapper.findComponent(Prompt);
+ const findMarkdown = () => wrapper.findComponent(Markdown);
+
+ it('renders the prompt', () => {
+ expect(findPrompt().props()).toMatchObject({ count: 2, showOutput: true, type: 'Out' });
+ });
+
+ it('renders the markdown', () => {
+ const expectedParsedMarkdown =
+ '```error\n' +
+ '---------------------------------------------------------------------------\n' +
+ 'NameError Traceback (most recent call last)\n' +
+ '/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py in <module>\n' +
+ '----> 1 To\n' +
+ '\n' +
+ "NameError: name 'To' is not defined\n" +
+ '```';
+
+ expect(findMarkdown().props()).toMatchObject({
+ cell: { source: [expectedParsedMarkdown] },
+ hidePrompt: true,
+ });
+ });
+});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 585cbb68eeb..efbdfca8d8c 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -2,7 +2,13 @@ import { mount } from '@vue/test-utils';
import json from 'test_fixtures/blob/notebook/basic.json';
import Output from '~/notebook/cells/output/index.vue';
import MarkdownOutput from '~/notebook/cells/output/markdown.vue';
-import { relativeRawPath, markdownCellContent } from '../../mock_data';
+import DataframeOutput from '~/notebook/cells/output/dataframe.vue';
+import {
+ relativeRawPath,
+ markdownCellContent,
+ outputWithDataframe,
+ outputWithDataframeContent,
+} from '../../mock_data';
describe('Output component', () => {
let wrapper;
@@ -17,10 +23,6 @@ describe('Output component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('text output', () => {
beforeEach(() => {
const textType = json.cells[2];
@@ -109,6 +111,16 @@ describe('Output component', () => {
});
});
+ describe('Dataframe output', () => {
+ it('renders DataframeOutput component', () => {
+ createComponent(outputWithDataframe);
+
+ expect(wrapper.findComponent(DataframeOutput).props('rawCode')).toBe(
+ outputWithDataframeContent.join(''),
+ );
+ });
+ });
+
describe('default to plain text', () => {
beforeEach(() => {
const unknownType = json.cells[6];
diff --git a/spec/frontend/notebook/cells/prompt_spec.js b/spec/frontend/notebook/cells/prompt_spec.js
index 0cda0c5bc2b..4c864a9b930 100644
--- a/spec/frontend/notebook/cells/prompt_spec.js
+++ b/spec/frontend/notebook/cells/prompt_spec.js
@@ -6,10 +6,6 @@ describe('Prompt component', () => {
const mountComponent = ({ type }) => shallowMount(Prompt, { propsData: { type, count: 1 } });
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('input', () => {
beforeEach(() => {
wrapper = mountComponent({ type: 'In' });
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
index b79000a3505..3c73d420703 100644
--- a/spec/frontend/notebook/index_spec.js
+++ b/spec/frontend/notebook/index_spec.js
@@ -1,16 +1,14 @@
import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import json from 'test_fixtures/blob/notebook/basic.json';
import jsonWithWorksheet from 'test_fixtures/blob/notebook/worksheets.json';
import Notebook from '~/notebook/index.vue';
-const Component = Vue.extend(Notebook);
-
describe('Notebook component', () => {
let vm;
function buildComponent(notebook) {
- return mount(Component, {
+ return mount(Notebook, {
propsData: { notebook },
provide: { relativeRawPath: '' },
}).vm;
diff --git a/spec/frontend/notebook/mock_data.js b/spec/frontend/notebook/mock_data.js
index b1419e1256f..9c63ad773b5 100644
--- a/spec/frontend/notebook/mock_data.js
+++ b/spec/frontend/notebook/mock_data.js
@@ -1,2 +1,104 @@
export const relativeRawPath = '/test';
export const markdownCellContent = ['# Test'];
+export const errorOutputContent = [
+ '\u001b[0;31m---------------------------------------------------------------------------\u001b[0m',
+ '\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)',
+ '\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_79203/294318627.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mTo\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m',
+ "\u001b[0;31mNameError\u001b[0m: name 'To' is not defined",
+];
+export const outputWithDataframeContent = [
+ '<div>\n',
+ '<style scoped>\n',
+ ' .dataframe tbody tr th:only-of-type {\n',
+ ' vertical-align: middle;\n',
+ ' }\n',
+ '\n',
+ ' .dataframe tbody tr th {\n',
+ ' vertical-align: top;\n',
+ ' }\n',
+ '\n',
+ ' .dataframe thead th {\n',
+ ' text-align: right;\n',
+ ' }\n',
+ '</style>\n',
+ '<table border="1" class="dataframe">\n',
+ ' <thead>\n',
+ ' <tr style="text-align: right;">\n',
+ ' <th></th>\n',
+ ' <th>column_1</th>\n',
+ ' <th>column_2</th>\n',
+ ' </tr>\n',
+ ' </thead>\n',
+ ' <tbody>\n',
+ ' <tr>\n',
+ ' <th>0</th>\n',
+ ' <td>abc de f</td>\n',
+ ' <td>a</td>\n',
+ ' </tr>\n',
+ ' <tr>\n',
+ ' <th>1</th>\n',
+ ' <td>True</td>\n',
+ ' <td>0.1</td>\n',
+ ' </tr>\n',
+ ' </tbody>\n',
+ '</table>\n',
+ '</div>',
+];
+
+export const outputWithMultiIndexDataFrame = [
+ '<div>\n',
+ '<style scoped>\n',
+ ' .dataframe tbody tr th:only-of-type {\n',
+ ' vertical-align: middle;\n',
+ ' }\n',
+ '\n',
+ ' .dataframe tbody tr th {\n',
+ ' vertical-align: top;\n',
+ ' }\n',
+ '\n',
+ ' .dataframe thead th {\n',
+ ' text-align: right;\n',
+ ' }\n',
+ '</style>\n',
+ '<table border="1" class="dataframe">\n',
+ ' <thead>\n',
+ ' <tr style="text-align: right;">\n',
+ ' <th></th>\n',
+ ' <th></th>\n',
+ ' <th>0</th>\n',
+ ' </tr>\n',
+ ' <tr>\n',
+ ' <th>first</th>\n',
+ ' <th>second</th>\n',
+ ' <th></th>\n',
+ ' </tr>\n',
+ ' </thead>\n',
+ ' <tbody>\n',
+ ' <tr>\n',
+ ' <th rowspan="2" valign="top">bar</th>\n',
+ ' <th>one</th>\n',
+ ' <td>1</td>\n',
+ ' </tr>\n',
+ ' <tr>\n',
+ ' <th>two</th>\n',
+ ' <td>2</td>\n',
+ ' </tr>\n',
+ ' <tr>\n',
+ ' <th rowspan="2" valign="top">baz</th>\n',
+ ' <th>one</th>\n',
+ ' <td>3</td>\n',
+ ' </tr>\n',
+ ' <tr>\n',
+ ' <th>two</th>\n',
+ ' <td>4</td>\n',
+ ' </tr>\n',
+ ' </tbody>\n',
+ '</table>\n',
+ '</div>',
+];
+
+export const outputWithDataframe = {
+ data: {
+ 'text/html': outputWithDataframeContent,
+ },
+};
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index dfb05c85fc8..70f25afc5ba 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -5,11 +5,13 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import Autosave from '~/autosave';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
+import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import axios from '~/lib/utils/axios_utils';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import CommentForm from '~/notes/components/comment_form.vue';
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
@@ -21,18 +23,20 @@ import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock }
jest.mock('autosize');
jest.mock('~/commons/nav/user_merge_requests');
-jest.mock('~/flash');
-jest.mock('~/autosave');
+jest.mock('~/alert');
Vue.use(Vuex);
describe('issue_comment_form component', () => {
+ useLocalStorageSpy();
+
let store;
let wrapper;
let axiosMock;
const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button');
- const findTextArea = () => wrapper.findByTestId('comment-field');
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
+ const findMarkdownEditorTextarea = () => findMarkdownEditor().find('textarea');
const findAddToReviewButton = () => wrapper.findByTestId('add-to-review-button');
const findAddCommentNowButton = () => wrapper.findByTestId('add-comment-now-button');
const findConfidentialNoteCheckbox = () => wrapper.findByTestId('internal-note-checkbox');
@@ -127,7 +131,6 @@ describe('issue_comment_form component', () => {
afterEach(() => {
axiosMock.restore();
- wrapper.destroy();
});
describe('user is logged in', () => {
@@ -136,7 +139,6 @@ describe('issue_comment_form component', () => {
mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- jest.spyOn(wrapper.vm, 'resizeTextarea');
jest.spyOn(wrapper.vm, 'stopPolling');
findCloseReopenButton().trigger('click');
@@ -145,7 +147,6 @@ describe('issue_comment_form component', () => {
expect(wrapper.vm.note).toBe('');
expect(wrapper.vm.saveNote).toHaveBeenCalled();
expect(wrapper.vm.stopPolling).toHaveBeenCalled();
- expect(wrapper.vm.resizeTextarea).toHaveBeenCalled();
});
it('does not report errors in the UI when the save succeeds', async () => {
@@ -260,6 +261,18 @@ describe('issue_comment_form component', () => {
});
});
+ it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
+ mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } });
+
+ expect(wrapper.text()).not.toContain('Switch to rich text');
+ });
+
+ it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
+ mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } });
+
+ expect(wrapper.text()).toContain('Switch to rich text');
+ });
+
describe('textarea', () => {
describe('general', () => {
it.each`
@@ -268,13 +281,13 @@ describe('issue_comment_form component', () => {
${'internal note'} | ${true} | ${'Write an internal note or drag your files here…'}
`(
'should render textarea with placeholder for $noteType',
- ({ noteIsInternal, placeholder }) => {
- mountComponent({
- mountFunction: mount,
- initialData: { noteIsInternal },
- });
+ async ({ noteIsInternal, placeholder }) => {
+ mountComponent();
- expect(findTextArea().attributes('placeholder')).toBe(placeholder);
+ wrapper.vm.noteIsInternal = noteIsInternal;
+ await nextTick();
+
+ expect(findMarkdownEditor().props('formFieldProps').placeholder).toBe(placeholder);
},
);
@@ -290,13 +303,13 @@ describe('issue_comment_form component', () => {
await findCommentButton().trigger('click');
- expect(findTextArea().attributes('disabled')).toBe('disabled');
+ expect(findMarkdownEditor().find('textarea').attributes('disabled')).toBeDefined();
});
it('should support quick actions', () => {
mountComponent({ mountFunction: mount });
- expect(findTextArea().attributes('data-supports-quick-actions')).toBe('true');
+ expect(findMarkdownEditor().props('supportsQuickActions')).toBe(true);
});
it('should link to markdown docs', () => {
@@ -336,63 +349,51 @@ describe('issue_comment_form component', () => {
it('should enter edit mode when arrow up is pressed', () => {
jest.spyOn(wrapper.vm, 'editCurrentUserLastNote');
- findTextArea().trigger('keydown.up');
+ findMarkdownEditorTextarea().trigger('keydown.up');
expect(wrapper.vm.editCurrentUserLastNote).toHaveBeenCalled();
});
- it('inits autosave', () => {
- expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [
- 'Note',
- 'Issue',
- noteableDataMock.id,
- ]);
- });
- });
-
- describe('event enter', () => {
- beforeEach(() => {
- mountComponent({ mountFunction: mount });
- });
-
- describe('when no draft exists', () => {
- it('should save note when cmd+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSave');
+ describe('event enter', () => {
+ describe('when no draft exists', () => {
+ it('should save note when cmd+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSave');
- findTextArea().trigger('keydown.enter', { metaKey: true });
+ findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
- expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
- });
+ expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
+ });
- it('should save note when ctrl+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSave');
+ it('should save note when ctrl+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSave');
- findTextArea().trigger('keydown.enter', { ctrlKey: true });
+ findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
- expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
+ expect(wrapper.vm.handleSave).toHaveBeenCalledWith();
+ });
});
- });
- describe('when a draft exists', () => {
- beforeEach(() => {
- store.registerModule('batchComments', batchComments());
- store.state.batchComments.drafts = [{ note: 'A' }];
- });
+ describe('when a draft exists', () => {
+ beforeEach(() => {
+ store.registerModule('batchComments', batchComments());
+ store.state.batchComments.drafts = [{ note: 'A' }];
+ });
- it('should save note draft when cmd+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSaveDraft');
+ it('should save note draft when cmd+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSaveDraft');
- findTextArea().trigger('keydown.enter', { metaKey: true });
+ findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
- expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
- });
+ expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
+ });
- it('should save note draft when ctrl+enter is pressed', () => {
- jest.spyOn(wrapper.vm, 'handleSaveDraft');
+ it('should save note draft when ctrl+enter is pressed', () => {
+ jest.spyOn(wrapper.vm, 'handleSaveDraft');
- findTextArea().trigger('keydown.enter', { ctrlKey: true });
+ findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
- expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
+ expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
+ });
});
});
});
@@ -482,7 +483,7 @@ describe('issue_comment_form component', () => {
it(`makes an API call to open it`, () => {
mountComponent({
noteableType,
- noteableData: { ...noteableDataMock, state: constants.OPENED },
+ noteableData: { ...noteableDataMock, state: STATUS_OPEN },
mountFunction: mount,
});
@@ -496,7 +497,7 @@ describe('issue_comment_form component', () => {
it(`shows an error when the API call fails`, async () => {
mountComponent({
noteableType,
- noteableData: { ...noteableDataMock, state: constants.OPENED },
+ noteableData: { ...noteableDataMock, state: STATUS_OPEN },
mountFunction: mount,
});
@@ -517,7 +518,7 @@ describe('issue_comment_form component', () => {
it('makes an API call to close it', () => {
mountComponent({
noteableType,
- noteableData: { ...noteableDataMock, state: constants.CLOSED },
+ noteableData: { ...noteableDataMock, state: STATUS_CLOSED },
mountFunction: mount,
});
@@ -532,7 +533,7 @@ describe('issue_comment_form component', () => {
it(`shows an error when the API call fails`, async () => {
mountComponent({
noteableType,
- noteableData: { ...noteableDataMock, state: constants.CLOSED },
+ noteableData: { ...noteableDataMock, state: STATUS_CLOSED },
mountFunction: mount,
});
@@ -651,6 +652,37 @@ describe('issue_comment_form component', () => {
});
});
+ describe('check sensitive tokens', () => {
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+ const nonSensitiveMessage = 'text';
+
+ it('should not save note when it contains sensitive token', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: sensitiveMessage },
+ });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+
+ clickCommentButton();
+
+ expect(wrapper.vm.saveNote).not.toHaveBeenCalled();
+ });
+
+ it('should save note it does not contain sensitive token', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: nonSensitiveMessage },
+ });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+
+ clickCommentButton();
+
+ expect(wrapper.vm.saveNote).toHaveBeenCalled();
+ });
+ });
+
describe('user is not logged in', () => {
beforeEach(() => {
mountComponent({ userData: null, noteableData: loggedOutnoteableData, mountFunction: mount });
@@ -661,7 +693,7 @@ describe('issue_comment_form component', () => {
});
it('should not render submission form', () => {
- expect(findTextArea().exists()).toBe(false);
+ expect(findMarkdownEditor().exists()).toBe(false);
});
});
diff --git a/spec/frontend/notes/components/comment_type_dropdown_spec.js b/spec/frontend/notes/components/comment_type_dropdown_spec.js
index cabf551deba..b891c1f553d 100644
--- a/spec/frontend/notes/components/comment_type_dropdown_spec.js
+++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js
@@ -24,10 +24,6 @@ describe('CommentTypeDropdown component', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
isInternalNote | buttonText
${false} | ${COMMENT_FORM.comment}
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index bb44563b87a..66b86ed3ce0 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -22,10 +22,6 @@ describe('diff_discussion_header component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Avatar', () => {
const firstNoteAuthor = discussionMock.notes[0].author;
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index e414ada1854..a9a20bd8bc3 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -38,15 +38,12 @@ describe('DiscussionActions', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
const createComponent = createComponentFactory();
it('renders reply placeholder, resolve discussion button, resolve with issue button and jump to next discussion button', () => {
createComponent();
+
expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true);
expect(wrapper.findComponent(ResolveDiscussionButton).exists()).toBe(true);
expect(wrapper.findComponent(ResolveWithIssueButton).exists()).toBe(true);
@@ -94,17 +91,15 @@ describe('DiscussionActions', () => {
it('emits showReplyForm event when clicking on reply placeholder', () => {
createComponent({}, { attachTo: document.body });
- jest.spyOn(wrapper.vm, '$emit');
wrapper.findComponent(ReplyPlaceholder).find('textarea').trigger('focus');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm');
+ expect(wrapper.emitted().showReplyForm).toHaveLength(1);
});
it('emits resolve event when clicking on resolve button', () => {
createComponent();
- jest.spyOn(wrapper.vm, '$emit');
wrapper.findComponent(ResolveDiscussionButton).find('button').trigger('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve');
+ expect(wrapper.emitted().resolve).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index f4ec7f835bb..ac677841ee1 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -40,7 +40,6 @@ describe('DiscussionCounter component', () => {
afterEach(() => {
wrapper.vm.$destroy();
- wrapper = null;
});
describe('has no discussions', () => {
@@ -119,8 +118,6 @@ describe('DiscussionCounter component', () => {
toggleAllButton = wrapper.find('[data-testid="toggle-all-discussions-btn"]');
};
- afterEach(() => wrapper.destroy());
-
it('calls button handler when clicked', async () => {
await updateStoreWithExpanded(true);
diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js
index 48f5030aa1a..e31155a028f 100644
--- a/spec/frontend/notes/components/discussion_filter_note_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_note_spec.js
@@ -18,11 +18,6 @@ describe('DiscussionFilterNote component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('timelineContent renders a string containing instruction for switching feed type', () => {
expect(wrapper.find('[data-testid="discussion-filter-timeline-content"]').html()).toBe(
'<div data-testid="discussion-filter-timeline-content">You\'re only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.</div>',
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index ed1ced1b3d1..7d8347b20d4 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -34,7 +34,8 @@ describe('DiscussionFilter component', () => {
const filterDiscussion = jest.fn();
const findFilter = (filterType) =>
- wrapper.find(`.dropdown-item[data-filter-type="${filterType}"]`);
+ wrapper.find(`.gl-new-dropdown-item[data-filter-type="${filterType}"]`);
+ const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
@@ -77,17 +78,16 @@ describe('DiscussionFilter component', () => {
// as it doesn't matter for our tests here
mock.onGet(DISCUSSION_PATH).reply(HTTP_STATUS_OK, '');
window.mrTabs = undefined;
- wrapper = mountComponent();
jest.spyOn(Tracking, 'event');
});
afterEach(() => {
- wrapper.vm.$destroy();
mock.restore();
});
describe('default', () => {
beforeEach(() => {
+ wrapper = mountComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
@@ -104,12 +104,13 @@ describe('DiscussionFilter component', () => {
describe('when asc', () => {
beforeEach(() => {
+ wrapper = mountComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
});
describe('when the dropdown is clicked', () => {
it('calls the right actions', () => {
- wrapper.find('.js-newest-first').vm.$emit('click');
+ wrapper.find('.js-newest-first').vm.$emit('action');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
direction: DESC,
@@ -123,13 +124,14 @@ describe('DiscussionFilter component', () => {
describe('when desc', () => {
beforeEach(() => {
+ wrapper = mountComponent();
store.state.discussionSortOrder = DESC;
jest.spyOn(store, 'dispatch').mockImplementation();
});
describe('when the dropdown item is clicked', () => {
it('calls the right actions', () => {
- wrapper.find('.js-oldest-first').vm.$emit('click');
+ wrapper.find('.js-oldest-first').vm.$emit('action');
expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', {
direction: ASC,
@@ -139,62 +141,68 @@ describe('DiscussionFilter component', () => {
});
});
- it('sets is-checked to true on the active button in the dropdown', () => {
- expect(wrapper.find('.js-newest-first').props('isChecked')).toBe(true);
+ it('sets is-selected to true on the active button in the dropdown', () => {
+ expect(findGlDisclosureDropdownItem().attributes('is-selected')).toBe('true');
});
});
});
- it('renders the all filters', () => {
- expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe(
- discussionFiltersMock.length,
- );
- });
+ describe('discussion filter functionality', () => {
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
- it('renders the default selected item', () => {
- expect(wrapper.find('.discussion-filter-container .dropdown-item').text().trim()).toBe(
- discussionFiltersMock[0].title,
- );
- });
+ it('renders the all filters', () => {
+ expect(wrapper.findAll('.discussion-filter-container .gl-new-dropdown-item').length).toBe(
+ discussionFiltersMock.length,
+ );
+ });
- it('disables the dropdown when discussions are loading', () => {
- store.state.isLoading = true;
+ it('renders the default selected item', () => {
+ expect(wrapper.find('.discussion-filter-container .gl-new-dropdown-item').text().trim()).toBe(
+ discussionFiltersMock[0].title,
+ );
+ });
- expect(wrapper.findComponent(GlDropdown).props('disabled')).toBe(true);
- });
+ it('disables the dropdown when discussions are loading', () => {
+ store.state.isLoading = true;
- it('updates to the selected item', () => {
- const filterItem = findFilter(DISCUSSION_FILTER_TYPES.ALL);
+ expect(wrapper.findComponent(GlDisclosureDropdown).props('disabled')).toBe(true);
+ });
- filterItem.trigger('click');
+ it('updates to the selected item', () => {
+ const filterItem = findFilter(DISCUSSION_FILTER_TYPES.ALL);
- expect(wrapper.vm.currentFilter.title).toBe(filterItem.text().trim());
- });
+ filterItem.vm.$emit('action');
- it('only updates when selected filter changes', () => {
- findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click');
+ expect(filterItem.text().trim()).toBe('Show all activity');
+ });
- expect(filterDiscussion).not.toHaveBeenCalled();
- });
+ it('only updates when selected filter changes', () => {
+ findFilter(DISCUSSION_FILTER_TYPES.ALL).vm.$emit('action');
+
+ expect(filterDiscussion).not.toHaveBeenCalled();
+ });
- it('disables timeline view if it was enabled', () => {
- store.state.isTimelineEnabled = true;
+ it('disables timeline view if it was enabled', () => {
+ store.state.isTimelineEnabled = true;
- findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click');
+ findFilter(DISCUSSION_FILTER_TYPES.HISTORY).vm.$emit('action');
- expect(wrapper.vm.$store.state.isTimelineEnabled).toBe(false);
- });
+ expect(store.state.isTimelineEnabled).toBe(false);
+ });
- it('disables commenting when "Show history only" filter is applied', () => {
- findFilter(DISCUSSION_FILTER_TYPES.HISTORY).trigger('click');
+ it('disables commenting when "Show history only" filter is applied', () => {
+ findFilter(DISCUSSION_FILTER_TYPES.HISTORY).vm.$emit('action');
- expect(wrapper.vm.$store.state.commentsDisabled).toBe(true);
- });
+ expect(store.state.commentsDisabled).toBe(true);
+ });
- it('enables commenting when "Show history only" filter is not applied', () => {
- findFilter(DISCUSSION_FILTER_TYPES.ALL).trigger('click');
+ it('enables commenting when "Show history only" filter is not applied', () => {
+ findFilter(DISCUSSION_FILTER_TYPES.ALL).vm.$emit('action');
- expect(wrapper.vm.$store.state.commentsDisabled).toBe(false);
+ expect(store.state.commentsDisabled).toBe(false);
+ });
});
describe('Merge request tabs', () => {
@@ -222,52 +230,41 @@ describe('DiscussionFilter component', () => {
});
describe('URL with Links to notes', () => {
+ const findGlDisclosureDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+
afterEach(() => {
window.location.hash = '';
});
- it('updates the filter when the URL links to a note', async () => {
- window.location.hash = `note_${discussionMock.notes[0].id}`;
- wrapper.vm.currentValue = discussionFiltersMock[2].value;
- wrapper.vm.handleLocationHash();
-
- await nextTick();
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
- });
-
it('does not update the filter when the current filter is "Show all activity"', async () => {
window.location.hash = `note_${discussionMock.notes[0].id}`;
- wrapper.vm.handleLocationHash();
+ wrapper = mountComponent();
await nextTick();
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
+ const filtered = findGlDisclosureDropdownItems().filter((el) => el.classes('is-active'));
+
+ expect(filtered).toHaveLength(1);
+ expect(filtered.at(0).text()).toBe(discussionFiltersMock[0].title);
});
it('only updates filter when the URL links to a note', async () => {
window.location.hash = `testing123`;
- wrapper.vm.handleLocationHash();
+ wrapper = mountComponent();
await nextTick();
- expect(wrapper.vm.currentValue).toBe(DISCUSSION_FILTERS_DEFAULT_VALUE);
- });
+ const filtered = findGlDisclosureDropdownItems().filter((el) => el.classes('is-active'));
- it('fetches discussions when there is a hash', async () => {
- window.location.hash = `note_${discussionMock.notes[0].id}`;
- wrapper.vm.currentValue = discussionFiltersMock[2].value;
- jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
- wrapper.vm.handleLocationHash();
-
- await nextTick();
- expect(wrapper.vm.selectFilter).toHaveBeenCalled();
+ expect(filtered).toHaveLength(1);
+ expect(filtered.at(0).text()).toBe(discussionFiltersMock[0].title);
});
it('does not fetch discussions when there is no hash', async () => {
window.location.hash = '';
- jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
- wrapper.vm.handleLocationHash();
+ const selectFilterSpy = jest.spyOn(wrapper.vm, 'selectFilter').mockImplementation(() => {});
+ wrapper = mountComponent();
await nextTick();
- expect(wrapper.vm.selectFilter).not.toHaveBeenCalled();
+ expect(selectFilterSpy).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/notes/components/discussion_navigator_spec.js b/spec/frontend/notes/components/discussion_navigator_spec.js
index 77ae7b2c3b5..885a7e2802e 100644
--- a/spec/frontend/notes/components/discussion_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_navigator_spec.js
@@ -1,5 +1,3 @@
-/* global Mousetrap */
-import 'mousetrap';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import {
@@ -7,6 +5,7 @@ import {
MR_NEXT_UNRESOLVED_DISCUSSION,
MR_PREVIOUS_UNRESOLVED_DISCUSSION,
} from '~/behaviors/shortcuts/keybindings';
+import { Mousetrap } from '~/lib/mousetrap';
import DiscussionNavigator from '~/notes/components/discussion_navigator.vue';
import eventHub from '~/notes/event_hub';
@@ -33,13 +32,6 @@ describe('notes/components/discussion_navigator', () => {
jumpToPreviousDiscussion = jest.fn();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- wrapper = null;
- });
-
describe('on create', () => {
let onSpy;
let vm;
diff --git a/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js b/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js
index 8d5ea108b50..d11ca7ad1ec 100644
--- a/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_replies_wrapper_spec.js
@@ -19,10 +19,6 @@ describe('DiscussionNotesRepliesWrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when normal discussion', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index add2ed1ba8a..bc0c04f2d8a 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -53,11 +53,6 @@ describe('DiscussionNotes', () => {
store.dispatch('setNotesData', notesDataMock);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('rendering', () => {
it('renders an element for each note in the discussion', () => {
createComponent();
diff --git a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
index 971e3987929..a9201b78669 100644
--- a/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
+++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
@@ -17,10 +17,6 @@ describe('ReplyPlaceholder', () => {
const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits focus event on button click', async () => {
createComponent({ options: { attachTo: document.body } });
diff --git a/spec/frontend/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js
index 17c3523cf48..4bd21842fec 100644
--- a/spec/frontend/notes/components/discussion_resolve_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js
@@ -23,10 +23,6 @@ describe('resolveDiscussionButton', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should emit a onClick event on button click', async () => {
const button = wrapper.findComponent(GlButton);
diff --git a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
index a185f11ffaa..3dfae45ec49 100644
--- a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
@@ -15,10 +15,6 @@ describe('ResolveWithIssueButton', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should have a link with the provided link property as href', () => {
const button = wrapper.findComponent(GlButton);
diff --git a/spec/frontend/notes/components/email_participants_warning_spec.js b/spec/frontend/notes/components/email_participants_warning_spec.js
index ab1a6b152a4..34b7524d8fb 100644
--- a/spec/frontend/notes/components/email_participants_warning_spec.js
+++ b/spec/frontend/notes/components/email_participants_warning_spec.js
@@ -4,11 +4,6 @@ import EmailParticipantsWarning from '~/notes/components/email_participants_warn
describe('Email Participants Warning Component', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findMoreButton = () => wrapper.find('button');
const createWrapper = (emails) => {
diff --git a/spec/frontend/notes/components/mr_discussion_filter_spec.js b/spec/frontend/notes/components/mr_discussion_filter_spec.js
new file mode 100644
index 00000000000..beb25c30af6
--- /dev/null
+++ b/spec/frontend/notes/components/mr_discussion_filter_spec.js
@@ -0,0 +1,110 @@
+import { mount } from '@vue/test-utils';
+import { GlCollapsibleListbox, GlListboxItem, GlButton } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import DiscussionFilter from '~/notes/components/mr_discussion_filter.vue';
+import { MR_FILTER_OPTIONS } from '~/notes/constants';
+
+Vue.use(Vuex);
+
+describe('Merge request discussion filter component', () => {
+ let wrapper;
+ let store;
+ let updateMergeRequestFilters;
+ let setDiscussionSortDirection;
+
+ function createComponent(mergeRequestFilters = MR_FILTER_OPTIONS.map((f) => f.value)) {
+ updateMergeRequestFilters = jest.fn();
+ setDiscussionSortDirection = jest.fn();
+
+ store = new Vuex.Store({
+ modules: {
+ notes: {
+ state: {
+ mergeRequestFilters,
+ discussionSortOrder: 'asc',
+ },
+ actions: {
+ updateMergeRequestFilters,
+ setDiscussionSortDirection,
+ },
+ },
+ },
+ });
+
+ wrapper = mount(DiscussionFilter, {
+ store,
+ });
+ }
+
+ afterEach(() => {
+ localStorage.removeItem('mr_activity_filters');
+ localStorage.removeItem('sort_direction_merge_request');
+ });
+
+ describe('local sync sort direction', () => {
+ it('calls setDiscussionSortDirection when mounted', () => {
+ localStorage.setItem('sort_direction_merge_request', 'desc');
+
+ createComponent();
+
+ expect(setDiscussionSortDirection).toHaveBeenCalledWith(expect.anything(), {
+ direction: 'desc',
+ });
+ });
+ });
+
+ describe('local sync sort filters', () => {
+ it('calls setDiscussionSortDirection when mounted', () => {
+ localStorage.setItem('mr_activity_filters', '["comments"]');
+
+ createComponent();
+
+ expect(updateMergeRequestFilters).toHaveBeenCalledWith(expect.anything(), ['comments']);
+ });
+ });
+
+ it('lists current filters', () => {
+ createComponent();
+
+ expect(wrapper.findAllComponents(GlListboxItem).length).toBe(MR_FILTER_OPTIONS.length);
+ });
+
+ it('updates store when selecting filter', async () => {
+ createComponent();
+
+ wrapper.findComponent(GlListboxItem).vm.$emit('select');
+
+ await nextTick();
+
+ wrapper.findComponent(GlCollapsibleListbox).vm.$emit('hidden');
+
+ expect(updateMergeRequestFilters).toHaveBeenCalledWith(expect.anything(), [
+ 'assignees_reviewers',
+ 'comments',
+ 'commit_branches',
+ 'edits',
+ 'labels',
+ 'lock_status',
+ 'mentions',
+ 'status',
+ 'tracking',
+ ]);
+ });
+
+ it.each`
+ state | expectedText
+ ${['status']} | ${'Merge request status'}
+ ${['status', 'comments']} | ${'Merge request status +1 more'}
+ ${[]} | ${'None'}
+ ${MR_FILTER_OPTIONS.map((f) => f.value)} | ${'All activity'}
+ `('updates toggle text to $expectedText with $state', async ({ state, expectedText }) => {
+ createComponent();
+
+ store.state.notes.mergeRequestFilters = state;
+
+ await nextTick();
+
+ expect(wrapper.findComponent(GlButton).text()).toBe(expectedText);
+ });
+});
diff --git a/spec/frontend/notes/components/note_actions/reply_button_spec.js b/spec/frontend/notes/components/note_actions/reply_button_spec.js
index 20b32b8c178..68b11fb3b1a 100644
--- a/spec/frontend/notes/components/note_actions/reply_button_spec.js
+++ b/spec/frontend/notes/components/note_actions/reply_button_spec.js
@@ -9,11 +9,6 @@ describe('ReplyButton', () => {
wrapper = shallowMount(ReplyButton);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('emits startReplying on click', () => {
wrapper.findComponent(GlButton).vm.$emit('click');
diff --git a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
index 658e844a9b1..7860e9d45da 100644
--- a/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
+++ b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js
@@ -20,13 +20,9 @@ describe('NoteTimelineEventButton', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTimelineButton = () => wrapper.findComponent(GlButton);
- it('emits click-promote-comment-to-event', async () => {
+ it('emits click-promote-comment-to-event', () => {
findTimelineButton().vm.$emit('click');
expect(wrapper.emitted('click-promote-comment-to-event')).toEqual([[emitData]]);
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index 8630b7b7d07..879bada4aee 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -1,9 +1,10 @@
-import { mount, createWrapper } from '@vue/test-utils';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import noteActions from '~/notes/components/note_actions.vue';
import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
@@ -19,6 +20,8 @@ describe('noteActions', () => {
let actions;
let axiosMock;
+ const mockCloseDropdown = jest.fn();
+
const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx);
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
const findTimelineButton = () => wrapper.findComponent(TimelineEventButton);
@@ -45,6 +48,14 @@ describe('noteActions', () => {
store,
propsData,
computed,
+ stubs: {
+ GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
+ methods: {
+ close: mockCloseDropdown,
+ },
+ }),
+ GlDisclosureDropdownItem,
+ },
});
};
@@ -77,7 +88,6 @@ describe('noteActions', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -145,17 +155,6 @@ describe('noteActions', () => {
expect(wrapper.find('.js-note-delete').exists()).toBe(true);
});
- it('closes tooltip when dropdown opens', async () => {
- wrapper.find('.more-actions-toggle').trigger('click');
-
- const rootWrapper = createWrapper(wrapper.vm.$root);
-
- await nextTick();
- const emitted = Object.keys(rootWrapper.emitted());
-
- expect(emitted).toEqual([BV_HIDE_TOOLTIP]);
- });
-
it('should not be possible to assign or unassign the comment author in a merge request', () => {
const assignUserButton = wrapper.find('[data-testid="assign-user"]');
expect(assignUserButton.exists()).toBe(false);
@@ -176,6 +175,11 @@ describe('noteActions', () => {
const { resolveButton } = wrapper.vm.$refs;
expect(resolveButton.$el.getAttribute('title')).toBe(`Resolved by ${complexUnescapedName}`);
});
+
+ it('closes the dropdown', () => {
+ findReportAbuseButton().vm.$emit('action');
+ expect(mockCloseDropdown).toHaveBeenCalled();
+ });
});
});
@@ -203,7 +207,6 @@ describe('noteActions', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -226,7 +229,6 @@ describe('noteActions', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -248,10 +250,6 @@ describe('noteActions', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should not be possible to assign the comment author', testButtonDoesNotRender);
it('should not be possible to unassign the comment author', testButtonDoesNotRender);
});
@@ -408,13 +406,13 @@ describe('noteActions', () => {
});
it('opens the drawer when report abuse button is clicked', async () => {
- await findReportAbuseButton().trigger('click');
+ await findReportAbuseButton().vm.$emit('action');
expect(findAbuseCategorySelector().props('showDrawer')).toEqual(true);
});
it('closes the drawer', async () => {
- await findReportAbuseButton().trigger('click');
+ await findReportAbuseButton().vm.$emit('action');
findAbuseCategorySelector().vm.$emit('close-drawer');
await nextTick();
diff --git a/spec/frontend/notes/components/note_attachment_spec.js b/spec/frontend/notes/components/note_attachment_spec.js
index 24632f8e427..7f44171f6cc 100644
--- a/spec/frontend/notes/components/note_attachment_spec.js
+++ b/spec/frontend/notes/components/note_attachment_spec.js
@@ -15,11 +15,6 @@ describe('Issue note attachment', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders attachment image if it is passed in attachment prop', () => {
createComponent({
image: 'test-image',
diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js
index 89ac0216f41..0107b27f980 100644
--- a/spec/frontend/notes/components/note_awards_list_spec.js
+++ b/spec/frontend/notes/components/note_awards_list_spec.js
@@ -1,76 +1,110 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
+import Vuex from 'vuex';
import { TEST_HOST } from 'helpers/test_constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { userDataMock } from 'jest/notes/mock_data';
+import EmojiPicker from '~/emoji/components/picker.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import awardsNote from '~/notes/components/note_awards_list.vue';
import createStore from '~/notes/stores';
-import { noteableDataMock, notesDataMock } from '../mock_data';
-describe('note_awards_list component', () => {
- let store;
- let vm;
- let awardsMock;
- let mock;
-
- const toggleAwardPath = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`;
-
- beforeEach(() => {
- mock = new AxiosMockAdapter(axios);
-
- mock.onPost(toggleAwardPath).reply(HTTP_STATUS_OK, '');
+Vue.use(Vuex);
- const Component = Vue.extend(awardsNote);
-
- store = createStore();
- store.dispatch('setNoteableData', noteableDataMock);
- store.dispatch('setNotesData', notesDataMock);
- awardsMock = [
- {
- name: 'flag_tz',
- user: { id: 1, name: 'Administrator', username: 'root' },
- },
- {
- name: 'cartwheel_tone3',
- user: { id: 12, name: 'Bobbie Stehr', username: 'erin' },
- },
- ];
+describe('Note Awards List', () => {
+ let wrapper;
+ let mock;
- vm = new Component({
+ const awardsMock = [
+ {
+ name: 'flag_tz',
+ user: { id: 1, name: 'Administrator', username: 'root' },
+ },
+ {
+ name: 'cartwheel_tone3',
+ user: { id: 12, name: 'Bobbie Stehr', username: 'erin' },
+ },
+ ];
+ const toggleAwardPathMock = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`;
+
+ const defaultProps = {
+ awards: awardsMock,
+ noteAuthorId: 2,
+ noteId: '545',
+ canAwardEmoji: false,
+ toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
+ };
+
+ const findAddAward = () => wrapper.find('.js-add-award');
+ const findAwardButton = () => wrapper.findByTestId('award-button');
+ const findAllEmojiAwards = () => wrapper.findAll('gl-emoji');
+ const findEmojiPicker = () => wrapper.findComponent(EmojiPicker);
+
+ const createComponent = (props = defaultProps, store = createStore()) => {
+ wrapper = mountExtended(awardsNote, {
store,
propsData: {
- awards: awardsMock,
- noteAuthorId: 2,
- noteId: '545',
- canAwardEmoji: true,
- toggleAwardPath,
+ ...props,
},
- }).$mount();
- });
+ });
+ };
+
+ describe('Note Awards functionality', () => {
+ const toggleAwardRequestSpy = jest.fn();
+ const fakeStore = () => {
+ return new Vuex.Store({
+ getters: {
+ getUserData: () => userDataMock,
+ },
+ actions: {
+ toggleAwardRequest: toggleAwardRequestSpy,
+ },
+ });
+ };
- afterEach(() => {
- mock.restore();
- vm.$destroy();
- });
+ beforeEach(() => {
+ mock = new AxiosMockAdapter(axios);
+ mock.onPost(toggleAwardPathMock).reply(HTTP_STATUS_OK, '');
- it('should render awarded emojis', () => {
- expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined();
- expect(
- vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]'),
- ).toBeDefined();
- });
+ createComponent(
+ {
+ awards: awardsMock,
+ noteAuthorId: 2,
+ noteId: '545',
+ canAwardEmoji: true,
+ toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
+ },
+ fakeStore(),
+ );
+ });
- it('should be possible to remove awarded emoji', () => {
- jest.spyOn(vm, 'handleAward');
- jest.spyOn(vm, 'toggleAwardRequest');
- vm.$el.querySelector('.js-awards-block button').click();
+ afterEach(() => {
+ mock.restore();
+ });
- expect(vm.handleAward).toHaveBeenCalledWith('flag_tz');
- expect(vm.toggleAwardRequest).toHaveBeenCalled();
- });
+ it('should render awarded emojis', () => {
+ const emojiAwards = findAllEmojiAwards();
+
+ expect(emojiAwards).toHaveLength(awardsMock.length);
+ expect(emojiAwards.at(0).attributes('data-name')).toBe('flag_tz');
+ expect(emojiAwards.at(1).attributes('data-name')).toBe('cartwheel_tone3');
+ });
+
+ it('should be possible to add new emoji', () => {
+ expect(findEmojiPicker().exists()).toBe(true);
+ });
+
+ it('should be possible to remove awarded emoji', async () => {
+ await findAwardButton().vm.$emit('click');
- it('should be possible to add new emoji', () => {
- expect(vm.$el.querySelector('.js-add-award')).toBeDefined();
+ const { toggleAwardPath, noteId } = defaultProps;
+ expect(toggleAwardRequestSpy).toHaveBeenCalledWith(expect.anything(), {
+ awardName: awardsMock[0].name,
+ endpoint: toggleAwardPath,
+ noteId,
+ });
+ });
});
describe('when the user name contains special HTML characters', () => {
@@ -79,85 +113,69 @@ describe('note_awards_list component', () => {
user: { id: index, name: `&<>"\`'-${index}`, username: `user-${index}` },
});
- const mountComponent = () => {
- const Component = Vue.extend(awardsNote);
- vm = new Component({
- store,
- propsData: {
- awards: awardsMock,
- noteAuthorId: 0,
- noteId: '545',
- canAwardEmoji: true,
- toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
- },
- }).$mount();
+ const customProps = {
+ awards: awardsMock,
+ noteAuthorId: 0,
+ noteId: '545',
+ canAwardEmoji: true,
+ toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
};
- const findTooltip = () => vm.$el.querySelector('[title]').getAttribute('title');
-
- it('should only escape & and " characters', () => {
- awardsMock = [...new Array(1)].map(createAwardEmoji);
- mountComponent();
- const escapedName = awardsMock[0].user.name.replace(/&/g, '&amp;').replace(/"/g, '&quot;');
-
- expect(vm.$el.querySelector('[title]').outerHTML).toContain(escapedName);
- });
-
it('should not escape special HTML characters twice when only 1 person awarded', () => {
- awardsMock = [...new Array(1)].map(createAwardEmoji);
- mountComponent();
+ const awardsCopy = [...new Array(1)].map(createAwardEmoji);
+ createComponent({
+ ...customProps,
+ awards: awardsCopy,
+ });
- awardsMock.forEach((award) => {
- expect(findTooltip()).toContain(award.user.name);
+ awardsCopy.forEach((award) => {
+ expect(findAwardButton().attributes('title')).toContain(award.user.name);
});
});
it('should not escape special HTML characters twice when 2 people awarded', () => {
- awardsMock = [...new Array(2)].map(createAwardEmoji);
- mountComponent();
+ const awardsCopy = [...new Array(2)].map(createAwardEmoji);
+ createComponent({
+ ...customProps,
+ awards: awardsCopy,
+ });
- awardsMock.forEach((award) => {
- expect(findTooltip()).toContain(award.user.name);
+ awardsCopy.forEach((award) => {
+ expect(findAwardButton().attributes('title')).toContain(award.user.name);
});
});
it('should not escape special HTML characters twice when more than 10 people awarded', () => {
- awardsMock = [...new Array(11)].map(createAwardEmoji);
- mountComponent();
+ const awardsCopy = [...new Array(11)].map(createAwardEmoji);
+ createComponent({
+ ...customProps,
+ awards: awardsCopy,
+ });
// Testing only the first 10 awards since 11 onward will not be displayed.
- awardsMock.slice(0, 10).forEach((award) => {
- expect(findTooltip()).toContain(award.user.name);
+ awardsCopy.slice(0, 10).forEach((award) => {
+ expect(findAwardButton().attributes('title')).toContain(award.user.name);
});
});
});
- describe('when the user cannot award emoji', () => {
+ describe('when the user cannot award an emoji', () => {
beforeEach(() => {
- const Component = Vue.extend(awardsNote);
-
- vm = new Component({
- store,
- propsData: {
- awards: awardsMock,
- noteAuthorId: 2,
- noteId: '545',
- canAwardEmoji: false,
- toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
- },
- }).$mount();
+ createComponent({
+ awards: awardsMock,
+ noteAuthorId: 2,
+ noteId: '545',
+ canAwardEmoji: false,
+ toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji',
+ });
});
- it('should not be possible to remove awarded emoji', () => {
- jest.spyOn(vm, 'toggleAwardRequest');
-
- vm.$el.querySelector('.js-awards-block button').click();
-
- expect(vm.toggleAwardRequest).not.toHaveBeenCalled();
+ it('should display an award emoji button with a disabled class', () => {
+ expect(findAwardButton().classes()).toContain('disabled');
});
it('should not be possible to add new emoji', () => {
- expect(vm.$el.querySelector('.js-add-award')).toBeNull();
+ expect(findAddAward().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index c71cf7666ab..c4f8e50b969 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -7,10 +7,7 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import notes from '~/notes/stores/modules/index';
-import Autosave from '~/autosave';
-
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
-
import { noteableDataMock, notesDataMock, note } from '../mock_data';
jest.mock('~/autosave');
@@ -49,10 +46,6 @@ describe('issue_note_body component', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render the note', () => {
expect(wrapper.find('.note-text').html()).toContain(note.note_html);
});
@@ -86,11 +79,6 @@ describe('issue_note_body component', () => {
expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText);
});
- it('adds autosave', () => {
- // passing undefined instead of an element because of shallowMount
- expect(Autosave).toHaveBeenCalledWith(undefined, ['Note', note.noteable_type, note.id]);
- });
-
describe('isInternalNote', () => {
beforeEach(() => {
wrapper.setProps({ isInternalNote: true });
diff --git a/spec/frontend/notes/components/note_edited_text_spec.js b/spec/frontend/notes/components/note_edited_text_spec.js
index 0a5fe48ef94..577e1044588 100644
--- a/spec/frontend/notes/components/note_edited_text_spec.js
+++ b/spec/frontend/notes/components/note_edited_text_spec.js
@@ -1,3 +1,4 @@
+import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import NoteEditedText from '~/notes/components/note_edited_text.vue';
@@ -5,41 +6,63 @@ const propsData = {
actionText: 'Edited',
className: 'foo-bar',
editedAt: '2017-08-04T09:52:31.062Z',
- editedBy: {
- avatar_url: 'path',
- id: 1,
- name: 'Root',
- path: '/root',
- state: 'active',
- username: 'root',
- },
+ editedBy: null,
};
describe('NoteEditedText', () => {
let wrapper;
- beforeEach(() => {
+ const createWrapper = (props = {}) => {
wrapper = shallowMount(NoteEditedText, {
- propsData,
+ propsData: {
+ ...propsData,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
});
- });
+ };
- afterEach(() => {
- wrapper.destroy();
- });
+ const findUserElement = () => wrapper.findComponent(GlLink);
- it('should render block with provided className', () => {
- expect(wrapper.classes()).toContain(propsData.className);
- });
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
- it('should render provided actionText', () => {
- expect(wrapper.text().trim()).toContain(propsData.actionText);
+ it('should render block with provided className', () => {
+ expect(wrapper.classes()).toContain(propsData.className);
+ });
+
+ it('should render provided actionText', () => {
+ expect(wrapper.text().trim()).toContain(propsData.actionText);
+ });
+
+ it('should not render user information', () => {
+ expect(findUserElement().exists()).toBe(false);
+ });
});
- it('should render provided user information', () => {
- const authorLink = wrapper.find('.js-user-link');
+ describe('edited note', () => {
+ const editedBy = {
+ avatar_url: 'path',
+ id: 1,
+ name: 'Root',
+ path: '/root',
+ state: 'active',
+ username: 'root',
+ };
+
+ beforeEach(() => {
+ createWrapper({ editedBy });
+ });
+
+ it('should render user information', () => {
+ const authorLink = findUserElement();
- expect(authorLink.attributes('href')).toEqual(propsData.editedBy.path);
- expect(authorLink.text().trim()).toEqual(propsData.editedBy.name);
+ expect(authorLink.attributes('href')).toEqual(editedBy.path);
+ expect(authorLink.text().trim()).toEqual(editedBy.name);
+ });
});
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 90473e7ccba..b5b33607282 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -1,42 +1,39 @@
-import { GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlLink, GlFormCheckbox } from '@gitlab/ui';
import { nextTick } from 'vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
-import { getDraft, updateDraft } from '~/lib/utils/autosave';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete';
+import eventHub from '~/environments/event_hub';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data';
jest.mock('~/lib/utils/autosave');
describe('issue_note_form component', () => {
- const dummyAutosaveKey = 'some-autosave-key';
- const dummyDraft = 'dummy draft content';
-
let store;
let wrapper;
let props;
- const createComponentWrapper = () => {
- return mount(NoteForm, {
+ const createComponentWrapper = (propsData = {}, provide = {}) => {
+ wrapper = mountExtended(NoteForm, {
store,
- propsData: props,
+ propsData: {
+ ...props,
+ ...propsData,
+ },
+ provide: {
+ glFeatures: provide,
+ },
});
};
- const findCancelButton = () => wrapper.find('[data-testid="cancel"]');
+ const findCancelButton = () => wrapper.findByTestId('cancel');
+ const findCancelCommentButton = () => wrapper.findByTestId('cancelBatchCommentsEnabled');
+ const findMarkdownField = () => wrapper.findComponent(MarkdownField);
beforeEach(() => {
- getDraft.mockImplementation((key) => {
- if (key === dummyAutosaveKey) {
- return dummyDraft;
- }
-
- return null;
- });
-
store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
@@ -48,33 +45,39 @@ describe('issue_note_form component', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('noteHash', () => {
beforeEach(() => {
- wrapper = createComponentWrapper();
+ createComponentWrapper();
});
it('returns note hash string based on `noteId`', () => {
expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`);
});
- it('return note hash as `#` when `noteId` is empty', async () => {
- wrapper.setProps({
- ...props,
+ it('return note hash as `#` when `noteId` is empty', () => {
+ createComponentWrapper({
noteId: '',
});
- await nextTick();
expect(wrapper.vm.noteHash).toBe('#');
});
});
+ it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
+ createComponentWrapper({}, { contentEditorOnIssues: false });
+
+ expect(wrapper.text()).not.toContain('Switch to rich text');
+ });
+
+ it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
+ createComponentWrapper({}, { contentEditorOnIssues: true });
+
+ expect(wrapper.text()).toContain('Switch to rich text');
+ });
+
describe('conflicts editing', () => {
beforeEach(() => {
- wrapper = createComponentWrapper();
+ createComponentWrapper();
});
it('should show conflict message if note changes outside the component', async () => {
@@ -98,15 +101,13 @@ describe('issue_note_form component', () => {
describe('form', () => {
beforeEach(() => {
- wrapper = createComponentWrapper();
+ createComponentWrapper();
});
it('should render text area with placeholder', () => {
const textarea = wrapper.find('textarea');
- expect(textarea.attributes('placeholder')).toEqual(
- 'Write a comment or drag your files here…',
- );
+ expect(textarea.attributes('placeholder')).toBe('Write a comment or drag your files here…');
});
it('should set data-supports-quick-actions to enable autocomplete', () => {
@@ -121,23 +122,21 @@ describe('issue_note_form component', () => {
${true} | ${'Write an internal note or drag your files here…'}
`(
'should set correct textarea placeholder text when discussion confidentiality is $internal',
- ({ internal, placeholder }) => {
+ async ({ internal, placeholder }) => {
props.note = {
...note,
internal,
};
- wrapper = createComponentWrapper();
+ createComponentWrapper();
+
+ await nextTick();
expect(wrapper.find('textarea').attributes('placeholder')).toBe(placeholder);
},
);
it('should link to markdown docs', () => {
- const { markdownDocsPath } = notesDataMock;
- const markdownField = wrapper.findComponent(MarkdownField);
- const markdownFieldProps = markdownField.props();
-
- expect(markdownFieldProps.markdownDocsPath).toBe(markdownDocsPath);
+ expect(findMarkdownField().props('markdownDocsPath')).toBe(notesDataMock.markdownDocsPath);
});
describe('keyboard events', () => {
@@ -150,12 +149,11 @@ describe('issue_note_form component', () => {
describe('up', () => {
it('should ender edit mode', () => {
- // TODO: do not spy on vm
- jest.spyOn(wrapper.vm, 'editMyLastNote');
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
textarea.trigger('keydown.up');
- expect(wrapper.vm.editMyLastNote).toHaveBeenCalled();
+ expect(eventHubSpy).not.toHaveBeenCalled();
});
});
@@ -163,17 +161,13 @@ describe('issue_note_form component', () => {
it('should save note when cmd+enter is pressed', () => {
textarea.trigger('keydown.enter', { metaKey: true });
- const { handleFormUpdate } = wrapper.emitted();
-
- expect(handleFormUpdate.length).toBe(1);
+ expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1);
});
it('should save note when ctrl+enter is pressed', () => {
textarea.trigger('keydown.enter', { ctrlKey: true });
- const { handleFormUpdate } = wrapper.emitted();
-
- expect(handleFormUpdate.length).toBe(1);
+ expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1);
});
it('should disable textarea when ctrl+enter is pressed', async () => {
@@ -183,157 +177,68 @@ describe('issue_note_form component', () => {
await nextTick();
- expect(textarea.attributes('disabled')).toBe('disabled');
+ expect(textarea.attributes('disabled')).toBeDefined();
});
});
});
describe('actions', () => {
- it('should be possible to cancel', async () => {
- wrapper.setProps({
- ...props,
- });
- await nextTick();
+ it('should be possible to cancel', () => {
+ createComponentWrapper();
- const cancelButton = findCancelButton();
- cancelButton.vm.$emit('click');
- await nextTick();
+ findCancelButton().vm.$emit('click');
- expect(wrapper.emitted().cancelForm).toHaveLength(1);
+ expect(wrapper.emitted('cancelForm')).toHaveLength(1);
});
it('will not cancel form if there is an active at-who-active class', async () => {
- wrapper.setProps({
- ...props,
- });
- await nextTick();
+ createComponentWrapper();
- const textareaEl = wrapper.vm.$refs.textarea;
+ const textareaEl = wrapper.vm.$refs.markdownEditor.$el.querySelector('textarea');
const cancelButton = findCancelButton();
textareaEl.classList.add(AT_WHO_ACTIVE_CLASS);
cancelButton.vm.$emit('click');
await nextTick();
- expect(wrapper.emitted().cancelForm).toBeUndefined();
+ expect(wrapper.emitted('cancelForm')).toBeUndefined();
});
- it('should be possible to update the note', async () => {
- wrapper.setProps({
- ...props,
- });
- await nextTick();
+ it('should be possible to update the note', () => {
+ createComponentWrapper();
const textarea = wrapper.find('textarea');
textarea.setValue('Foo');
const saveButton = wrapper.find('.js-vue-issue-save');
saveButton.vm.$emit('click');
- expect(wrapper.vm.isSubmitting).toBe(true);
+ expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1);
});
});
});
- describe('with autosaveKey', () => {
- describe('with draft', () => {
- beforeEach(() => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- });
- wrapper = createComponentWrapper();
-
- return nextTick();
- });
-
- it('displays the draft in textarea', () => {
- const textarea = wrapper.find('textarea');
-
- expect(textarea.element.value).toBe(dummyDraft);
- });
- });
-
- describe('without draft', () => {
- beforeEach(() => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: 'some key without draft',
- });
- wrapper = createComponentWrapper();
-
- return nextTick();
- });
-
- it('leaves the textarea empty', () => {
- const textarea = wrapper.find('textarea');
-
- expect(textarea.element.value).toBe('');
- });
- });
-
- it('updates the draft if textarea content changes', () => {
- Object.assign(props, {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- });
- wrapper = createComponentWrapper();
- const textarea = wrapper.find('textarea');
- const dummyContent = 'some new content';
-
- textarea.setValue(dummyContent);
-
- expect(updateDraft).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
- });
-
- it('does not save draft when ctrl+enter is pressed', () => {
- const options = {
- noteBody: '',
- autosaveKey: dummyAutosaveKey,
- };
-
- props = { ...props, ...options };
- wrapper = createComponentWrapper();
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isSubmittingWithKeydown: true });
-
- const textarea = wrapper.find('textarea');
- textarea.setValue('some content');
- textarea.trigger('keydown.enter', { metaKey: true });
-
- expect(updateDraft).not.toHaveBeenCalled();
- });
- });
-
describe('with batch comments', () => {
beforeEach(() => {
store.registerModule('batchComments', batchComments());
- wrapper = createComponentWrapper();
- wrapper.setProps({
- ...props,
+ createComponentWrapper({
isDraft: true,
noteId: '',
discussion: { ...discussionMock, for_commit: false },
});
});
- it('should be possible to cancel', async () => {
- jest.spyOn(wrapper.vm, 'cancelHandler');
+ it('should be possible to cancel', () => {
+ findCancelCommentButton().vm.$emit('click');
- await nextTick();
- const cancelButton = wrapper.find('[data-testid="cancelBatchCommentsEnabled"]');
- cancelButton.vm.$emit('click');
-
- expect(wrapper.vm.cancelHandler).toHaveBeenCalledWith(true);
+ expect(wrapper.emitted('cancelForm')).toEqual([[true, false]]);
});
it('shows resolve checkbox', () => {
- expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(true);
+ expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true);
});
- it('hides resolve checkbox', async () => {
- wrapper.setProps({
+ it('hides resolve checkbox', () => {
+ createComponentWrapper({
isDraft: false,
discussion: {
...discussionMock,
@@ -348,15 +253,11 @@ describe('issue_note_form component', () => {
},
});
- await nextTick();
-
- expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(false);
+ expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(false);
});
- it('hides actions for commits', async () => {
- wrapper.setProps({ discussion: { for_commit: true } });
-
- await nextTick();
+ it('hides actions for commits', () => {
+ createComponentWrapper({ discussion: { for_commit: true } });
expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review');
});
@@ -365,13 +266,12 @@ describe('issue_note_form component', () => {
it('should start review or add to review when cmd+enter is pressed', async () => {
const textarea = wrapper.find('textarea');
- jest.spyOn(wrapper.vm, 'handleAddToReview');
-
textarea.setValue('Foo');
textarea.trigger('keydown.enter', { metaKey: true });
await nextTick();
- expect(wrapper.vm.handleAddToReview).toHaveBeenCalled();
+
+ expect(wrapper.emitted('handleFormUpdateAddToReview')).toEqual([['Foo', false]]);
});
});
});
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 56c22b09e1b..60ad9e3344a 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -19,7 +19,9 @@ describe('NoteHeader component', () => {
const findTimestampLink = () => wrapper.findComponent({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.findComponent({ ref: 'noteTimestamp' });
const findInternalNoteIndicator = () => wrapper.findByTestId('internal-note-indicator');
+ const findAuthorName = () => wrapper.findByTestId('author-name');
const findSpinner = () => wrapper.findComponent({ ref: 'spinner' });
+ const authorUsernameLink = () => wrapper.findComponent({ ref: 'authorUsernameLink' });
const statusHtml =
'"<span class="user-status-emoji has-tooltip" title="foo bar" data-html="true" data-placement="top"><gl-emoji title="basketball and hoop" data-name="basketball" data-unicode-version="6.0">🏀</gl-emoji></span>"';
@@ -35,6 +37,17 @@ describe('NoteHeader component', () => {
status_tooltip_html: statusHtml,
};
+ const supportBotAuthor = {
+ avatar_url: null,
+ id: 1,
+ name: 'Gitlab Support Bot',
+ path: '/support-bot',
+ state: 'active',
+ username: 'support-bot',
+ show_status: true,
+ status_tooltip_html: statusHtml,
+ };
+
const createComponent = (props) => {
wrapper = shallowMountExtended(NoteHeader, {
store: new Vuex.Store({
@@ -44,11 +57,6 @@ describe('NoteHeader component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('does not render discussion actions when includeToggle is false', () => {
createComponent({
includeToggle: false,
@@ -119,6 +127,16 @@ describe('NoteHeader component', () => {
expect(wrapper.text()).toContain('A deleted user');
});
+ it('renders participant email when author is a support-bot', () => {
+ createComponent({
+ author: supportBotAuthor,
+ emailParticipant: 'email@example.com',
+ });
+
+ expect(findAuthorName().text()).toBe('email@example.com');
+ expect(authorUsernameLink().exists()).toBe(false);
+ });
+
it('does not render created at information if createdAt is not passed as a prop', () => {
createComponent();
@@ -209,16 +227,15 @@ describe('NoteHeader component', () => {
it('toggles hover specific CSS classes on author name link', async () => {
createComponent({ author });
- const authorUsernameLink = wrapper.findComponent({ ref: 'authorUsernameLink' });
const authorNameLink = wrapper.findComponent({ ref: 'authorNameLink' });
- authorUsernameLink.trigger('mouseenter');
+ authorUsernameLink().trigger('mouseenter');
await nextTick();
expect(authorNameLink.classes()).toContain('hover');
expect(authorNameLink.classes()).toContain('text-underline');
- authorUsernameLink.trigger('mouseleave');
+ authorUsernameLink().trigger('mouseleave');
await nextTick();
expect(authorNameLink.classes()).not.toContain('hover');
diff --git a/spec/frontend/notes/components/note_signed_out_widget_spec.js b/spec/frontend/notes/components/note_signed_out_widget_spec.js
index 84f20e4ad58..d56ee234cd9 100644
--- a/spec/frontend/notes/components/note_signed_out_widget_spec.js
+++ b/spec/frontend/notes/components/note_signed_out_widget_spec.js
@@ -12,10 +12,6 @@ describe('NoteSignedOutWidget component', () => {
wrapper = shallowMount(NoteSignedOutWidget, { store });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders sign in link provided in the store', () => {
expect(wrapper.find(`a[href="${notesDataMock.newSessionPath}"]`).text()).toBe('sign in');
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index a90d8bdde06..ac0c037fe36 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -22,7 +22,6 @@ jest.mock('~/behaviors/markdown/render_gfm');
describe('noteable_discussion component', () => {
let store;
let wrapper;
- let originalGon;
beforeEach(() => {
window.mrTabs = {};
@@ -36,10 +35,6 @@ describe('noteable_discussion component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should not render thread header for non diff threads', () => {
expect(wrapper.find('.discussion-header').exists()).toBe(false);
});
@@ -167,16 +162,6 @@ describe('noteable_discussion component', () => {
});
describe('signout widget', () => {
- beforeEach(() => {
- originalGon = { ...window.gon };
- window.gon = window.gon || {};
- });
-
- afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
- });
-
describe('user is logged in', () => {
beforeEach(() => {
window.gon.current_user_id = userDataMock.id;
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index af1b4f64037..5d81a7a9a0f 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -1,7 +1,7 @@
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { GlAvatar } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DiffsModule from '~/diffs/store/modules';
import NoteActions from '~/notes/components/note_actions.vue';
@@ -37,7 +37,9 @@ describe('issue_note', () => {
const REPORT_ABUSE_PATH = '/abuse_reports/add_category';
- const findMultilineComment = () => wrapper.find('[data-testid="multiline-comment"]');
+ const findNoteBody = () => wrapper.findComponent(NoteBody);
+
+ const findMultilineComment = () => wrapper.findByTestId('multiline-comment');
const createWrapper = (props = {}, storeUpdater = (s) => s) => {
store = new Vuex.Store(
@@ -52,7 +54,7 @@ describe('issue_note', () => {
store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
- wrapper = mount(issueNote, {
+ wrapper = mountExtended(issueNote, {
store,
propsData: {
note,
@@ -71,10 +73,6 @@ describe('issue_note', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('mutiline comments', () => {
beforeEach(() => {
createWrapper();
@@ -254,21 +252,17 @@ describe('issue_note', () => {
});
it('should render issue body', () => {
- const noteBody = wrapper.findComponent(NoteBody);
- const noteBodyProps = noteBody.props();
-
- expect(noteBodyProps.note).toBe(note);
- expect(noteBodyProps.line).toBe(null);
- expect(noteBodyProps.canEdit).toBe(note.current_user.can_edit);
- expect(noteBodyProps.isEditing).toBe(false);
- expect(noteBodyProps.helpPagePath).toBe('');
+ expect(findNoteBody().props().note).toBe(note);
+ expect(findNoteBody().props().line).toBe(null);
+ expect(findNoteBody().props().canEdit).toBe(note.current_user.can_edit);
+ expect(findNoteBody().props().isEditing).toBe(false);
+ expect(findNoteBody().props().helpPagePath).toBe('');
});
it('prevents note preview xss', async () => {
const noteBody =
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" onload="alert(1)" />';
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
- const noteBodyComponent = wrapper.findComponent(NoteBody);
store.hotUpdate({
modules: {
@@ -281,7 +275,7 @@ describe('issue_note', () => {
},
});
- noteBodyComponent.vm.$emit('handleFormUpdate', {
+ findNoteBody().vm.$emit('handleFormUpdate', {
noteText: noteBody,
parentElement: null,
callback: () => {},
@@ -289,7 +283,7 @@ describe('issue_note', () => {
await waitForPromises();
expect(alertSpy).not.toHaveBeenCalled();
- expect(wrapper.vm.note.note_html).toBe(
+ expect(findNoteBody().props().note.note_html).toBe(
'<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">',
);
});
@@ -325,26 +319,21 @@ describe('issue_note', () => {
},
},
});
- const noteBody = wrapper.findComponent(NoteBody);
- noteBody.vm.resetAutoSave = () => {};
- noteBody.vm.$emit('handleFormUpdate', {
+ findNoteBody().vm.$emit('handleFormUpdate', {
noteText: updatedText,
parentElement: null,
callback: () => {},
});
await nextTick();
- let noteBodyProps = noteBody.props();
- expect(noteBodyProps.note.note_html).toBe(`<p>${updatedText}</p>\n`);
+ expect(findNoteBody().props().note.note_html).toBe(`<p dir="auto">${updatedText}</p>\n`);
- noteBody.vm.$emit('cancelForm', {});
+ findNoteBody().vm.$emit('cancelForm', {});
await nextTick();
- noteBodyProps = noteBody.props();
-
- expect(noteBodyProps.note.note_html).toBe(note.note_html);
+ expect(findNoteBody().props().note.note_html).toBe(note.note_html);
});
});
@@ -375,14 +364,23 @@ describe('issue_note', () => {
it('responds to handleFormUpdate', () => {
createWrapper();
updateActions();
- wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
+ findNoteBody().vm.$emit('handleFormUpdate', params);
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
});
+ it('should not update note with sensitive token', () => {
+ const sensitiveMessage = 'token: glpat-1234567890abcdefghij';
+
+ createWrapper();
+ updateActions();
+ findNoteBody().vm.$emit('handleFormUpdate', { ...params, noteText: sensitiveMessage });
+ expect(updateNote).not.toHaveBeenCalled();
+ });
+
it('does not stringify empty position', () => {
createWrapper();
updateActions();
- wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
+ findNoteBody().vm.$emit('handleFormUpdate', params);
expect(updateNote.mock.calls[0][1].note.note.position).toBeUndefined();
});
@@ -391,7 +389,7 @@ describe('issue_note', () => {
const expectation = JSON.stringify(position);
createWrapper({ note: { ...note, position } });
updateActions();
- wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
+ findNoteBody().vm.$emit('handleFormUpdate', params);
expect(updateNote.mock.calls[0][1].note.note.position).toBe(expectation);
});
});
@@ -416,7 +414,7 @@ describe('issue_note', () => {
createWrapper({ note: noteDef, discussionFile: null }, storeUpdater);
- expect(wrapper.vm.diffFile).toBe(null);
+ expect(findNoteBody().props().file).toBe(null);
},
);
@@ -434,7 +432,7 @@ describe('issue_note', () => {
},
);
- expect(wrapper.vm.diffFile.testId).toBe('diffFileTest');
+ expect(findNoteBody().props().file.testId).toBe('diffFileTest');
});
it('returns the provided diff file if the more robust getters fail', () => {
@@ -450,7 +448,7 @@ describe('issue_note', () => {
},
);
- expect(wrapper.vm.diffFile.testId).toBe('diffFileTest');
+ expect(findNoteBody().props().file.testId).toBe('diffFileTest');
});
});
});
diff --git a/spec/frontend/notes/components/notes_activity_header_spec.js b/spec/frontend/notes/components/notes_activity_header_spec.js
index 5b3165bf401..2de491477b6 100644
--- a/spec/frontend/notes/components/notes_activity_header_spec.js
+++ b/spec/frontend/notes/components/notes_activity_header_spec.js
@@ -24,10 +24,6 @@ describe('~/notes/components/notes_activity_header.vue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index b08a22f8674..cdfe8b02b48 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -90,8 +90,9 @@ describe('note_app', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
});
describe('render', () => {
@@ -121,15 +122,19 @@ describe('note_app', () => {
);
});
- it('should render form comment button as disabled', () => {
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/410409
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('should render form comment button as disabled', () => {
expect(findCommentButton().props('disabled')).toEqual(true);
});
it('should render notes activity header', () => {
- expect(wrapper.findComponent(NotesActivityHeader).props()).toEqual({
- notesFilterValue: TEST_NOTES_FILTER_VALUE,
- notesFilters: mockData.notesFilters,
- });
+ expect(wrapper.findComponent(NotesActivityHeader).props().notesFilterValue).toEqual(
+ TEST_NOTES_FILTER_VALUE,
+ );
+ expect(wrapper.findComponent(NotesActivityHeader).props().notesFilters).toEqual(
+ mockData.notesFilters,
+ );
});
});
@@ -173,7 +178,7 @@ describe('note_app', () => {
});
describe('while fetching data', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent();
});
diff --git a/spec/frontend/notes/components/timeline_toggle_spec.js b/spec/frontend/notes/components/timeline_toggle_spec.js
index cf79416d300..caa6f95d5da 100644
--- a/spec/frontend/notes/components/timeline_toggle_spec.js
+++ b/spec/frontend/notes/components/timeline_toggle_spec.js
@@ -35,10 +35,6 @@ describe('Timeline toggle', () => {
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
store.dispatch.mockReset();
mockEvent.currentTarget.blur.mockReset();
Tracking.event.mockReset();
diff --git a/spec/frontend/notes/components/toggle_replies_widget_spec.js b/spec/frontend/notes/components/toggle_replies_widget_spec.js
index 8c3696e88b7..ef5f06ad2fa 100644
--- a/spec/frontend/notes/components/toggle_replies_widget_spec.js
+++ b/spec/frontend/notes/components/toggle_replies_widget_spec.js
@@ -30,10 +30,6 @@ describe('toggle replies widget for notes', () => {
const mountComponent = ({ collapsed = false }) =>
mountExtended(ToggleRepliesWidget, { propsData: { replies, collapsed } });
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('collapsed state', () => {
beforeEach(() => {
wrapper = mountComponent({ collapsed: true });
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 6d3bc19bd45..355ecb78187 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -1,9 +1,11 @@
/* eslint-disable import/no-commonjs, no-new */
-import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
+import htmlPipelineSchedulesEditSnippets from 'test_fixtures/snippets/show.html';
+import htmlPipelineSchedulesEditCommit from 'test_fixtures/commit/show.html';
import '~/behaviors/markdown/render_gfm';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -19,11 +21,9 @@ const Notes = require('~/deprecated_notes').default;
const FLASH_TYPE_ALERT = 'alert';
const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
-const fixture = 'snippets/show.html';
let mockAxios;
window.project_uploads_path = `${TEST_HOST}/uploads`;
-window.gon = window.gon || {};
window.gl = window.gl || {};
gl.utils = gl.utils || {};
gl.utils.disableButtonIfEmptyField = () => {};
@@ -37,7 +37,7 @@ function wrappedDiscussionNote(note) {
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
- loadHTMLFixture(fixture);
+ setHTMLFixture(htmlPipelineSchedulesEditSnippets);
// Re-declare this here so that test_setup.js#beforeEach() doesn't
// overwrite it.
@@ -672,7 +672,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let $notesContainer;
beforeEach(() => {
- loadHTMLFixture('commit/show.html');
+ setHTMLFixture(htmlPipelineSchedulesEditCommit);
mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
new Notes('', []);
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index c4c0dc58b0d..97249d232dc 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -3,7 +3,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import Api from '~/api';
-import { createAlert } from '~/flash';
+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';
@@ -36,7 +36,7 @@ import {
const TEST_ERROR_MESSAGE = 'Test error message';
const mockAlertDismiss = jest.fn();
-jest.mock('~/flash', () => ({
+jest.mock('~/alert', () => ({
createAlert: jest.fn().mockImplementation(() => ({
dismiss: mockAlertDismiss,
})),
@@ -257,14 +257,14 @@ describe('Actions Notes Store', () => {
axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_OK, pollResponse, pollHeaders);
const failureMock = () =>
axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- const advanceAndRAF = async (time) => {
+ const advanceAndRAF = (time) => {
if (time) {
jest.advanceTimersByTime(time);
}
return waitForPromises();
};
- const advanceXMoreIntervals = async (number) => {
+ const advanceXMoreIntervals = (number) => {
const timeoutLength = pollInterval * number;
return advanceAndRAF(timeoutLength);
@@ -273,7 +273,7 @@ describe('Actions Notes Store', () => {
await store.dispatch('poll');
await advanceAndRAF(2);
};
- const cleanUp = async () => {
+ const cleanUp = () => {
jest.clearAllTimers();
return store.dispatch('stopPolling');
@@ -876,7 +876,7 @@ describe('Actions Notes Store', () => {
const res = { errors: { base: ['something went wrong'] } };
const error = { message: 'Unprocessable entity', response: { data: res } };
- it('sets flash alert using errors.base message', async () => {
+ it('sets an alert using errors.base message', async () => {
const resp = await actions.saveNote(
{
commit() {},
@@ -906,6 +906,20 @@ describe('Actions Notes Store', () => {
expect(data).toBe(res);
expect(createAlert).not.toHaveBeenCalled();
});
+
+ it('dispatches clearDrafts is command names contains submit_review', async () => {
+ const response = { command_names: ['submit_review'], valid: true };
+ dispatch = jest.fn().mockResolvedValue(response);
+ await actions.saveNote(
+ {
+ commit() {},
+ dispatch,
+ },
+ payload,
+ );
+
+ expect(dispatch).toHaveBeenCalledWith('batchComments/clearDrafts');
+ });
});
});
@@ -946,7 +960,7 @@ describe('Actions Notes Store', () => {
});
});
- it('when service fails, flashes error message', () => {
+ it('when service fails, creates an alert with error message', () => {
const response = { response: { data: { message: TEST_ERROR_MESSAGE } } };
Api.applySuggestion.mockReturnValue(Promise.reject(response));
@@ -1439,10 +1453,6 @@ describe('Actions Notes Store', () => {
describe('fetchDiscussions', () => {
const discussion = { notes: [] };
- afterEach(() => {
- window.gon = {};
- });
-
it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', () => {
axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion });
return testAction(
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index 70749557e61..480d617fcb2 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -2,7 +2,6 @@ import { GlSprintf, GlModal, GlFormGroup, GlFormCheckbox, GlLoadingIcon } from '
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -66,8 +65,6 @@ describe('CustomNotificationsModal', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
});
@@ -87,27 +84,26 @@ describe('CustomNotificationsModal', () => {
describe('checkbox items', () => {
beforeEach(async () => {
+ const endpointUrl = '/api/v4/notification_settings';
+
+ mockAxios
+ .onGet(endpointUrl)
+ .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
+
wrapper = createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- events: [
- { id: 'new_release', enabled: true, name: 'New release', loading: false },
- { id: 'new_note', enabled: false, name: 'New note', loading: true },
- ],
- });
+ wrapper.findComponent(GlModal).vm.$emit('show');
- await nextTick();
+ await waitForPromises();
});
it.each`
index | eventId | eventName | enabled | loading
${0} | ${'new_release'} | ${'New release'} | ${true} | ${false}
- ${1} | ${'new_note'} | ${'New note'} | ${false} | ${true}
+ ${1} | ${'new_note'} | ${'New note'} | ${false} | ${false}
`(
'renders a checkbox for "$eventName" with checked=$enabled',
- async ({ index, eventName, enabled, loading }) => {
+ ({ index, eventName, enabled, loading }) => {
const checkbox = findCheckboxAt(index);
expect(checkbox.text()).toContain(eventName);
expect(checkbox.vm.$attrs.checked).toBe(enabled);
@@ -214,16 +210,9 @@ describe('CustomNotificationsModal', () => {
wrapper = createComponent({ injectedProperties });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- events: [
- { id: 'new_release', enabled: true, name: 'New release', loading: false },
- { id: 'new_note', enabled: false, name: 'New note', loading: false },
- ],
- });
+ wrapper.findComponent(GlModal).vm.$emit('show');
- await nextTick();
+ await waitForPromises();
findCheckboxAt(1).vm.$emit('change', true);
@@ -241,19 +230,18 @@ describe('CustomNotificationsModal', () => {
);
it('shows a toast message when the request fails', async () => {
- mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_NOT_FOUND, {});
+ const endpointUrl = '/api/v4/notification_settings';
+
+ mockAxios
+ .onGet(endpointUrl)
+ .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
+
+ mockAxios.onPut(endpointUrl).reply(HTTP_STATUS_NOT_FOUND, {});
wrapper = createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- events: [
- { id: 'new_release', enabled: true, name: 'New release', loading: false },
- { id: 'new_note', enabled: false, name: 'New note', loading: false },
- ],
- });
+ wrapper.findComponent(GlModal).vm.$emit('show');
- await nextTick();
+ await waitForPromises();
findCheckboxAt(1).vm.$emit('change', true);
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
index 0f13de0e6d8..bae9b028cf7 100644
--- a/spec/frontend/notifications/components/notifications_dropdown_spec.js
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -25,7 +25,7 @@ describe('NotificationsDropdown', () => {
CustomNotificationsModal,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
provide: {
dropdownItems: mockDropdownItems,
@@ -61,8 +61,6 @@ describe('NotificationsDropdown', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
});
diff --git a/spec/frontend/oauth_application/components/oauth_secret_spec.js b/spec/frontend/oauth_application/components/oauth_secret_spec.js
new file mode 100644
index 00000000000..c38bd066da8
--- /dev/null
+++ b/spec/frontend/oauth_application/components/oauth_secret_spec.js
@@ -0,0 +1,116 @@
+import { GlButton, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import OAuthSecret from '~/oauth_application/components/oauth_secret.vue';
+import {
+ RENEW_SECRET_FAILURE,
+ RENEW_SECRET_SUCCESS,
+ WARNING_NO_SECRET,
+} from '~/oauth_application/constants';
+import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
+
+jest.mock('~/alert');
+const mockEvent = { preventDefault: jest.fn() };
+
+describe('OAuthSecret', () => {
+ let wrapper;
+ const renewPath = '/applications/1/renew';
+
+ const createComponent = (provide = {}) => {
+ wrapper = shallowMount(OAuthSecret, {
+ provide: {
+ initialSecret: undefined,
+ renewPath,
+ ...provide,
+ },
+ });
+ };
+
+ const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
+ const findRenewSecretButton = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ describe('when secret is provided', () => {
+ const initialSecret = 'my secret';
+ beforeEach(() => {
+ createComponent({ initialSecret });
+ });
+
+ it('shows the masked secret', () => {
+ expect(findInputCopyToggleVisibility().props('value')).toBe(initialSecret);
+ });
+
+ it('shows the renew secret button', () => {
+ expect(findRenewSecretButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when secret is not provided', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows an alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: WARNING_NO_SECRET,
+ variant: VARIANT_WARNING,
+ });
+ });
+
+ it('shows the renew secret button', () => {
+ expect(findRenewSecretButton().exists()).toBe(true);
+ });
+
+ describe('when renew secret button is selected', () => {
+ beforeEach(() => {
+ createComponent();
+ findRenewSecretButton().vm.$emit('click');
+ });
+
+ it('shows a modal', () => {
+ expect(findModal().props('visible')).toBe(true);
+ });
+
+ describe('when secret renewal succeeds', () => {
+ const initialSecret = 'my secret';
+
+ beforeEach(async () => {
+ const mockAxios = new MockAdapter(axios);
+ mockAxios.onPut().reply(HTTP_STATUS_OK, { secret: initialSecret });
+ findModal().vm.$emit('primary', mockEvent);
+ await waitForPromises();
+ });
+
+ it('shows an alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: RENEW_SECRET_SUCCESS,
+ variant: VARIANT_SUCCESS,
+ });
+ });
+
+ it('shows the new secret', () => {
+ expect(findInputCopyToggleVisibility().props('value')).toBe(initialSecret);
+ });
+ });
+
+ describe('when secret renewal fails', () => {
+ beforeEach(async () => {
+ const mockAxios = new MockAdapter(axios);
+ mockAxios.onPut().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ findModal().vm.$emit('primary', mockEvent);
+ await waitForPromises();
+ });
+
+ it('creates an alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: RENEW_SECRET_FAILURE,
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js
index 1fa0e0aa8f6..33295d46fea 100644
--- a/spec/frontend/oauth_remember_me_spec.js
+++ b/spec/frontend/oauth_remember_me_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlOauthRememberMe from 'test_fixtures_static/oauth_remember_me.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me';
describe('OAuthRememberMe', () => {
@@ -8,7 +9,7 @@ describe('OAuthRememberMe', () => {
};
beforeEach(() => {
- loadHTMLFixture('static/oauth_remember_me.html');
+ setHTMLFixture(htmlOauthRememberMe);
new OAuthRememberMe({ container: $('#oauth-container') }).bindEvents();
});
@@ -17,19 +18,16 @@ describe('OAuthRememberMe', () => {
resetHTMLFixture();
});
- it('adds the "remember_me" query parameter to all OAuth login buttons', () => {
- $('#oauth-container #remember_me').click();
+ it('adds and removes the "remember_me" query parameter from all OAuth login buttons', () => {
+ $('#oauth-container #remember_me_omniauth').click();
expect(findFormAction('.twitter')).toBe('http://example.com/?remember_me=1');
expect(findFormAction('.github')).toBe('http://example.com/?remember_me=1');
expect(findFormAction('.facebook')).toBe(
'http://example.com/?redirect_fragment=L1&remember_me=1',
);
- });
- it('removes the "remember_me" query parameter from all OAuth login buttons', () => {
- $('#oauth-container #remember_me').click();
- $('#oauth-container #remember_me').click();
+ $('#oauth-container #remember_me_omniauth').click();
expect(findFormAction('.twitter')).toBe('http://example.com/');
expect(findFormAction('.github')).toBe('http://example.com/');
diff --git a/spec/frontend/observability/index_spec.js b/spec/frontend/observability/index_spec.js
new file mode 100644
index 00000000000..25eb048c62b
--- /dev/null
+++ b/spec/frontend/observability/index_spec.js
@@ -0,0 +1,64 @@
+import { createWrapper } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import renderObservability from '~/observability/index';
+import ObservabilityApp from '~/observability/components/observability_app.vue';
+import { SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants';
+
+describe('renderObservability', () => {
+ let element;
+ let vueInstance;
+ let component;
+
+ const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE);
+ const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
+
+ beforeEach(() => {
+ element = document.createElement('div');
+ element.setAttribute('id', 'js-observability-app');
+ element.dataset.observabilityIframeSrc = 'https://observe.gitlab.com/';
+ document.body.appendChild(element);
+
+ vueInstance = renderObservability();
+ component = createWrapper(vueInstance).findComponent(ObservabilityApp);
+ });
+
+ afterEach(() => {
+ element.remove();
+ });
+
+ it('should return a Vue instance', () => {
+ expect(vueInstance).toEqual(expect.any(Vue));
+ });
+
+ it('should render the ObservabilityApp component', () => {
+ expect(component.props('observabilityIframeSrc')).toBe('https://observe.gitlab.com/');
+ });
+
+ describe('skeleton variant', () => {
+ it.each`
+ pathDescription | path | variant
+ ${'dashboards'} | ${OBSERVABILITY_ROUTES[0]} | ${SKELETON_VARIANTS[0]}
+ ${'explore'} | ${OBSERVABILITY_ROUTES[1]} | ${SKELETON_VARIANTS[1]}
+ ${'manage dashboards'} | ${OBSERVABILITY_ROUTES[2]} | ${SKELETON_VARIANTS[2]}
+ ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANTS[0]}
+ `(
+ 'renders the $variant skeleton variant for $pathDescription path',
+ async ({ path, variant }) => {
+ component.vm.$router.push(path);
+ await nextTick();
+
+ expect(component.props('skeletonVariant')).toBe(variant);
+ },
+ );
+ });
+
+ it('handle route-update events', () => {
+ component.vm.$router.push('/something?foo=bar');
+ component.vm.$emit('route-update', { url: '/some_path' });
+ expect(component.vm.$router.currentRoute.path).toBe('/something');
+ expect(component.vm.$router.currentRoute.query).toEqual({
+ foo: 'bar',
+ observability_path: '/some_path',
+ });
+ });
+});
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
index e3bcd140d60..4a9be71b880 100644
--- a/spec/frontend/observability/observability_app_spec.js
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -1,19 +1,20 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ObservabilityApp from '~/observability/components/observability_app.vue';
import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
-
-import { MESSAGE_EVENT_TYPE, SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants';
+import {
+ MESSAGE_EVENT_TYPE,
+ INLINE_EMBED_DIMENSIONS,
+ FULL_APP_DIMENSIONS,
+ SKELETON_VARIANT_EMBED,
+} from '~/observability/constants';
import { darkModeEnabled } from '~/lib/utils/color_utils';
jest.mock('~/lib/utils/color_utils');
-describe('Observability root app', () => {
+describe('ObservabilityApp', () => {
let wrapper;
- const replace = jest.fn();
- const $router = {
- replace,
- };
+
const $route = {
pathname: 'https://gitlab.com/gitlab-org/',
path: 'https://gitlab.com/gitlab-org/-/observability/dashboards',
@@ -26,21 +27,19 @@ describe('Observability root app', () => {
const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
- const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE);
-
- const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
+ const TEST_USERNAME = 'test-user';
- const mountComponent = (route = $route) => {
+ const mountComponent = (props) => {
wrapper = shallowMountExtended(ObservabilityApp, {
propsData: {
observabilityIframeSrc: TEST_IFRAME_SRC,
+ ...props,
},
stubs: {
'observability-skeleton': ObservabilitySkeleton,
},
mocks: {
- $router,
- $route: route,
+ $route,
},
});
};
@@ -48,17 +47,11 @@ describe('Observability root app', () => {
const dispatchMessageEvent = (message) =>
window.dispatchEvent(new MessageEvent('message', message));
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ gon.current_username = TEST_USERNAME;
});
describe('iframe src', () => {
- const TEST_USERNAME = 'test-user';
-
- beforeAll(() => {
- gon.current_username = TEST_USERNAME;
- });
-
it('should render an iframe with observabilityIframeSrc, decorated with light theme and username', () => {
darkModeEnabled.mockReturnValueOnce(false);
mountComponent();
@@ -92,48 +85,70 @@ describe('Observability root app', () => {
});
});
- describe('on GOUI_ROUTE_UPDATE', () => {
- it('should not call replace method from vue router if message event does not have url', () => {
- mountComponent();
- dispatchMessageEvent({
- type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE,
- payload: { data: 'some other data' },
+ describe('iframe kiosk query param', () => {
+ it('when inlineEmbed, it should set the proper kiosk query parameter', () => {
+ mountComponent({
+ inlineEmbed: true,
});
- expect(replace).not.toHaveBeenCalled();
+
+ const iframe = findIframe();
+
+ expect(iframe.attributes('src')).toBe(
+ `${TEST_IFRAME_SRC}&theme=light&username=${TEST_USERNAME}&kiosk=inline-embed`,
+ );
});
+ });
- it.each`
- condition | origin | observability_path | url
- ${'message origin is different from iframe source origin'} | ${'https://example.com'} | ${'/'} | ${'/explore'}
- ${'path is same as before (observability_path)'} | ${'https://observe.gitlab.com'} | ${'/foo?bar=test'} | ${'/foo?bar=test'}
- `(
- 'should not call replace method from vue router if $condition',
- async ({ origin, observability_path, url }) => {
- mountComponent({ ...$route, query: { observability_path } });
- dispatchMessageEvent({
- data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url } },
- origin,
- });
- expect(replace).not.toHaveBeenCalled();
- },
- );
+ describe('iframe size', () => {
+ it('should set the specified size', () => {
+ mountComponent({
+ height: INLINE_EMBED_DIMENSIONS.HEIGHT,
+ width: INLINE_EMBED_DIMENSIONS.WIDTH,
+ });
+
+ const iframe = findIframe();
+
+ expect(iframe.attributes('width')).toBe(INLINE_EMBED_DIMENSIONS.WIDTH);
+ expect(iframe.attributes('height')).toBe(INLINE_EMBED_DIMENSIONS.HEIGHT);
+ });
+
+ it('should fallback to default size', () => {
+ mountComponent({});
+
+ const iframe = findIframe();
- it('should call replace method from vue router on message event callback', () => {
+ expect(iframe.attributes('width')).toBe(FULL_APP_DIMENSIONS.WIDTH);
+ expect(iframe.attributes('height')).toBe(FULL_APP_DIMENSIONS.HEIGHT);
+ });
+ });
+
+ describe('skeleton variant', () => {
+ it('sets the specified skeleton variant', () => {
+ mountComponent({ skeletonVariant: SKELETON_VARIANT_EMBED });
+ const props = wrapper.findComponent(ObservabilitySkeleton).props();
+
+ expect(props.variant).toBe(SKELETON_VARIANT_EMBED);
+ });
+
+ it('should have a default skeleton variant', () => {
+ mountComponent();
+ const props = wrapper.findComponent(ObservabilitySkeleton).props();
+
+ expect(props.variant).toBe('dashboards');
+ });
+ });
+
+ describe('on GOUI_ROUTE_UPDATE', () => {
+ it('should emit a route-update event', () => {
mountComponent();
+ const payload = { url: '/explore' };
dispatchMessageEvent({
- data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
+ data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload },
origin: 'https://observe.gitlab.com',
});
- expect(replace).toHaveBeenCalled();
- expect(replace).toHaveBeenCalledWith({
- name: 'https://gitlab.com/gitlab-org/',
- query: {
- otherQuery: 100,
- observability_path: '/explore',
- },
- });
+ expect(wrapper.emitted('route-update')[0]).toEqual([payload]);
});
});
@@ -167,34 +182,17 @@ describe('Observability root app', () => {
});
});
- describe('skeleton variant', () => {
- it.each`
- pathDescription | path | variant
- ${'dashboards'} | ${OBSERVABILITY_ROUTES[0]} | ${SKELETON_VARIANTS[0]}
- ${'explore'} | ${OBSERVABILITY_ROUTES[1]} | ${SKELETON_VARIANTS[1]}
- ${'manage dashboards'} | ${OBSERVABILITY_ROUTES[2]} | ${SKELETON_VARIANTS[2]}
- ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANTS[0]}
- `('renders the $variant skeleton variant for $pathDescription path', ({ path, variant }) => {
- mountComponent({ ...$route, path });
- const props = wrapper.findComponent(ObservabilitySkeleton).props();
-
- expect(props.variant).toBe(variant);
- });
- });
-
- describe('on observability ui unmount', () => {
- it('should remove message event and should not call replace method from vue router', () => {
+ describe('on unmount', () => {
+ it('should not emit any even on route update', () => {
mountComponent();
wrapper.destroy();
- // testing event cleanup logic, should not call on messege event after component is destroyed
-
dispatchMessageEvent({
data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
origin: 'https://observe.gitlab.com',
});
- expect(replace).not.toHaveBeenCalled();
+ expect(wrapper.emitted('route-update')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
index a95597d8516..65dbb003743 100644
--- a/spec/frontend/observability/skeleton_spec.js
+++ b/spec/frontend/observability/skeleton_spec.js
@@ -6,8 +6,13 @@ import Skeleton from '~/observability/components/skeleton/index.vue';
import DashboardsSkeleton from '~/observability/components/skeleton/dashboards.vue';
import ExploreSkeleton from '~/observability/components/skeleton/explore.vue';
import ManageSkeleton from '~/observability/components/skeleton/manage.vue';
+import EmbedSkeleton from '~/observability/components/skeleton/embed.vue';
-import { SKELETON_VARIANTS_BY_ROUTE, DEFAULT_TIMERS } from '~/observability/constants';
+import {
+ SKELETON_VARIANTS_BY_ROUTE,
+ DEFAULT_TIMERS,
+ SKELETON_VARIANT_EMBED,
+} from '~/observability/constants';
describe('Skeleton component', () => {
let wrapper;
@@ -22,6 +27,8 @@ describe('Skeleton component', () => {
const findManageSkeleton = () => wrapper.findComponent(ManageSkeleton);
+ const findEmbedSkeleton = () => wrapper.findComponent(EmbedSkeleton);
+
const findAlert = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ ...props } = {}) => {
@@ -97,16 +104,20 @@ describe('Skeleton component', () => {
${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANTS[0]}
${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANTS[1]}
${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANTS[2]}
+ ${'embed'} | ${'variant is embed'} | ${SKELETON_VARIANT_EMBED}
${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
`('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
mountComponent({ variant });
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
- const showsDefaultSkeleton = !SKELETON_VARIANTS.includes(variant);
+ const showsDefaultSkeleton = ![...SKELETON_VARIANTS, SKELETON_VARIANT_EMBED].includes(
+ variant,
+ );
expect(findDashboardsSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[0]);
expect(findExploreSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[1]);
expect(findManageSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[2]);
+ expect(findEmbedSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANT_EMBED);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
});
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 732dfdd42fb..5bccf4943ae 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -2,7 +2,7 @@ import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitla
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { timezones } from '~/monitoring/format_date';
@@ -13,7 +13,7 @@ import MetricsSettings from '~/operation_settings/components/metrics_settings.vu
import store from '~/operation_settings/store';
jest.mock('~/lib/utils/url_utility');
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('operation settings external dashboard component', () => {
let wrapper;
@@ -47,9 +47,6 @@ describe('operation settings external dashboard component', () => {
});
afterEach(() => {
- if (wrapper.destroy) {
- wrapper.destroy();
- }
axios.patch.mockReset();
refreshCurrentPage.mockReset();
createAlert.mockReset();
@@ -198,7 +195,7 @@ describe('operation settings external dashboard component', () => {
expect(refreshCurrentPage).toHaveBeenCalled();
});
- it('creates flash banner on error', async () => {
+ it('creates an alert on error', async () => {
mountComponent(false);
const message = 'mockErrorMessage';
axios.patch.mockRejectedValue({ response: { data: { message } } });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
index ff11c8843bb..8ba7e40d728 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_button_spec.js
@@ -27,11 +27,6 @@ describe('delete_button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('tooltip', () => {
it('the title is controlled by tooltipTitle prop', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
index 620c96e8c9e..5a7cbdcff5b 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_image_spec.js
@@ -46,11 +46,6 @@ describe('Delete Image', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('executes apollo mutate on doDelete', () => {
const mutate = jest.fn().mockResolvedValue({});
mountComponent({ mutate });
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_modal_spec.js
index 16c9485e69e..1eaabf1ad09 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_modal_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/delete_modal_spec.js
@@ -1,14 +1,14 @@
import { GlSprintf, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import component from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue';
+import component from '~/packages_and_registries/container_registry/explorer/components/delete_modal.vue';
import {
REMOVE_TAG_CONFIRMATION_TEXT,
REMOVE_TAGS_CONFIRMATION_TEXT,
DELETE_IMAGE_CONFIRMATION_TITLE,
DELETE_IMAGE_CONFIRMATION_TEXT,
} from '~/packages_and_registries/container_registry/explorer/constants';
-import { GlModal } from '../../stubs';
+import { GlModal } from '../stubs';
describe('Delete Modal', () => {
let wrapper;
@@ -30,15 +30,10 @@ describe('Delete Modal', () => {
const expectPrimaryActionStatus = (disabled = true) =>
expect(findModal().props('actionPrimary')).toMatchObject(
expect.objectContaining({
- attributes: [{ variant: 'danger' }, { disabled }],
+ attributes: { variant: 'danger', disabled },
}),
);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('contains a GlModal', () => {
mountComponent();
expect(findModal().exists()).toBe(true);
@@ -80,7 +75,7 @@ describe('Delete Modal', () => {
});
describe('delete button', () => {
- const itemsToBeDeleted = [{ project: { path: 'foo' } }];
+ let itemsToBeDeleted = [{ project: { path: 'foo' } }];
it('is disabled by default', () => {
mountComponent({ deleteImage: true });
@@ -107,6 +102,17 @@ describe('Delete Modal', () => {
expectPrimaryActionStatus(false);
});
+
+ it('if the user types the image name when available', async () => {
+ itemsToBeDeleted = [{ name: 'foo' }];
+ mountComponent({ deleteImage: true, itemsToBeDeleted });
+
+ findInputComponent().vm.$emit('input', 'foo');
+
+ await nextTick();
+
+ expectPrimaryActionStatus(false);
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
index d45b993b5a2..9d187439ca3 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/delete_alert_spec.js
@@ -19,11 +19,6 @@ describe('Delete alert', () => {
wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData });
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when deleteAlertType is null', () => {
it('does not show the alert', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
index b37edac83f7..01089422376 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js
@@ -1,10 +1,10 @@
import { GlDropdownItem, GlIcon, GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue';
@@ -22,37 +22,27 @@ import {
} from '~/packages_and_registries/container_registry/explorer/constants';
import getContainerRepositoryMetadata from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_metadata.query.graphql';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import { imageTagsCountMock } from '../../mock_data';
+import { containerRepositoryMock, imageTagsCountMock } from '../../mock_data';
describe('Details Header', () => {
let wrapper;
let apolloProvider;
const defaultImage = {
- name: 'foo',
- updatedAt: '2020-11-03T13:29:21Z',
- canDelete: true,
- project: {
- visibility: 'public',
- path: 'path',
- containerExpirationPolicy: {
- enabled: false,
- },
- },
+ ...containerRepositoryMock,
};
// set the date to Dec 4, 2020
useFakeDate(2020, 11, 4);
- const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
- const findLastUpdatedAndVisibility = () => findByTestId('updated-and-visibility');
- const findTitle = () => findByTestId('title');
- const findTagsCount = () => findByTestId('tags-count');
- const findCleanup = () => findByTestId('cleanup');
+ const findCreatedAndVisibility = () => wrapper.findByTestId('created-and-visibility');
+ const findTitle = () => wrapper.findByTestId('title');
+ const findTagsCount = () => wrapper.findByTestId('tags-count');
+ const findCleanup = () => wrapper.findByTestId('cleanup');
const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
const findInfoIcon = () => wrapper.findComponent(GlIcon);
const findMenu = () => wrapper.findComponent(GlDropdown);
- const findSize = () => findByTestId('image-size');
+ const findSize = () => wrapper.findByTestId('image-size');
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
@@ -69,11 +59,11 @@ describe('Details Header', () => {
const requestHandlers = [[getContainerRepositoryMetadata, resolver]];
apolloProvider = createMockApollo(requestHandlers);
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
apolloProvider,
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
TitleArea,
@@ -85,9 +75,7 @@ describe('Details Header', () => {
afterEach(() => {
// if we want to mix createMockApollo and manual mocks we need to reset everything
- wrapper.destroy();
apolloProvider = undefined;
- wrapper = null;
});
describe('image name', () => {
@@ -99,7 +87,7 @@ describe('Details Header', () => {
});
it('root image shows project path name', () => {
- expect(findTitle().text()).toBe('path');
+ expect(findTitle().text()).toBe('gitlab-test');
});
it('has an icon', () => {
@@ -121,7 +109,7 @@ describe('Details Header', () => {
});
it('shows image.name', () => {
- expect(findTitle().text()).toContain('foo');
+ expect(findTitle().text()).toContain('rails-12009');
});
it('has no icon', () => {
@@ -249,7 +237,7 @@ describe('Details Header', () => {
expect(findCleanup().props('icon')).toBe('expire');
});
- it('when the expiration policy is disabled', async () => {
+ it('when cleanup is not scheduled', async () => {
mountComponent();
await waitForMetadataItems();
@@ -289,12 +277,12 @@ describe('Details Header', () => {
);
});
- describe('visibility and updated at', () => {
- it('has last updated text', async () => {
+ describe('visibility and created at', () => {
+ it('has created text', async () => {
mountComponent();
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('text')).toBe('Last updated 1 month ago');
+ expect(findCreatedAndVisibility().props('text')).toBe('Created Nov 3, 2020 13:29');
});
describe('visibility icon', () => {
@@ -302,7 +290,7 @@ describe('Details Header', () => {
mountComponent();
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye');
+ expect(findCreatedAndVisibility().props('icon')).toBe('eye');
});
it('shows an eye slashed when the project is not public', async () => {
mountComponent({
@@ -310,7 +298,7 @@ describe('Details Header', () => {
});
await waitForMetadataItems();
- expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
+ expect(findCreatedAndVisibility().props('icon')).toBe('eye-slash');
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
index ce5ecfe4608..d6c1b2c3f51 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/partial_cleanup_alert_spec.js
@@ -23,11 +23,6 @@ describe('Partial Cleanup alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it(`gl-alert has the correct properties`, () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
index d83a5099bcd..3e1fd14475d 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/status_alert_spec.js
@@ -27,11 +27,6 @@ describe('Status Alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
status | title | variant | message | link
${DELETE_SCHEDULED} | ${SCHEDULED_FOR_DELETION_STATUS_TITLE} | ${'info'} | ${SCHEDULED_FOR_DELETION_STATUS_MESSAGE} | ${PACKAGE_DELETE_HELP_PAGE_PATH}
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index fa0d76762df..f74dfcb029d 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -50,16 +50,11 @@ describe('tags list row', () => {
},
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('checkbox', () => {
it('exists', () => {
mountComponent();
@@ -158,7 +153,7 @@ describe('tags list row', () => {
it('is disabled when the component is disabled', () => {
mountComponent({ ...defaultProps, disabled: true });
- expect(findClipboardButton().attributes('disabled')).toBe('true');
+ expect(findClipboardButton().attributes('disabled')).toBeDefined();
});
});
@@ -283,26 +278,30 @@ describe('tags list row', () => {
textSrOnly: true,
category: 'tertiary',
right: true,
+ disabled: false,
});
});
- it.each`
- canDelete | digest | disabled | buttonDisabled
- ${true} | ${null} | ${true} | ${true}
- ${false} | ${'foo'} | ${true} | ${true}
- ${false} | ${null} | ${true} | ${true}
- ${true} | ${'foo'} | ${true} | ${true}
- ${true} | ${'foo'} | ${false} | ${false}
- `(
- 'is $visible that is visible when canDelete is $canDelete and digest is $digest and disabled is $disabled',
- ({ canDelete, digest, disabled, buttonDisabled }) => {
- mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest }, disabled });
+ it('has the correct classes', () => {
+ mountComponent();
- expect(findAdditionalActionsMenu().props('disabled')).toBe(buttonDisabled);
- expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(buttonDisabled);
- expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(buttonDisabled);
- },
- );
+ expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(false);
+ expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(false);
+ });
+
+ it('is not rendered when tag.canDelete is false', () => {
+ mountComponent({ ...defaultProps, tag: { ...tag, canDelete: false } });
+
+ expect(findAdditionalActionsMenu().exists()).toBe(false);
+ });
+
+ it('is hidden when disabled prop is set to true', () => {
+ mountComponent({ ...defaultProps, disabled: true });
+
+ expect(findAdditionalActionsMenu().props('disabled')).toBe(true);
+ expect(findAdditionalActionsMenu().classes('gl-opacity-0')).toBe(true);
+ expect(findAdditionalActionsMenu().classes('gl-pointer-events-none')).toBe(true);
+ });
describe('delete button', () => {
it('exists and has the correct attrs', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index 1017ff06a25..0cbb9eab018 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -4,13 +4,15 @@ import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-
+import Tracking from '~/tracking';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
+import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
+
import {
GRAPHQL_PAGE_SIZE,
NO_TAGS_TITLE,
@@ -19,7 +21,13 @@ import {
NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
} from '~/packages_and_registries/container_registry/explorer/constants/index';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
-import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data';
+import {
+ graphQLDeleteImageRepositoryTagsMock,
+ tagsMock,
+ imageTagsMock,
+ tagsPageInfo,
+} from '../../mock_data';
+import { DeleteModal } from '../../stubs';
describe('Tags List', () => {
let wrapper;
@@ -31,6 +39,7 @@ describe('Tags List', () => {
noContainersImage: 'noContainersImage',
};
+ const findDeleteModal = () => wrapper.findComponent(DeleteModal);
const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findTagsListRow = () => wrapper.findAllComponents(TagsListRow);
const findRegistryList = () => wrapper.findComponent(RegistryList);
@@ -42,20 +51,23 @@ describe('Tags List', () => {
};
const waitForApolloRequestRender = async () => {
+ fireFirstSortUpdate();
await waitForPromises();
- await nextTick();
};
- const mountComponent = ({ propsData = { isMobile: false, id: 1 } } = {}) => {
+ const mountComponent = ({ propsData = { isMobile: false, id: 1 }, mutationResolver } = {}) => {
Vue.use(VueApollo);
- const requestHandlers = [[getContainerRepositoryTagsQuery, resolver]];
+ const requestHandlers = [
+ [getContainerRepositoryTagsQuery, resolver],
+ [deleteContainerRepositoryTagsMutation, mutationResolver],
+ ];
apolloProvider = createMockApollo(requestHandlers);
wrapper = shallowMount(component, {
apolloProvider,
propsData,
- stubs: { RegistryList },
+ stubs: { RegistryList, DeleteModal },
provide() {
return {
config: defaultConfig,
@@ -66,17 +78,13 @@ describe('Tags List', () => {
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(imageTagsMock());
- });
-
- afterEach(() => {
- wrapper.destroy();
+ jest.spyOn(Tracking, 'event');
});
describe('registry list', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mountComponent();
- fireFirstSortUpdate();
- return waitForApolloRequestRender();
+ await waitForApolloRequestRender();
});
it('has a persisted search', () => {
@@ -98,6 +106,7 @@ describe('Tags List', () => {
pagination: tagsPageInfo,
items: tags,
idProperty: 'name',
+ hiddenDelete: false,
});
});
@@ -129,11 +138,46 @@ describe('Tags List', () => {
});
});
- it('emits a delete event when list emits delete', () => {
- const eventPayload = 'foo';
- findRegistryList().vm.$emit('delete', eventPayload);
+ describe('delete event', () => {
+ describe('single item', () => {
+ beforeEach(() => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+ });
+
+ it('opens the modal', () => {
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ });
+
+ it('sets modal props', () => {
+ expect(findDeleteModal().props('itemsToBeDeleted')).toMatchObject([tags[0]]);
+ });
+
+ it('tracks a single delete event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'registry_tag_delete',
+ });
+ });
+ });
- expect(wrapper.emitted('delete')).toEqual([[eventPayload]]);
+ describe('multiple items', () => {
+ beforeEach(() => {
+ findRegistryList().vm.$emit('delete', tags);
+ });
+
+ it('opens the modal', () => {
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ });
+
+ it('sets modal props', () => {
+ expect(findDeleteModal().props('itemsToBeDeleted')).toMatchObject(tags);
+ });
+
+ it('tracks multiple delete event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'bulk_registry_tag_delete',
+ });
+ });
+ });
});
});
});
@@ -141,7 +185,6 @@ describe('Tags List', () => {
describe('list rows', () => {
it('one row exist for each tag', async () => {
mountComponent();
- fireFirstSortUpdate();
await waitForApolloRequestRender();
@@ -150,7 +193,6 @@ describe('Tags List', () => {
it('the correct props are bound to it', async () => {
mountComponent({ propsData: { disabled: true, id: 1 } });
- fireFirstSortUpdate();
await waitForApolloRequestRender();
@@ -165,7 +207,6 @@ describe('Tags List', () => {
describe('events', () => {
it('select event update the selected items', async () => {
mountComponent();
- fireFirstSortUpdate();
await waitForApolloRequestRender();
findTagsListRow().at(0).vm.$emit('select');
@@ -175,23 +216,63 @@ describe('Tags List', () => {
expect(findTagsListRow().at(0).attributes('selected')).toBe('true');
});
- it('delete event emit a delete event', async () => {
- mountComponent();
- fireFirstSortUpdate();
- await waitForApolloRequestRender();
+ describe('delete event', () => {
+ let mutationResolver;
- findTagsListRow().at(0).vm.$emit('delete');
- expect(wrapper.emitted('delete')[0][0][0].name).toBe(tags[0].name);
+ beforeEach(async () => {
+ mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
+ resolver = jest.fn().mockResolvedValue(imageTagsMock());
+ mountComponent({ mutationResolver });
+
+ await waitForApolloRequestRender();
+ findTagsListRow().at(0).vm.$emit('delete');
+ });
+
+ it('opens the modal', () => {
+ expect(DeleteModal.methods.show).toHaveBeenCalled();
+ });
+
+ it('tracks a single delete event', () => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
+ label: 'registry_tag_delete',
+ });
+ });
+
+ it('confirmDelete event calls apollo mutation with the right parameters and refetches the tags list query', async () => {
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: [tags[0].name] }),
+ );
+
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenLastCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ name: '',
+ sort: 'NAME_ASC',
+ id: '1',
+ });
+ });
});
});
});
+ describe('when user does not have permission to delete list rows', () => {
+ it('sets registry list hiddenDelete prop to true', async () => {
+ resolver = jest.fn().mockResolvedValue(imageTagsMock({ canDelete: false }));
+ mountComponent();
+ await waitForApolloRequestRender();
+
+ expect(findRegistryList().props('hiddenDelete')).toBe(true);
+ });
+ });
+
describe('when the list of tags is empty', () => {
- beforeEach(() => {
- resolver = jest.fn().mockResolvedValue(imageTagsMock([]));
+ beforeEach(async () => {
+ resolver = jest.fn().mockResolvedValue(imageTagsMock({ nodes: [] }));
mountComponent();
- fireFirstSortUpdate();
- return waitForApolloRequestRender();
+ await waitForApolloRequestRender();
});
it('does not show the loader', () => {
@@ -217,7 +298,7 @@ describe('Tags List', () => {
filters: [{ type: FILTERED_SEARCH_TERM, value: { data: 'foo' } }],
});
- await waitForApolloRequestRender();
+ await waitForPromises();
expect(findEmptyState().props()).toMatchObject({
svgPath: defaultConfig.noContainersImage,
@@ -228,6 +309,175 @@ describe('Tags List', () => {
});
});
+ describe('modal', () => {
+ it('exists', async () => {
+ mountComponent();
+ await waitForApolloRequestRender();
+
+ expect(findDeleteModal().exists()).toBe(true);
+ });
+
+ describe('cancel event', () => {
+ it('tracks cancel_delete', async () => {
+ mountComponent();
+ await waitForApolloRequestRender();
+
+ findDeleteModal().vm.$emit('cancel');
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
+ label: 'registry_tag_delete',
+ });
+ });
+ });
+
+ describe('confirmDelete event', () => {
+ let mutationResolver;
+
+ describe('when mutation', () => {
+ beforeEach(() => {
+ mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
+ mountComponent({ mutationResolver });
+
+ return waitForApolloRequestRender();
+ });
+
+ it('is started renders loader', async () => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+
+ findDeleteModal().vm.$emit('confirmDelete');
+ await nextTick();
+
+ expect(findTagsLoader().exists()).toBe(true);
+ expect(findTagsListRow().exists()).toBe(false);
+ });
+
+ it('ends, loader is hidden', async () => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+
+ findDeleteModal().vm.$emit('confirmDelete');
+ await waitForPromises();
+
+ expect(findTagsLoader().exists()).toBe(false);
+ expect(findTagsListRow().exists()).toBe(true);
+ });
+ });
+
+ describe.each([
+ {
+ description: 'rejection',
+ mutationMock: jest.fn().mockRejectedValue(),
+ },
+ {
+ description: 'error',
+ mutationMock: jest.fn().mockResolvedValue({
+ data: {
+ destroyContainerRepositoryTags: {
+ errors: [new Error()],
+ },
+ },
+ }),
+ },
+ ])('when mutation fails with $description', ({ mutationMock }) => {
+ beforeEach(() => {
+ mutationResolver = mutationMock;
+ mountComponent({ mutationResolver });
+
+ return waitForApolloRequestRender();
+ });
+
+ it('when one item is selected to be deleted calls apollo mutation with the right parameters and emits delete event with right arguments', async () => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+
+ resolver.mockClear();
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: [tags[0].name] }),
+ );
+
+ expect(resolver).not.toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0][0]).toEqual('danger_tag');
+ });
+
+ it('when more than one item is selected to be deleted calls apollo mutation with the right parameters and emits delete event with right arguments', async () => {
+ findRegistryList().vm.$emit('delete', tagsMock);
+ resolver.mockClear();
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }),
+ );
+
+ expect(resolver).not.toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0][0]).toEqual('danger_tags');
+ });
+ });
+
+ describe('when mutation is successful', () => {
+ beforeEach(() => {
+ mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
+ mountComponent({ mutationResolver });
+
+ return waitForApolloRequestRender();
+ });
+
+ it('and one item is selected to be deleted calls apollo mutation with the right parameters and refetches the tags list query', async () => {
+ findRegistryList().vm.$emit('delete', [tags[0]]);
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: [tags[0].name] }),
+ );
+
+ expect(resolver).toHaveBeenLastCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ name: '',
+ sort: 'NAME_ASC',
+ id: '1',
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0][0]).toEqual('success_tag');
+ });
+
+ it('and more than one item is selected to be deleted calls apollo mutation with the right parameters and refetches the tags list query', async () => {
+ findRegistryList().vm.$emit('delete', tagsMock);
+
+ findDeleteModal().vm.$emit('confirmDelete');
+
+ expect(mutationResolver).toHaveBeenCalledWith(
+ expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }),
+ );
+
+ expect(resolver).toHaveBeenLastCalledWith({
+ first: GRAPHQL_PAGE_SIZE,
+ name: '',
+ sort: 'NAME_ASC',
+ id: '1',
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ expect(wrapper.emitted('delete')[0][0]).toEqual('success_tags');
+ });
+ });
+ });
+ });
+
describe('loading state', () => {
it.each`
isImageLoading | queryExecuting | loadingVisible
@@ -239,7 +489,6 @@ describe('Tags List', () => {
'when the isImageLoading is $isImageLoading, and is $queryExecuting that the query is still executing is $loadingVisible that the loader is shown',
async ({ isImageLoading, queryExecuting, loadingVisible }) => {
mountComponent({ propsData: { isImageLoading, isMobile: false, id: 1 } });
- fireFirstSortUpdate();
if (!queryExecuting) {
await waitForApolloRequestRender();
}
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
index 88e79c513bc..8896185ce67 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_loader_spec.js
@@ -20,11 +20,6 @@ describe('TagsLoader component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('produces the correct amount of loaders', () => {
mountComponent();
expect(findGlSkeletonLoaders().length).toBe(1);
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
index 535faebdd4e..0d1d2c53cab 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status_spec.js
@@ -36,10 +36,6 @@ describe('cleanup_status', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
status | visible | text
${UNFINISHED_STATUS} | ${true} | ${CLEANUP_STATUS_UNFINISHED}
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
index d2086943e4f..900ea61e4ea 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/group_empty_state_spec.js
@@ -26,10 +26,6 @@ describe('Registry Group Empty state', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index 75068591007..5d8df45415e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui';
+import { GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective } from 'helpers/vue_mock_directive';
import { mockTracking } from 'helpers/tracking_helper';
@@ -49,16 +49,11 @@ describe('Image List Row', () => {
config: {},
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('image title and path', () => {
it('renders shortened name of image and contains a link to the details page', () => {
mountComponent();
@@ -150,7 +145,7 @@ describe('Image List Row', () => {
});
it('the clipboard button is disabled', () => {
- expect(findClipboardButton().attributes('disabled')).toBe('true');
+ expect(findClipboardButton().attributes('disabled')).toBeDefined();
});
});
});
@@ -206,13 +201,6 @@ describe('Image List Row', () => {
expect(findTagsCount().exists()).toBe(true);
});
- it('contains a tag icon', () => {
- mountComponent();
- const icon = findTagsCount().findComponent(GlIcon);
- expect(icon.exists()).toBe(true);
- expect(icon.props('name')).toBe('tag');
- });
-
describe('loading state', () => {
it('shows a loader when metadataLoading is true', () => {
mountComponent({ metadataLoading: true });
@@ -231,12 +219,12 @@ describe('Image List Row', () => {
it('with one tag in the image', () => {
mountComponent({ item: { ...item, tagsCount: 1 } });
- expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
+ expect(findTagsCount().text()).toMatchInterpolatedText('1 tag');
});
it('with more than one tag in the image', () => {
mountComponent({ item: { ...item, tagsCount: 3 } });
- expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
+ expect(findTagsCount().text()).toMatchInterpolatedText('3 tags');
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
index 042b8383571..6c771887b88 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_spec.js
@@ -21,11 +21,6 @@ describe('Image List', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('list', () => {
it('contains one list element for each image', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
index 8cfa8128021..e4d13143484 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/project_empty_state_spec.js
@@ -34,10 +34,6 @@ describe('Registry Project Empty state', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('to match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
index bcc8e41fce8..b7f3698e155 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/registry_header_spec.js
@@ -35,11 +35,6 @@ describe('registry_header', () => {
await nextTick();
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('header', () => {
it('has a title', () => {
mountComponent({ metadataLoading: true });
@@ -86,7 +81,7 @@ describe('registry_header', () => {
});
});
- describe('expiration policy', () => {
+ describe('cleanup policy', () => {
it('when is disabled', async () => {
await mountComponent({
expirationPolicy: { enabled: false },
@@ -116,11 +111,11 @@ describe('registry_header', () => {
const cleanupLink = findSetupCleanUpLink();
expect(text.exists()).toBe(true);
- expect(text.props('text')).toBe('Expiration policy will run in ');
+ expect(text.props('text')).toBe('Cleanup will run in ');
expect(cleanupLink.exists()).toBe(true);
expect(cleanupLink.text()).toBe(SET_UP_CLEANUP);
});
- it('when the expiration policy is completely disabled', async () => {
+ it('when the cleanup policy is not scheduled', async () => {
await mountComponent({
expirationPolicy: { enabled: true },
expirationPolicyHelpPagePath: 'foo',
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
index e5b99f15e8c..8ca74f5077e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js
@@ -127,7 +127,6 @@ export const containerRepositoryMock = {
location: 'host.docker.internal:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true,
createdAt: '2020-11-03T13:29:21Z',
- updatedAt: '2020-11-03T13:29:21Z',
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
project: {
@@ -177,11 +176,12 @@ export const tagsMock = [
},
];
-export const imageTagsMock = (nodes = tagsMock) => ({
+export const imageTagsMock = ({ nodes = tagsMock, canDelete = true } = {}) => ({
data: {
containerRepository: {
id: containerRepositoryMock.id,
tagsCount: nodes.length,
+ canDelete,
tags: {
nodes,
pageInfo: { ...tagsPageInfo },
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
index 26f0e506829..7fed81acead 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/details_spec.js
@@ -22,22 +22,15 @@ import {
MISSING_OR_DELETED_IMAGE_TITLE,
MISSING_OR_DELETED_IMAGE_MESSAGE,
} from '~/packages_and_registries/container_registry/explorer/constants';
-import deleteContainerRepositoryTagsMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
-import getContainerRepositoryTagsQuery from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql';
-import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
import component from '~/packages_and_registries/container_registry/explorer/pages/details.vue';
import Tracking from '~/tracking';
import {
graphQLImageDetailsMock,
- graphQLDeleteImageRepositoryTagsMock,
- graphQLProjectImageRepositoriesDetailsMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
- tagsMock,
- imageTagsMock,
} from '../mock_data';
import { DeleteModal } from '../stubs';
@@ -69,13 +62,6 @@ describe('Details Page', () => {
isGroupPage: false,
};
- const cleanTags = tagsMock.map((t) => {
- const result = { ...t };
- // eslint-disable-next-line no-underscore-dangle
- delete result.__typename;
- return result;
- });
-
const waitForApolloRequestRender = async () => {
await waitForPromises();
await nextTick();
@@ -83,20 +69,12 @@ describe('Details Page', () => {
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()),
- mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock())),
- detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
options,
config = defaultConfig,
} = {}) => {
Vue.use(VueApollo);
- const requestHandlers = [
- [getContainerRepositoryDetailsQuery, resolver],
- [deleteContainerRepositoryTagsMutation, mutationResolver],
- [getContainerRepositoryTagsQuery, tagsResolver],
- [getContainerRepositoriesDetails, detailsResolver],
- ];
+ const requestHandlers = [[getContainerRepositoryDetailsQuery, resolver]];
apolloProvider = createMockApollo(requestHandlers);
@@ -127,11 +105,6 @@ describe('Details Page', () => {
jest.spyOn(Tracking, 'event');
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when isLoading is true', () => {
it('shows the loader', () => {
mountComponent();
@@ -189,50 +162,6 @@ describe('Details Page', () => {
isMobile: false,
});
});
-
- describe('deleteEvent', () => {
- describe('single item', () => {
- let tagToBeDeleted;
- beforeEach(async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
- [tagToBeDeleted] = cleanTags;
- findTagsList().vm.$emit('delete', [tagToBeDeleted]);
- });
-
- it('open the modal', async () => {
- expect(DeleteModal.methods.show).toHaveBeenCalled();
- });
-
- it('tracks a single delete event', () => {
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
- label: 'registry_tag_delete',
- });
- });
- });
-
- describe('multiple items', () => {
- beforeEach(async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
- findTagsList().vm.$emit('delete', cleanTags);
- });
-
- it('open the modal', () => {
- expect(DeleteModal.methods.show).toHaveBeenCalled();
- });
-
- it('tracks a single delete event', () => {
- expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', {
- label: 'bulk_registry_tag_delete',
- });
- });
- });
- });
});
describe('modal', () => {
@@ -253,61 +182,24 @@ describe('Details Page', () => {
findDeleteModal().vm.$emit('cancel');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'cancel_delete', {
- label: 'registry_tag_delete',
+ label: 'registry_image_delete',
});
});
});
- describe('confirmDelete event', () => {
- let mutationResolver;
- let tagsResolver;
- let detailsResolver;
-
+ describe('tags list delete event', () => {
beforeEach(() => {
- mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock);
- tagsResolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock(imageTagsMock()));
- detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
- mountComponent({ mutationResolver, tagsResolver, detailsResolver });
+ mountComponent();
return waitForApolloRequestRender();
});
- describe('when one item is selected to be deleted', () => {
- it('calls apollo mutation with the right parameters and refetches the tags list query', async () => {
- findTagsList().vm.$emit('delete', [cleanTags[0]]);
-
- await nextTick();
-
- findDeleteModal().vm.$emit('confirmDelete');
-
- expect(mutationResolver).toHaveBeenCalledWith(
- expect.objectContaining({ tagNames: [cleanTags[0].name] }),
- );
-
- await waitForPromises();
+ it('sets delete alert modal deleteAlertType value', async () => {
+ findTagsList().vm.$emit('delete', 'success_tag');
- expect(tagsResolver).toHaveBeenCalled();
- expect(detailsResolver).toHaveBeenCalled();
- });
- });
-
- describe('when more than one item is selected to be deleted', () => {
- it('calls apollo mutation with the right parameters and refetches the tags list query', async () => {
- findTagsList().vm.$emit('delete', tagsMock);
-
- await nextTick();
-
- findDeleteModal().vm.$emit('confirmDelete');
-
- expect(mutationResolver).toHaveBeenCalledWith(
- expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }),
- );
-
- await waitForPromises();
+ await nextTick();
- expect(tagsResolver).toHaveBeenCalled();
- expect(detailsResolver).toHaveBeenCalled();
- });
+ expect(findDeleteAlert().props('deleteAlertType')).toBe('success_tag');
});
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
index 1e514d85e82..1823bbfe533 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/list_spec.js
@@ -1,8 +1,8 @@
-import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
+import { GlSkeletonLoader, GlSprintf, GlAlert, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-
import VueApollo from 'vue-apollo';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
@@ -16,6 +16,7 @@ import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
SORT_FIELDS,
+ SETTINGS_TEXT,
} from '~/packages_and_registries/container_registry/explorer/constants';
import deleteContainerRepositoryMutation from '~/packages_and_registries/container_registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
import getContainerRepositoriesDetails from '~/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
@@ -36,18 +37,19 @@ import {
graphQLProjectImageRepositoriesDetailsMock,
dockerCommands,
} from '../mock_data';
-import { GlModal, GlEmptyState } from '../stubs';
+import { GlEmptyState, DeleteModal } from '../stubs';
describe('List Page', () => {
let wrapper;
let apolloProvider;
- const findDeleteModal = () => wrapper.findComponent(GlModal);
+ const findDeleteModal = () => wrapper.findComponent(DeleteModal);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findCliCommands = () => wrapper.findComponent(CliCommands);
+ const findSettingsLink = () => wrapper.findComponent(GlButton);
const findProjectEmptyState = () => wrapper.findComponent(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.findComponent(GroupEmptyState);
const findRegistryHeader = () => wrapper.findComponent(RegistryHeader);
@@ -89,7 +91,7 @@ describe('List Page', () => {
wrapper = shallowMount(component, {
apolloProvider,
stubs: {
- GlModal,
+ DeleteModal,
GlEmptyState,
GlSprintf,
RegistryHeader,
@@ -110,13 +112,12 @@ describe('List Page', () => {
...dockerCommands,
};
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains registry header', async () => {
mountComponent();
fireFirstSortUpdate();
@@ -126,6 +127,42 @@ describe('List Page', () => {
expect(findRegistryHeader().props()).toMatchObject({
imagesCount: 2,
metadataLoading: false,
+ helpPagePath: '',
+ hideExpirationPolicyData: false,
+ showCleanupPolicyLink: false,
+ expirationPolicy: {},
+ cleanupPoliciesSettingsPath: '',
+ });
+ });
+
+ describe('link to settings', () => {
+ beforeEach(() => {
+ const config = {
+ showContainerRegistrySettings: true,
+ cleanupPoliciesSettingsPath: 'bar',
+ };
+ mountComponent({ config });
+ });
+
+ it('is rendered', () => {
+ expect(findSettingsLink().exists()).toBe(true);
+ });
+
+ it('has the right icon', () => {
+ expect(findSettingsLink().props('icon')).toBe('settings');
+ });
+
+ it('has the right attributes', () => {
+ expect(findSettingsLink().attributes()).toMatchObject({
+ 'aria-label': SETTINGS_TEXT,
+ href: 'bar',
+ });
+ });
+
+ it('sets tooltip with right label', () => {
+ const tooltip = getBinding(findSettingsLink().element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(SETTINGS_TEXT);
});
});
@@ -239,6 +276,14 @@ describe('List Page', () => {
expect(findCliCommands().exists()).toBe(false);
});
+
+ it('link to settings is not visible', async () => {
+ mountComponent({ resolver, config });
+
+ await waitForApolloRequestRender();
+
+ expect(findSettingsLink().exists()).toBe(false);
+ });
});
});
@@ -310,7 +355,7 @@ describe('List Page', () => {
await selectImageForDeletion();
- findDeleteModal().vm.$emit('primary');
+ findDeleteModal().vm.$emit('confirmDelete');
expect(mutationResolver).toHaveBeenCalledWith({ id: deletedContainerRepository.id });
});
@@ -468,11 +513,15 @@ describe('List Page', () => {
expect(findDeleteModal().exists()).toBe(true);
});
- it('contains a description with the path of the item to delete', async () => {
- await waitForPromises();
- findImageList().vm.$emit('delete', { path: 'foo' });
+ it('contains the deleted image as props', async () => {
await waitForPromises();
- expect(findDeleteModal().html()).toContain('foo');
+ findImageList().vm.$emit('delete', deletedContainerRepository);
+ await nextTick();
+
+ expect(findDeleteModal().props()).toEqual({
+ itemsToBeDeleted: [deletedContainerRepository],
+ deleteImage: true,
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/stubs.js b/spec/frontend/packages_and_registries/container_registry/explorer/stubs.js
index 7d281a53a59..0d80028adf6 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/stubs.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/stubs.js
@@ -6,7 +6,7 @@ import {
} from '@gitlab/ui';
import { RouterLinkStub } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
-import RealDeleteModal from '~/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue';
+import RealDeleteModal from '~/packages_and_registries/container_registry/explorer/components/delete_modal.vue';
import RealListItem from '~/vue_shared/components/registry/list_item.vue';
export const GlModal = stubComponent(RealGlModal, {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js
index 5063759a620..d7a9c200c7b 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/utils_spec.js
@@ -1,6 +1,23 @@
-import { timeTilRun } from '~/packages_and_registries/container_registry/explorer/utils';
+import {
+ getImageName,
+ timeTilRun,
+} from '~/packages_and_registries/container_registry/explorer/utils';
describe('Container registry utilities', () => {
+ describe('getImageName', () => {
+ it('returns name when present', () => {
+ const result = getImageName({ name: 'foo' });
+
+ expect(result).toBe('foo');
+ });
+
+ it('returns project path when name is empty', () => {
+ const result = getImageName({ name: '', project: { path: 'foo' } });
+
+ expect(result).toBe('foo');
+ });
+ });
+
describe('timeTilRun', () => {
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
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 601f8abd34d..1928dbf72b6 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -5,7 +5,6 @@ import {
GlFormInputGroup,
GlFormGroup,
GlModal,
- GlSkeletonLoader,
GlSprintf,
GlEmptyState,
} from '@gitlab/ui';
@@ -13,6 +12,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
@@ -31,11 +31,6 @@ import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock
const dummyApiVersion = 'v3000';
const dummyGrouptId = 1;
const dummyUrlRoot = '/gitlab';
-const dummyGon = {
- api_version: dummyApiVersion,
- relative_url_root: dummyUrlRoot,
-};
-let originalGon;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${dummyGrouptId}/dependency_proxy/cache`;
Vue.use(VueApollo);
@@ -51,6 +46,7 @@ describe('DependencyProxyApp', () => {
groupId: dummyGrouptId,
noManifestsIllustration: 'noManifestsIllustration',
canClearCache: true,
+ settingsPath: 'path',
};
function createComponent({ provide = provideDefaults } = {}) {
@@ -71,56 +67,46 @@ describe('DependencyProxyApp', () => {
GlSprintf,
TitleArea,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
}
const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
- const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findMainArea = () => wrapper.findByTestId('main-area');
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
const findManifestList = () => wrapper.findComponent(ManifestsList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findClearCacheDropdownList = () => wrapper.findComponent(GlDropdown);
const findClearCacheModal = () => wrapper.findComponent(GlModal);
const findClearCacheAlert = () => wrapper.findComponent(GlAlert);
+ const findSettingsLink = () => wrapper.findByTestId('settings-link');
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
- originalGon = window.gon;
- window.gon = { ...dummyGon };
+ window.gon = {
+ api_version: dummyApiVersion,
+ relative_url_root: dummyUrlRoot,
+ };
mock = new MockAdapter(axios);
mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED, {});
});
afterEach(() => {
- wrapper.destroy();
- window.gon = originalGon;
mock.restore();
});
describe('when the dependency proxy is available', () => {
describe('when is loading', () => {
- it('renders the skeleton loader', () => {
- createComponent();
-
- expect(findSkeletonLoader().exists()).toBe(true);
- });
-
it('does not render a form group with label', () => {
createComponent();
expect(findFormGroup().exists()).toBe(false);
});
-
- it('does not show the main section', () => {
- createComponent();
-
- expect(findMainArea().exists()).toBe(false);
- });
});
describe('when the app is loaded', () => {
@@ -130,10 +116,6 @@ describe('DependencyProxyApp', () => {
return waitForPromises();
});
- it('renders the main area', () => {
- expect(findMainArea().exists()).toBe(true);
- });
-
it('renders a form group with a label', () => {
expect(findFormGroup().attributes('label')).toBe(
DependencyProxyApp.i18n.proxyImagePrefix,
@@ -157,6 +139,29 @@ describe('DependencyProxyApp', () => {
expect(findProxyCountText().text()).toBe('Contains 2 blobs of images (1024 Bytes)');
});
+ describe('link to settings', () => {
+ it('is rendered', () => {
+ expect(findSettingsLink().exists()).toBe(true);
+ });
+
+ it('has the right icon', () => {
+ expect(findSettingsLink().props('icon')).toBe('settings');
+ });
+
+ it('has the right attributes', () => {
+ expect(findSettingsLink().attributes()).toMatchObject({
+ 'aria-label': DependencyProxyApp.i18n.settingsText,
+ href: 'path',
+ });
+ });
+
+ it('sets tooltip with right label', () => {
+ const tooltip = getBinding(findSettingsLink().element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(DependencyProxyApp.i18n.settingsText);
+ });
+ });
+
describe('manifest lists', () => {
describe('when there are no manifests', () => {
beforeEach(() => {
@@ -189,6 +194,7 @@ describe('DependencyProxyApp', () => {
it('shows list', () => {
expect(findManifestList().props()).toMatchObject({
+ dependencyProxyImagePrefix: proxyData().dependencyProxyImagePrefix,
manifests: proxyManifests(),
pagination: pagination(),
});
@@ -218,13 +224,6 @@ describe('DependencyProxyApp', () => {
});
describe('triggering page event on list', () => {
- it('re-renders the skeleton loader', async () => {
- findManifestList().vm.$emit('next-page');
- await nextTick();
-
- expect(findSkeletonLoader().exists()).toBe(true);
- });
-
it('renders form group with label', async () => {
findManifestList().vm.$emit('next-page');
await nextTick();
@@ -233,13 +232,6 @@ describe('DependencyProxyApp', () => {
expect.stringMatching(DependencyProxyApp.i18n.proxyImagePrefix),
);
});
-
- it('does not show the main section', async () => {
- findManifestList().vm.$emit('next-page');
- await nextTick();
-
- expect(findMainArea().exists()).toBe(false);
- });
});
it('shows the clear cache dropdown list', () => {
@@ -274,9 +266,7 @@ describe('DependencyProxyApp', () => {
beforeEach(() => {
createComponent({
provide: {
- groupPath: 'gitlab-org',
- groupId: dummyGrouptId,
- noManifestsIllustration: 'noManifestsIllustration',
+ ...provideDefaults,
canClearCache: false,
},
});
@@ -285,6 +275,10 @@ describe('DependencyProxyApp', () => {
it('does not show the clear cache dropdown list', () => {
expect(findClearCacheDropdownList().exists()).toBe(false);
});
+
+ it('does not show link to settings', () => {
+ expect(findSettingsLink().exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
index 2f415bfd6f9..4149f728cd8 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
@@ -1,9 +1,9 @@
-import { GlKeysetPagination } from '@gitlab/ui';
+import { GlKeysetPagination, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
-
import Component from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import {
+ proxyData,
proxyManifests,
pagination,
} from 'jest/packages_and_registries/dependency_proxy/mock_data';
@@ -12,8 +12,10 @@ describe('Manifests List', () => {
let wrapper;
const defaultProps = {
+ dependencyProxyImagePrefix: proxyData().dependencyProxyImagePrefix,
manifests: proxyManifests(),
pagination: pagination(),
+ loading: false,
};
const createComponent = (propsData = defaultProps) => {
@@ -24,10 +26,8 @@ describe('Manifests List', () => {
const findRows = () => wrapper.findAllComponents(ManifestRow);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findMainArea = () => wrapper.findByTestId('main-area');
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
it('has the correct title', () => {
createComponent();
@@ -44,8 +44,27 @@ describe('Manifests List', () => {
it('binds a manifest to each row', () => {
createComponent();
- expect(findRows().at(0).props()).toMatchObject({
- manifest: defaultProps.manifests[0],
+ expect(findRows().at(0).props('manifest')).toBe(defaultProps.manifests[0]);
+ });
+
+ it('binds a dependencyProxyImagePrefix to each row', () => {
+ createComponent();
+
+ expect(findRows().at(0).props('dependencyProxyImagePrefix')).toBe(
+ proxyData().dependencyProxyImagePrefix,
+ );
+ });
+
+ describe('loading', () => {
+ it.each`
+ loading | expectLoader | expectContent
+ ${false} | ${false} | ${true}
+ ${true} | ${true} | ${false}
+ `('when loading is $loading', ({ loading, expectLoader, expectContent }) => {
+ createComponent({ ...defaultProps, loading });
+
+ expect(findSkeletonLoader().exists()).toBe(expectLoader);
+ expect(findMainArea().exists()).toBe(expectContent);
});
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
index be3236d1f9c..5f47a1b8098 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_row_spec.js
@@ -1,15 +1,17 @@
import { GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import Component from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
import { MANIFEST_PENDING_DESTRUCTION_STATUS } from '~/packages_and_registries/dependency_proxy/constants';
-import { proxyManifests } from 'jest/packages_and_registries/dependency_proxy/mock_data';
+import { proxyData, proxyManifests } from 'jest/packages_and_registries/dependency_proxy/mock_data';
describe('Manifest Row', () => {
let wrapper;
const defaultProps = {
+ dependencyProxyImagePrefix: proxyData().dependencyProxyImagePrefix,
manifest: proxyManifests()[0],
};
@@ -24,15 +26,13 @@ describe('Manifest Row', () => {
});
};
+ const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findListItem = () => wrapper.findComponent(ListItem);
const findCachedMessages = () => wrapper.findByTestId('cached-message');
+ const findDigest = () => wrapper.findByTestId('manifest-row-short-digest');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
const findStatus = () => wrapper.findByTestId('status');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('With a manifest on the DEFAULT status', () => {
beforeEach(() => {
createComponent();
@@ -42,12 +42,18 @@ describe('Manifest Row', () => {
expect(findListItem().exists()).toBe(true);
});
- it('displays the name', () => {
- expect(wrapper.text()).toContain('alpine');
+ it('displays the name with tag & digest', () => {
+ expect(wrapper.text()).toContain('alpine:latest');
+ expect(findDigest().text()).toMatchInterpolatedText('Digest: 995efde');
});
- it('displays the version', () => {
- expect(wrapper.text()).toContain('latest');
+ it('displays the name & digest for manifests that contain digest in image name', () => {
+ createComponent({
+ ...defaultProps,
+ manifest: proxyManifests()[1],
+ });
+ expect(wrapper.text()).toContain('alpine');
+ expect(findDigest().text()).toMatchInterpolatedText('Digest: e95efde');
});
it('displays the cached time', () => {
@@ -86,4 +92,35 @@ describe('Manifest Row', () => {
expect(findStatus().text()).toBe('Scheduled for deletion');
});
});
+
+ describe('clipboard button', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('exists', () => {
+ expect(findClipboardButton().exists()).toBe(true);
+ });
+
+ it('passes the correct title prop', () => {
+ expect(findClipboardButton().attributes('title')).toBe(Component.i18n.copyImagePathTitle);
+ });
+
+ it('has the correct copy text when image name contains tag name', () => {
+ expect(findClipboardButton().attributes('text')).toBe(
+ 'gdk.test:3000/private-group/dependency_proxy/containers/alpine:latest',
+ );
+ });
+
+ it('has the correct copy text when image name contains digest', () => {
+ createComponent({
+ ...defaultProps,
+ manifest: proxyManifests()[1],
+ });
+
+ expect(findClipboardButton().attributes('text')).toBe(
+ 'gdk.test:3000/private-group/dependency_proxy/containers/alpine@sha256:e95efde2e81b21d1ea7066aa77a59298a62a9e9fbb4b77f36c189774ec9b1089',
+ );
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
index 37c8eb669ba..4d0be0a0c09 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/mock_data.js
@@ -11,13 +11,15 @@ export const proxyManifests = () => [
{
id: 'proxy-1',
createdAt: '2021-09-22T09:45:28Z',
+ digest: 'sha256:995efde2e81b21d1ea7066aa77a59298a62a9e9fbb4b77f36c189774ec9b1089',
imageName: 'alpine:latest',
status: 'DEFAULT',
},
{
id: 'proxy-2',
createdAt: '2021-09-21T09:45:28Z',
- imageName: 'alpine:stable',
+ digest: 'sha256:e95efde2e81b21d1ea7066aa77a59298a62a9e9fbb4b77f36c189774ec9b1089',
+ imageName: 'alpine:sha256:e95efde2e81b21d1ea7066aa77a59298a62a9e9fbb4b77f36c189774ec9b1089',
status: 'DEFAULT',
},
];
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js
index a2e5cbdce8b..1e9b9b1ce47 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js
@@ -63,10 +63,6 @@ describe('Harbor artifact list row', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('list item', () => {
beforeEach(() => {
mountComponent({
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js
index b9d6dc2679e..786a4715731 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js
@@ -26,10 +26,6 @@ describe('Harbor artifacts list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js
index e8cc2b2e22d..d8fb91c085c 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js
@@ -20,10 +20,6 @@ describe('Harbor Details Header', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('artifact name', () => {
describe('missing image name', () => {
beforeEach(() => {
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
index 7a6169d300c..9a7ad759dba 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_header_spec.js
@@ -29,10 +29,6 @@ describe('harbor_list_header', () => {
await nextTick();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('header', () => {
it('has a title', () => {
mountComponent({ metadataLoading: true });
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
index b62d4e8836b..1e031e0557a 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_row_spec.js
@@ -28,10 +28,6 @@ describe('Harbor List Row', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('image title and path', () => {
it('contains a link to the details page', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js
index e7e74a0da58..a1803ecf7fb 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/list/harbor_list_spec.js
@@ -20,10 +20,6 @@ describe('Harbor List', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('list', () => {
it('contains one list element for each image', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js
index 5e299a269e3..9370ff1fdd4 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js
@@ -26,10 +26,6 @@ describe('Harbor Tags Header', () => {
totalPages: 1,
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
mountComponent({
propsData: { artifactDetail: mockArtifactDetail, pageInfo: mockPageInfo, tagsLoading: false },
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js
index 849215e286b..0b2ce01ebf6 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js
@@ -37,10 +37,6 @@ describe('Harbor tag list row', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('list item', () => {
beforeEach(() => {
mountComponent({
diff --git a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js
index 4c6b2b6daaa..e2a2a584b7d 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js
@@ -24,10 +24,6 @@ describe('Harbor Tags List', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
index 69765d31674..90c3d9082f7 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js
@@ -74,10 +74,6 @@ describe('Harbor Details Page', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when isLoading is true', () => {
it('shows the loader', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
index 97d30e6fe99..1bc2657822e 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/list_spec.js
@@ -60,10 +60,6 @@ describe('Harbor List Page', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains harbor registry header', async () => {
mountComponent();
fireFirstSortUpdate();
@@ -78,7 +74,7 @@ describe('Harbor List Page', () => {
});
describe('isLoading is true', () => {
- it('shows the skeleton loader', async () => {
+ it('shows the skeleton loader', () => {
mountComponent();
fireFirstSortUpdate();
@@ -97,7 +93,7 @@ describe('Harbor List Page', () => {
expect(findCliCommands().exists()).toBe(false);
});
- it('title has the metadataLoading props set to true', async () => {
+ it('title has the metadataLoading props set to true', () => {
mountComponent();
fireFirstSortUpdate();
diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
index 10901c6ec1e..6002faa1fa3 100644
--- a/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
+++ b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js
@@ -60,10 +60,6 @@ describe('Harbor Tags page', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains tags header', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
index e74375b7705..f8130287c12 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/app_spec.js
@@ -86,10 +86,6 @@ describe('PackagesApp', () => {
const findTerraformInstallation = () => wrapper.findComponent(TerraformInstallation);
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the app and displays the package title', async () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
index b504f7489ab..148e87699f1 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/details_title_spec.js
@@ -39,10 +39,6 @@ describe('PackageTitle', () => {
const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
const packageRef = () => wrapper.find('[data-testid="package-ref"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('module title', () => {
it('is correctly bound', async () => {
await createComponent();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
index d7caa8ca2d8..7352afff051 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/file_sha_spec.js
@@ -23,10 +23,6 @@ describe('FileSha', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
index b76d7c2b57b..c3e0818fc11 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_files_spec.js
@@ -37,11 +37,6 @@ describe('Package Files', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('rows', () => {
it('renders a single file for an npm package', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
index 0cbe2755f7e..a650aba464e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
@@ -30,11 +30,6 @@ describe('Package History', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findHistoryElement = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findElementLink = (container) => container.findComponent(GlLink);
const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip);
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
index 78c1b840dbc..94797f01d16 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/terraform_installation_spec.js
@@ -30,10 +30,6 @@ describe('TerraformInstallation', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
index bb970336b94..ea4d268d84e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/store/actions_spec.js
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages_and_registries/infrastructure_registry/details/constants';
import {
fetchPackageVersions,
@@ -15,7 +15,7 @@ import {
} from '~/packages_and_registries/shared/constants';
import { npmPackage as packageEntity } from '../../mock_data';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/api.js');
describe('Actions Package details store', () => {
@@ -53,7 +53,7 @@ describe('Actions Package details store', () => {
expect(Api.projectPackage).toHaveBeenCalledWith(packageEntity.project_id, packageEntity.id);
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
Api.projectPackage = jest.fn().mockRejectedValue();
await testAction(
@@ -83,7 +83,7 @@ describe('Actions Package details store', () => {
packageEntity.id,
);
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
Api.deleteProjectPackage = jest.fn().mockRejectedValue();
await testAction(deletePackage, undefined, { packageEntity }, [], []);
@@ -118,7 +118,7 @@ describe('Actions Package details store', () => {
});
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
Api.deleteProjectPackageFile = jest.fn().mockRejectedValue();
await testAction(deletePackageFile, fileId, { packageEntity }, [], []);
expect(createAlert).toHaveBeenCalledWith({
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 801cde8582e..d0841c6110f 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
@@ -49,9 +49,7 @@ exports[`packages_list_app renders 1`] = `
Learn how to
<b-link-stub
class="gl-link"
- event="click"
href="helpUrl"
- routertag="a"
target="_blank"
>
publish and share your packages
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
index a086c20a5e7..a89247c0a97 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_search_spec.js
@@ -55,11 +55,6 @@ describe('Infrastructure Search', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('has a registry search component', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
index aca6b0942cc..12859b1d77c 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/infrastructure_title_spec.js
@@ -22,11 +22,6 @@ describe('Infrastructure Title', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('title area', () => {
beforeEach(() => {
mountComponent();
@@ -37,7 +32,7 @@ describe('Infrastructure Title', () => {
});
it('has the correct title', () => {
- expect(findTitleArea().props('title')).toBe('Infrastructure Registry');
+ expect(findTitleArea().props('title')).toBe('Terraform Module Registry');
});
describe('with no modules', () => {
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
index d237023d0cd..47d36d11e35 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_app_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
-import { createAlert, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import * as commonUtils from '~/lib/utils/common_utils';
import PackageListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
@@ -14,7 +14,7 @@ import InfrastructureSearch from '~/packages_and_registries/infrastructure_regis
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
jest.mock('~/lib/utils/common_utils');
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(Vuex);
@@ -72,10 +72,6 @@ describe('packages_list_app', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
createStore({ packageCount: 1 });
mountComponent();
@@ -217,7 +213,7 @@ describe('packages_list_app', () => {
setWindowLocation(originalLocation);
});
- it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
+ it(`creates an alert if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
mountComponent();
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
index 0164d92ce34..51445942eaa 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/packages_list_spec.js
@@ -4,13 +4,13 @@ import Vue from 'vue';
import { last } from 'lodash';
import Vuex from 'vuex';
import stubChildren from 'helpers/stub_children';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import PackagesList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue';
import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants';
import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants';
-import Tracking from '~/tracking';
import { packageList } from '../../mock_data';
Vue.use(Vuex);
@@ -72,11 +72,6 @@ describe('packages_list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when is loading', () => {
beforeEach(() => {
mountComponent({
@@ -179,23 +174,23 @@ describe('packages_list', () => {
});
describe('tracking', () => {
- let eventSpy;
+ let trackingSpy = null;
beforeEach(() => {
mountComponent();
- eventSpy = jest.spyOn(Tracking, 'event');
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
- });
-
- it('deleteItemConfirmation calls event', () => {
- wrapper.vm.deleteItemConfirmation();
- expect(eventSpy).toHaveBeenCalledWith(
- TRACK_CATEGORY,
- TRACKING_ACTIONS.DELETE_PACKAGE,
- expect.any(Object),
- );
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('deleteItemConfirmation calls event', async () => {
+ await findPackageListDeleteModal().vm.$emit('ok');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACK_CATEGORY, TRACKING_ACTIONS.DELETE_PACKAGE, {
+ category: TRACK_CATEGORY,
+ });
});
});
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
index 2c185e040f4..4f051264172 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
@@ -2,14 +2,14 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { MISSING_DELETE_PATH_ERROR } from '~/packages_and_registries/infrastructure_registry/list/constants';
import * as actions from '~/packages_and_registries/infrastructure_registry/list/stores/actions';
import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
jest.mock('~/api.js');
describe('Actions Package list store', () => {
@@ -96,7 +96,7 @@ describe('Actions Package list store', () => {
});
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
Api.projectPackages = jest.fn().mockRejectedValue();
await testAction(
actions.requestPackagesList,
@@ -198,7 +198,7 @@ describe('Actions Package list store', () => {
);
});
- it('should stop the loading and call create flash on api error', async () => {
+ it('should stop the loading and call create alert on api error', async () => {
mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.requestDeletePackage,
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
index 721bdd34a4f..d00d7180f75 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/package_list_row_spec.js
@@ -49,16 +49,11 @@ describe('packages_list_row', () => {
disableDelete,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
@@ -143,7 +138,7 @@ describe('packages_list_row', () => {
});
it('details link is disabled', () => {
- expect(findPackageLink().attributes('disabled')).toBe('true');
+ expect(findPackageLink().attributes('disabled')).toBeDefined();
});
it('has a warning icon', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
index 9c1ebf5a2eb..4ab81182c9a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
@@ -1,7 +1,14 @@
-import { GlModal as RealGlModal } from '@gitlab/ui';
+import { GlModal as RealGlModal, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
+import {
+ DELETE_PACKAGE_MODAL_PRIMARY_ACTION,
+ DELETE_PACKAGE_WITH_REQUEST_FORWARDING_PRIMARY_ACTION,
+ DELETE_PACKAGES_REQUEST_FORWARDING_MODAL_CONTENT,
+ DELETE_PACKAGES_WITH_REQUEST_FORWARDING_PRIMARY_ACTION,
+ REQUEST_FORWARDING_HELP_PAGE_PATH,
+} from '~/packages_and_registries/package_registry/constants';
const GlModal = stubComponent(RealGlModal, {
methods: {
@@ -14,22 +21,30 @@ describe('DeleteModal', () => {
const defaultItemsToBeDeleted = [
{
- name: 'package 01',
+ name: 'package-1',
+ version: '1.0.0',
},
{
- name: 'package 02',
+ name: 'package-2',
+ version: '1.0.0',
},
];
const findModal = () => wrapper.findComponent(GlModal);
+ const findLink = () => wrapper.findComponent(GlLink);
- const mountComponent = ({ itemsToBeDeleted = defaultItemsToBeDeleted } = {}) => {
+ const mountComponent = ({
+ itemsToBeDeleted = defaultItemsToBeDeleted,
+ showRequestForwardingContent = false,
+ } = {}) => {
wrapper = shallowMountExtended(DeleteModal, {
propsData: {
itemsToBeDeleted,
+ showRequestForwardingContent,
},
stubs: {
GlModal,
+ GlSprintf,
},
});
};
@@ -45,16 +60,81 @@ describe('DeleteModal', () => {
it('passes actionPrimary prop', () => {
expect(findModal().props('actionPrimary')).toStrictEqual({
text: 'Permanently delete',
- attributes: [{ variant: 'danger' }, { category: 'primary' }],
+ attributes: { variant: 'danger', category: 'primary' },
});
});
it('renders description', () => {
- expect(findModal().text()).toContain(
+ expect(findModal().text()).toMatchInterpolatedText(
'You are about to delete 2 packages. This operation is irreversible.',
);
});
+ it('with only one item to be deleted renders correct description', () => {
+ mountComponent({ itemsToBeDeleted: [defaultItemsToBeDeleted[0]] });
+
+ expect(findModal().text()).toMatchInterpolatedText(
+ 'You are about to delete version 1.0.0 of package-1. Are you sure?',
+ );
+ });
+
+ it('sets the right action primary text', () => {
+ expect(findModal().props('actionPrimary')).toMatchObject({
+ text: DELETE_PACKAGE_MODAL_PRIMARY_ACTION,
+ });
+ });
+
+ describe('when showRequestForwardingContent is set', () => {
+ it('renders correct description', () => {
+ mountComponent({ showRequestForwardingContent: true });
+
+ expect(findModal().text()).toMatchInterpolatedText(
+ DELETE_PACKAGES_REQUEST_FORWARDING_MODAL_CONTENT,
+ );
+ });
+
+ it('contains link to help page', () => {
+ mountComponent({ showRequestForwardingContent: true });
+
+ expect(findLink().exists()).toBe(true);
+ expect(findLink().attributes('href')).toBe(REQUEST_FORWARDING_HELP_PAGE_PATH);
+ });
+
+ it('sets the right action primary text', () => {
+ mountComponent({ showRequestForwardingContent: true });
+
+ expect(findModal().props('actionPrimary')).toMatchObject({
+ text: DELETE_PACKAGES_WITH_REQUEST_FORWARDING_PRIMARY_ACTION,
+ });
+ });
+
+ describe('and only one item to be deleted', () => {
+ beforeEach(() => {
+ mountComponent({
+ showRequestForwardingContent: true,
+ itemsToBeDeleted: [defaultItemsToBeDeleted[0]],
+ });
+ });
+
+ it('renders correct description', () => {
+ expect(findModal().text()).toMatchInterpolatedText(
+ 'Deleting this package while request forwarding is enabled for the project can pose a security risk. Do you want to delete package-1 version 1.0.0 anyway? What are the risks?',
+ );
+ });
+
+ it('contains link to help page', () => {
+ expect(findLink().exists()).toBe(true);
+ expect(findLink().attributes('href')).toBe(REQUEST_FORWARDING_HELP_PAGE_PATH);
+ });
+
+ it('sets the right action primary text', () => {
+ expect(findModal().props('actionPrimary')).toMatchObject({
+ text: DELETE_PACKAGE_WITH_REQUEST_FORWARDING_PRIMARY_ACTION,
+ });
+ });
+ });
+ });
+
it('emits confirm when primary event is emitted', () => {
expect(wrapper.emitted('confirm')).toBeUndefined();
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 67f1906f6fd..9b429c39faa 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
@@ -89,8 +89,9 @@ exports[`MavenInstallation maven renders all the messages 1`] = `
/>
<code-instruction-stub
+ class="gl-w-20 gl-mt-5"
copytext="Copy Maven command"
- instruction="mvn dependency:get -Dartifact=appGroup:appName:appVersion"
+ instruction="mvn install"
label="Maven Command"
trackingaction="copy_maven_command"
trackinglabel="code_instruction"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
deleted file mode 100644
index 047fa04947c..00000000000
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/package_title_spec.js.snap
+++ /dev/null
@@ -1,199 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`PackageTitle renders with tags 1`] = `
-<div
- class="gl-display-flex gl-flex-direction-column"
- data-qa-selector="package_title"
->
- <div
- class="gl-display-flex gl-justify-content-space-between gl-py-3"
- >
- <div
- class="gl-flex-direction-column gl-flex-grow-1"
- >
- <div
- class="gl-display-flex"
- >
- <!---->
-
- <div
- class="gl-display-flex gl-flex-direction-column"
- >
- <h2
- class="gl-font-size-h1 gl-mt-3 gl-mb-0"
- data-testid="title"
- >
- @gitlab-org/package-15
- </h2>
-
- <div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
- >
- <div
- class="gl-display-flex gl-gap-3"
- data-testid="sub-header"
- >
- v
- 1.0.0
- published
- <time-ago-tooltip-stub
- cssclass=""
- time="2020-08-17T14:23:32Z"
- tooltipplacement="top"
- />
-
- <package-tags-stub
- hidelabel="true"
- tagdisplaylimit="2"
- tags="[object Object],[object Object],[object Object]"
- />
- </div>
- </div>
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-type"
- icon="package"
- link=""
- size="s"
- text="npm"
- texttooltip=""
- />
- </div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-size"
- icon="disk"
- link=""
- size="s"
- text="800.00 KiB"
- texttooltip=""
- />
- </div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-last-downloaded-at"
- icon="download"
- link=""
- size="m"
- text="Last downloaded Aug 17, 2021"
- texttooltip=""
- />
- </div>
- </div>
- </div>
-
- <!---->
- </div>
-
- <p />
-</div>
-`;
-
-exports[`PackageTitle renders without tags 1`] = `
-<div
- class="gl-display-flex gl-flex-direction-column"
- data-qa-selector="package_title"
->
- <div
- class="gl-display-flex gl-justify-content-space-between gl-py-3"
- >
- <div
- class="gl-flex-direction-column gl-flex-grow-1"
- >
- <div
- class="gl-display-flex"
- >
- <!---->
-
- <div
- class="gl-display-flex gl-flex-direction-column"
- >
- <h2
- class="gl-font-size-h1 gl-mt-3 gl-mb-0"
- data-testid="title"
- >
- @gitlab-org/package-15
- </h2>
-
- <div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-3"
- >
- <div
- class="gl-display-flex gl-gap-3"
- data-testid="sub-header"
- >
- v
- 1.0.0
- published
- <time-ago-tooltip-stub
- cssclass=""
- time="2020-08-17T14:23:32Z"
- tooltipplacement="top"
- />
-
- <!---->
- </div>
- </div>
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mt-3"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-type"
- icon="package"
- link=""
- size="s"
- text="npm"
- texttooltip=""
- />
- </div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-size"
- icon="disk"
- link=""
- size="s"
- text="800.00 KiB"
- texttooltip=""
- />
- </div>
- <div
- class="gl-display-flex gl-align-items-center gl-mr-5"
- >
- <metadata-item-stub
- data-testid="package-last-downloaded-at"
- icon="download"
- link=""
- size="m"
- text="Last downloaded Aug 17, 2021"
- texttooltip=""
- />
- </div>
- </div>
- </div>
-
- <!---->
- </div>
-
- <p />
-</div>
-`;
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 b2375da7b11..b4ea6543446 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
@@ -20,7 +20,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<!---->
<button
aria-expanded="false"
- aria-haspopup="true"
+ aria-haspopup="menu"
class="btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle"
id="__BVID__27__BV_toggle_"
type="button"
@@ -42,7 +42,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
role="img"
>
<use
- href="#chevron-down"
+ href="file-mock#chevron-down"
/>
</svg>
</button>
@@ -59,7 +59,6 @@ exports[`PypiInstallation renders all the messages 1`] = `
</div>
<fieldset
- aria-describedby="installation-pip-command-group__BV_description_"
class="form-group gl-form-group"
id="installation-pip-command-group"
>
@@ -75,12 +74,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
<!---->
</legend>
- <div
- aria-labelledby="installation-pip-command-group__BV_label_"
- class="bv-no-focus-ring"
- role="group"
- tabindex="-1"
- >
+ <div>
<div
data-testid="pip-command"
id="installation-pip-command"
@@ -128,7 +122,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
role="img"
>
<use
- href="#copy-to-clipboard"
+ href="file-mock#copy-to-clipboard"
/>
</svg>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
index 4f3d780b149..2e59c27cc1b 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
@@ -65,11 +65,6 @@ describe('Package Additional metadata', () => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTitle = () => wrapper.findByTestId('title');
const findMainArea = () => wrapper.findByTestId('main');
const findComponentIs = () => wrapper.findByTestId('component-is');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
index 0aba8f7efc7..a6298ebdea7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/composer_installation_spec.js
@@ -34,10 +34,6 @@ describe('ComposerInstallation', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('install command switch', () => {
it('has the installation title component', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
index bf9425def9a..70534b1d0a6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/conan_installation_spec.js
@@ -33,10 +33,6 @@ describe('ConanInstallation', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
index 9aed5b90c73..19aedf120b2 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/dependency_row_spec.js
@@ -19,10 +19,6 @@ describe('DependencyRow', () => {
const dependencyVersion = () => wrapper.findByTestId('version-pattern');
const dependencyFramework = () => wrapper.findByTestId('target-framework');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders', () => {
it('full dependency', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
index feed7a7c46c..a9428773a60 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/file_sha_spec.js
@@ -23,10 +23,6 @@ describe('FileSha', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js
index 5fe795f768e..a2d30be13c2 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/installation_title_spec.js
@@ -20,10 +20,6 @@ describe('InstallationTitle', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a title', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
index 8bb05b00e65..d35d95e319f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/installations_commands_spec.js
@@ -40,10 +40,6 @@ describe('InstallationCommands', () => {
const pypiInstallation = () => wrapper.findComponent(PypiInstallation);
const composerInstallation = () => wrapper.findComponent(ComposerInstallation);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('installation instructions', () => {
describe.each`
packageEntity | selector
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
index fc60039db30..5ea81dccf7d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/maven_installation_spec.js
@@ -35,7 +35,7 @@ describe('MavenInstallation', () => {
<artifactId>appName</artifactId>
<version>appVersion</version>
</dependency>`;
- const mavenCommandStr = 'mvn dependency:get -Dartifact=appGroup:appName:appVersion';
+ const mavenCommandStr = 'mvn install';
const mavenSetupXml = `<repositories>
<repository>
<id>gitlab-maven</id>
@@ -79,10 +79,6 @@ describe('MavenInstallation', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('install command switch', () => {
it('has the installation title component', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
index bb6846d354f..f2f3b8507c3 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/composer_spec.js
@@ -18,11 +18,6 @@ describe('Composer Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findComposerTargetSha = () => wrapper.findByTestId('composer-target-sha');
const findComposerTargetShaCopyButton = () => wrapper.findComponent(ClipboardButton);
const findComposerJson = () => wrapper.findByTestId('composer-json');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
index e7e47401aa1..2832dc3a712 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/conan_spec.js
@@ -19,11 +19,6 @@ describe('Conan Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findConanRecipe = () => wrapper.findByTestId('conan-recipe');
beforeEach(() => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
index 8680d983042..7b253a26fc7 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/maven_spec.js
@@ -19,11 +19,6 @@ describe('Maven Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findMavenApp = () => wrapper.findByTestId('maven-app');
const findMavenGroup = () => wrapper.findByTestId('maven-group');
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
index af3692023f0..9fb467f9af1 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/nuget_spec.js
@@ -19,11 +19,6 @@ describe('Nuget Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findNugetSource = () => wrapper.findByTestId('nuget-source');
const findNugetLicense = () => wrapper.findByTestId('nuget-license');
const findElementLink = (container) => container.findComponent(GlLink);
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
index d7c6ea8379d..67f5fbc9e80 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/metadata/pypi_spec.js
@@ -20,11 +20,6 @@ describe('Package Additional Metadata', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findPypiRequiredPython = () => wrapper.findByTestId('pypi-required-python');
beforeEach(() => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
index 8c0e2d948ca..e711f9ee45d 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/npm_installation_spec.js
@@ -51,10 +51,6 @@ describe('NpmInstallation', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
index 9449c40c7c6..bcc0b78bfce 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/nuget_installation_spec.js
@@ -36,10 +36,6 @@ describe('NugetInstallation', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders all the messages', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 529a6a22ddf..1dcac017ccf 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -48,10 +48,6 @@ describe('Package Files', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rows', () => {
it('renders a single file for an npm package', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index bb2fa9eb6f5..ed470f63b8a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -63,11 +63,6 @@ describe('Package History', () => {
jest.spyOn(Sentry, 'captureException').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findPackageHistoryLoader = () => wrapper.findComponent(PackageHistoryLoader);
const findHistoryElement = (testId) => wrapper.findByTestId(testId);
const findElementLink = (container) => container.findComponent(GlLink);
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
index 1fda77f2aaa..fc0ca0e898f 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_title_spec.js
@@ -38,7 +38,7 @@ describe('PackageTitle', () => {
},
provide,
directives: {
- GlResizeObserver: createMockDirective(),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
});
await nextTick();
@@ -55,21 +55,17 @@ describe('PackageTitle', () => {
const findSubHeaderText = () => wrapper.findByTestId('sub-header');
const findSubHeaderTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders', () => {
it('without tags', async () => {
await createComponent({ ...packageData(), packageFiles: { nodes: packageFiles() } });
- expect(wrapper.element).toMatchSnapshot();
+ expect(findPackageTags().exists()).toBe(false);
});
it('with tags', async () => {
await createComponent();
- expect(wrapper.element).toMatchSnapshot();
+ expect(findPackageTags().exists()).toBe(true);
});
it('with tags on mobile', async () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
index 27c0ab96cfc..e9f2a2c5095 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -1,5 +1,11 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
@@ -7,39 +13,53 @@ import RegistryList from '~/packages_and_registries/shared/components/registry_l
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import Tracking from '~/tracking';
import {
+ CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ DELETE_PACKAGE_VERSION_TRACKING_ACTION,
DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
-import { packageData } from '../../mock_data';
+import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql//queries/get_package_versions.query.graphql';
+import {
+ emptyPackageVersionsQuery,
+ packageVersionsQuery,
+ packageVersions,
+ pagination,
+} from '../../mock_data';
+
+Vue.use(VueApollo);
describe('PackageVersionsList', () => {
let wrapper;
+ let apolloProvider;
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>empty message</div>' };
- const packageList = [
- packageData({
- name: 'version 1',
- }),
- packageData({
- id: `gid://gitlab/Packages::Package/112`,
- name: 'version 2',
- }),
- ];
const uiElements = {
+ findAlert: () => wrapper.findComponent(GlAlert),
findLoader: () => wrapper.findComponent(PackagesListLoader),
findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
- findListRow: () => wrapper.findAllComponents(VersionRow),
+ findListRow: () => wrapper.findComponent(VersionRow),
+ findAllListRow: () => wrapper.findAllComponents(VersionRow),
findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
};
- const mountComponent = (props) => {
+
+ const mountComponent = ({
+ props = {},
+ resolver = jest.fn().mockResolvedValue(packageVersionsQuery()),
+ } = {}) => {
+ const requestHandlers = [[getPackageVersionsQuery, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
+
wrapper = shallowMountExtended(PackageVersionsList, {
+ apolloProvider,
propsData: {
- versions: packageList,
- pageInfo: {},
- isLoading: false,
+ packageId: packageVersionsQuery().data.package.id,
+ isMutationLoading: false,
+ count: packageVersions().length,
...props,
},
stubs: {
@@ -56,9 +76,13 @@ describe('PackageVersionsList', () => {
});
};
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ });
+
describe('when list is loading', () => {
beforeEach(() => {
- mountComponent({ isLoading: true, versions: [] });
+ mountComponent({ props: { isMutationLoading: true } });
});
it('displays loader', () => {
expect(uiElements.findLoader().exists()).toBe(true);
@@ -75,11 +99,24 @@ describe('PackageVersionsList', () => {
it('does not display registry list', () => {
expect(uiElements.findRegistryList().exists()).toBe(false);
});
+
+ it('does not display alert', () => {
+ expect(uiElements.findAlert().exists()).toBe(false);
+ });
});
describe('when list is loaded and has no data', () => {
- beforeEach(() => {
- mountComponent({ isLoading: false, versions: [] });
+ const resolver = jest.fn().mockResolvedValue(emptyPackageVersionsQuery);
+ beforeEach(async () => {
+ mountComponent({
+ props: { isMutationLoading: false, count: 0 },
+ resolver,
+ });
+ await waitForPromises();
+ });
+
+ it('skips graphql query', () => {
+ expect(resolver).not.toHaveBeenCalled();
});
it('displays empty slot message', () => {
@@ -97,11 +134,44 @@ describe('PackageVersionsList', () => {
it('does not display registry list', () => {
expect(uiElements.findRegistryList().exists()).toBe(false);
});
+
+ it('does not display alert', () => {
+ expect(uiElements.findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('if load fails, alert', () => {
+ beforeEach(async () => {
+ mountComponent({ resolver: jest.fn().mockRejectedValue() });
+
+ await waitForPromises();
+ });
+
+ it('is displayed', () => {
+ expect(uiElements.findAlert().exists()).toBe(true);
+ });
+
+ it('shows error message', () => {
+ expect(uiElements.findAlert().text()).toMatchInterpolatedText('Failed to load version data');
+ });
+
+ it('is not dismissible', () => {
+ expect(uiElements.findAlert().props('dismissible')).toBe(false);
+ });
+
+ it('is of variant danger', () => {
+ expect(uiElements.findAlert().attributes('variant')).toBe('danger');
+ });
+
+ it('error is logged in sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
});
describe('when list is loaded with data', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mountComponent();
+ await waitForPromises();
});
it('displays package registry list', () => {
@@ -110,7 +180,7 @@ describe('PackageVersionsList', () => {
it('binds the right props', () => {
expect(uiElements.findRegistryList().props()).toMatchObject({
- items: packageList,
+ items: packageVersions(),
pagination: {},
isLoading: false,
hiddenDelete: true,
@@ -118,17 +188,17 @@ describe('PackageVersionsList', () => {
});
it('displays package version rows', () => {
- expect(uiElements.findListRow().exists()).toEqual(true);
- expect(uiElements.findListRow()).toHaveLength(packageList.length);
+ expect(uiElements.findAllListRow().exists()).toEqual(true);
+ expect(uiElements.findAllListRow()).toHaveLength(packageVersions().length);
});
it('binds the correct props', () => {
- expect(uiElements.findListRow().at(0).props()).toMatchObject({
- packageEntity: expect.objectContaining(packageList[0]),
+ expect(uiElements.findAllListRow().at(0).props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageVersions()[0]),
});
- expect(uiElements.findListRow().at(1).props()).toMatchObject({
- packageEntity: expect.objectContaining(packageList[1]),
+ expect(uiElements.findAllListRow().at(1).props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageVersions()[1]),
});
});
@@ -142,20 +212,94 @@ describe('PackageVersionsList', () => {
});
describe('when user interacts with pagination', () => {
- beforeEach(() => {
- mountComponent({ pageInfo: { hasNextPage: true } });
+ const resolver = jest.fn().mockResolvedValue(packageVersionsQuery());
+
+ beforeEach(async () => {
+ mountComponent({ resolver });
+ await waitForPromises();
+ });
+
+ it('when list emits next-page fetches the next set of records', async () => {
+ uiElements.findRegistryList().vm.$emit('next-page');
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({ after: pagination().endCursor, first: GRAPHQL_PAGE_SIZE }),
+ );
});
- it('emits prev-page event when registry list emits prev event', () => {
+ it('when list emits prev-page fetches the prev set of records', async () => {
uiElements.findRegistryList().vm.$emit('prev-page');
+ await waitForPromises();
- expect(wrapper.emitted('prev-page')).toHaveLength(1);
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }),
+ );
});
+ });
- it('emits next-page when registry list emits next event', () => {
- uiElements.findRegistryList().vm.$emit('next-page');
+ describe.each`
+ description | finderFunction | deletePayload
+ ${'when the user can destroy the package'} | ${uiElements.findListRow} | ${packageVersions()[0]}
+ ${'when the user can bulk destroy packages and deletes only one package'} | ${uiElements.findRegistryList} | ${[packageVersions()[0]]}
+ `('$description', ({ finderFunction, deletePayload }) => {
+ let eventSpy;
+ const category = 'UI::NpmPackages';
+ const { findDeletePackagesModal } = uiElements;
+
+ beforeEach(async () => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ mountComponent({ props: { canDestroy: true } });
+ await waitForPromises();
+ finderFunction().vm.$emit('delete', deletePayload);
+ });
+
+ it('passes itemsToBeDeleted to the modal', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([
+ packageVersions()[0],
+ ]);
+ });
+
+ it('requesting delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ describe('when modal confirms', () => {
+ beforeEach(() => {
+ findDeletePackagesModal().vm.$emit('confirm');
+ });
+
+ it('emits delete when modal confirms', () => {
+ expect(wrapper.emitted('delete')[0][0]).toEqual([packageVersions()[0]]);
+ });
- expect(wrapper.emitted('next-page')).toHaveLength(1);
+ it('tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ DELETE_PACKAGE_VERSION_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it.each(['confirm', 'cancel'])('resets itemsToBeDeleted when modal emits %s', async (event) => {
+ await findDeletePackagesModal().vm.$emit(event);
+
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual([]);
+ });
+
+ it('canceling delete tracks the right action', () => {
+ findDeletePackagesModal().vm.$emit('cancel');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION,
+ expect.any(Object),
+ );
});
});
@@ -163,14 +307,15 @@ describe('PackageVersionsList', () => {
let eventSpy;
const { findDeletePackagesModal, findRegistryList } = uiElements;
- beforeEach(() => {
+ beforeEach(async () => {
eventSpy = jest.spyOn(Tracking, 'event');
- mountComponent({ canDestroy: true });
+ mountComponent({ props: { canDestroy: true } });
+ await waitForPromises();
});
it('binds the right props', () => {
expect(uiElements.findRegistryList().props()).toMatchObject({
- items: packageList,
+ items: packageVersions(),
pagination: {},
isLoading: false,
hiddenDelete: false,
@@ -180,11 +325,13 @@ describe('PackageVersionsList', () => {
describe('upon deletion', () => {
beforeEach(() => {
- findRegistryList().vm.$emit('delete', packageList);
+ findRegistryList().vm.$emit('delete', packageVersions());
});
it('passes itemsToBeDeleted to the modal', () => {
- expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(packageList);
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(
+ packageVersions(),
+ );
expect(wrapper.emitted('delete')).toBeUndefined();
});
@@ -202,7 +349,7 @@ describe('PackageVersionsList', () => {
});
it('emits delete event', () => {
- expect(wrapper.emitted('delete')[0]).toEqual([packageList]);
+ expect(wrapper.emitted('delete')[0]).toEqual([packageVersions()]);
});
it('tracks the right action', () => {
@@ -234,4 +381,15 @@ describe('PackageVersionsList', () => {
});
});
});
+
+ describe('with isRequestForwardingEnabled prop', () => {
+ const { findDeletePackagesModal } = uiElements;
+
+ it.each([true, false])('sets modal prop showRequestForwardingContent to %s', async (value) => {
+ mountComponent({ props: { isRequestForwardingEnabled: value } });
+ await waitForPromises();
+
+ expect(findDeletePackagesModal().props('showRequestForwardingContent')).toBe(value);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
index 4a27f8011df..3f4358bb3b0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
@@ -29,10 +29,13 @@ password = <your personal access token>`;
const findInstallationTitle = () => wrapper.findComponent(InstallationTitle);
const findSetupDocsLink = () => wrapper.findByTestId('pypi-docs-link');
- function createComponent() {
+ function createComponent(props = {}) {
wrapper = mountExtended(PypiInstallation, {
propsData: {
- packageEntity,
+ packageEntity: {
+ ...packageEntity,
+ ...props,
+ },
},
stubs: {
GlSprintf,
@@ -44,10 +47,6 @@ password = <your personal access token>`;
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('install command switch', () => {
it('has the installation title component', () => {
expect(findInstallationTitle().exists()).toBe(true);
@@ -86,6 +85,12 @@ password = <your personal access token>`;
});
});
+ it('does not have a link to personal access token docs when package is public', () => {
+ createComponent({ publicPackage: true });
+
+ expect(findAccessTokenLink().exists()).toBe(false);
+ });
+
it('has a link to the docs', () => {
expect(findSetupDocsLink().attributes()).toMatchObject({
href: PYPI_HELP_PATH,
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
index 67340822fa5..f7c8e909ff6 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
@@ -1,4 +1,4 @@
-import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
+import { GlDropdownItem, GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
@@ -24,6 +24,7 @@ describe('VersionRow', () => {
const findPackageName = () => wrapper.findComponent(GlTruncate);
const findWarningIcon = () => wrapper.findComponent(GlIcon);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
+ const findDeleteDropdownItem = () => wrapper.findComponent(GlDropdownItem);
function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
wrapper = shallowMountExtended(VersionRow, {
@@ -36,15 +37,11 @@ describe('VersionRow', () => {
GlTruncate,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a link to the version detail', () => {
createComponent();
@@ -112,6 +109,31 @@ describe('VersionRow', () => {
});
});
+ describe('delete button', () => {
+ it('does not exist when package cannot be destroyed', () => {
+ createComponent({ packageEntity: { ...packageVersion, canDestroy: false } });
+
+ expect(findDeleteDropdownItem().exists()).toBe(false);
+ });
+
+ it('exists and has the correct props', () => {
+ createComponent();
+
+ expect(findDeleteDropdownItem().exists()).toBe(true);
+ expect(findDeleteDropdownItem().attributes()).toMatchObject({
+ variant: 'danger',
+ });
+ });
+
+ it('emits the delete event when the delete button is clicked', () => {
+ createComponent();
+
+ findDeleteDropdownItem().vm.$emit('click');
+
+ expect(wrapper.emitted('delete')).toHaveLength(1);
+ });
+ });
+
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
index 689b53fa2a4..04546c4cea4 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
@@ -14,7 +14,7 @@ import {
packagesListQuery,
} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('DeletePackages', () => {
let wrapper;
@@ -67,10 +67,6 @@ describe('DeletePackages', () => {
mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('binds deletePackages method to the default slot', () => {
createComponent();
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 ec8e77fa923..c647230bc5f 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
@@ -66,11 +66,12 @@ exports[`packages_list_row renders 1`] = `
<!---->
- <package-icon-and-name-stub>
-
- npm
-
- </package-icon-and-name-stub>
+ <span
+ class="gl-ml-2"
+ data-testid="package-type"
+ >
+ · npm
+ </span>
<!---->
</div>
@@ -95,6 +96,7 @@ exports[`packages_list_row renders 1`] = `
Created
<timeago-tooltip-stub
cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
time="2020-08-17T14:23:32Z"
tooltipplacement="top"
/>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 2a78cfb13f9..81ad47b1e13 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -1,5 +1,5 @@
import { GlFormCheckbox, GlSprintf, GlTruncate } from '@gitlab/ui';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -9,7 +9,6 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
-import PackageIconAndName from '~/packages_and_registries/shared/components/package_icon_and_name.vue';
import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry/constants';
@@ -39,7 +38,7 @@ describe('packages_list_row', () => {
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackagePath = () => wrapper.findComponent(PackagePath);
const findDeleteDropdown = () => wrapper.findByTestId('action-delete');
- const findPackageIconAndName = () => wrapper.findComponent(PackageIconAndName);
+ const findPackageType = () => wrapper.findByTestId('package-type');
const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
@@ -67,15 +66,11 @@ describe('packages_list_row', () => {
selected,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
@@ -136,12 +131,11 @@ describe('packages_list_row', () => {
});
});
- it('emits the delete event when the delete button is clicked', async () => {
+ it('emits the delete event when the delete button is clicked', () => {
mountComponent({ packageEntity: packageWithoutTags });
findDeleteDropdown().vm.$emit('click');
- await nextTick();
expect(wrapper.emitted('delete')).toHaveLength(1);
});
});
@@ -237,10 +231,10 @@ describe('packages_list_row', () => {
expect(findLeftSecondaryInfos().text()).toContain('published by Administrator');
});
- it('has icon and name component', () => {
+ it('has package type with middot', () => {
mountComponent();
- expect(findPackageIconAndName().text()).toBe(packageWithoutTags.packageType.toLowerCase());
+ expect(findPackageType().text()).toBe(`· ${packageWithoutTags.packageType.toLowerCase()}`);
});
});
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 610640e0ca3..483b7a9383d 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
@@ -4,7 +4,6 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
-import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import {
@@ -17,7 +16,7 @@ import {
} from '~/packages_and_registries/package_registry/constants';
import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import Tracking from '~/tracking';
-import { packageData } from '../../mock_data';
+import { defaultPackageGroupSettings, packageData } from '../../mock_data';
describe('packages_list', () => {
let wrapper;
@@ -39,18 +38,20 @@ describe('packages_list', () => {
list: [firstPackage, secondPackage],
isLoading: false,
pageInfo: {},
+ groupSettings: defaultPackageGroupSettings,
};
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
- const findPackageListDeleteModal = () => wrapper.findComponent(DeletePackageModal);
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
const findRegistryList = () => wrapper.findComponent(RegistryList);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
+ const showMock = jest.fn();
+
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackagesList, {
propsData: {
@@ -58,10 +59,9 @@ describe('packages_list', () => {
...props,
},
stubs: {
- DeletePackageModal,
DeleteModal: stubComponent(DeleteModal, {
methods: {
- show: jest.fn(),
+ show: showMock,
},
}),
GlSprintf,
@@ -73,10 +73,6 @@ describe('packages_list', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when is loading', () => {
beforeEach(() => {
mountComponent({ isLoading: true });
@@ -123,15 +119,20 @@ describe('packages_list', () => {
});
describe('layout', () => {
- it("doesn't contain a visible modal component", () => {
+ beforeEach(() => {
mountComponent();
+ });
- expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
+ it('modal component is not shown', () => {
+ expect(showMock).not.toHaveBeenCalled();
});
- it('does not have an error alert displayed', () => {
- mountComponent();
+ it('modal component props is empty', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual([]);
+ expect(findDeletePackagesModal().props('showRequestForwardingContent')).toBe(false);
+ });
+ it('does not have an error alert displayed', () => {
expect(findErrorPackageAlert().exists()).toBe(false);
});
});
@@ -150,8 +151,8 @@ describe('packages_list', () => {
finderFunction().vm.$emit('delete', deletePayload);
});
- it('passes itemToBeDeleted to the modal', () => {
- expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
+ it('passes itemsToBeDeleted to the modal', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([firstPackage]);
});
it('requesting delete tracks the right action', () => {
@@ -162,9 +163,13 @@ describe('packages_list', () => {
);
});
+ it('modal component is shown', () => {
+ expect(showMock).toHaveBeenCalledTimes(1);
+ });
+
describe('when modal confirms', () => {
beforeEach(() => {
- findPackageListDeleteModal().vm.$emit('ok');
+ findDeletePackagesModal().vm.$emit('confirm');
});
it('emits delete when modal confirms', () => {
@@ -180,14 +185,14 @@ describe('packages_list', () => {
});
});
- it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
- await findPackageListDeleteModal().vm.$emit(event);
+ it.each(['confirm', 'cancel'])('resets itemsToBeDeleted when modal emits %s', async (event) => {
+ await findDeletePackagesModal().vm.$emit(event);
- expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual([]);
});
it('canceling delete tracks the right action', () => {
- findPackageListDeleteModal().vm.$emit('cancel');
+ findDeletePackagesModal().vm.$emit('cancel');
expect(eventSpy).toHaveBeenCalledWith(
category,
@@ -241,7 +246,7 @@ describe('packages_list', () => {
it.each(['confirm', 'cancel'])('resets itemsToBeDeleted when modal emits %s', async (event) => {
await findDeletePackagesModal().vm.$emit(event);
- expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0);
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual([]);
});
it('canceling delete tracks the right action', () => {
@@ -262,7 +267,7 @@ describe('packages_list', () => {
return nextTick();
});
- it('should display an alert message', () => {
+ it('should display an alert', () => {
expect(findErrorPackageAlert().exists()).toBe(true);
expect(findErrorPackageAlert().props('title')).toBe(
'There was an error publishing a error package package',
@@ -277,7 +282,9 @@ describe('packages_list', () => {
await nextTick();
- expect(findPackageListDeleteModal().text()).toContain(errorPackage.name);
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([errorPackage]);
});
});
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 a884959ab62..82fa5b76367 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
@@ -46,10 +46,6 @@ describe('Package Search', () => {
extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a registry search component', async () => {
mountComponent();
@@ -58,7 +54,7 @@ describe('Package Search', () => {
expect(findRegistrySearch().exists()).toBe(true);
});
- it('registry search is mounted after mount', async () => {
+ it('registry search is mounted after mount', () => {
mountComponent();
expect(findRegistrySearch().exists()).toBe(false);
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
index b47515e15c3..1296458155a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_title_spec.js
@@ -20,11 +20,6 @@ describe('PackageTitle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('title area', () => {
it('exists', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js
index fcbd7cc6a50..e9119b736c2 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/publish_method_spec.js
@@ -19,10 +19,6 @@ describe('publish_method', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
index 8f3c8667c47..c98f5f32344 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/tokens/package_type_token_spec.js
@@ -19,11 +19,6 @@ describe('packages_filter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('binds all of his attrs to filtered search token', () => {
mountComponent({ attrs: { foo: 'bar' } });
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index d897be1f344..5fb53566d4e 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -16,7 +16,7 @@ export const packagePipelines = (extend) => [
ref: 'master',
sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
project: {
- id: '1',
+ id: '14',
name: 'project14',
webUrl: 'http://gdk.test:3000/namespace14/project14',
__typename: 'Project',
@@ -103,12 +103,20 @@ export const linksData = {
},
};
+export const defaultPackageGroupSettings = {
+ mavenPackageRequestsForwarding: true,
+ npmPackageRequestsForwarding: true,
+ pypiPackageRequestsForwarding: true,
+ __typename: 'PackageSettings',
+};
+
export const packageVersions = () => [
{
createdAt: '2021-08-10T09:33:54Z',
id: 'gid://gitlab/Packages::Package/243',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ packageType: 'NPM',
canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.1',
@@ -120,6 +128,7 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/244',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ packageType: 'NPM',
canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.2',
@@ -130,7 +139,7 @@ export const packageVersions = () => [
export const packageData = (extend) => ({
__typename: 'Package',
- id: 'gid://gitlab/Packages::Package/111',
+ id: 'gid://gitlab/Packages::Package/1',
canDestroy: true,
name: '@gitlab-org/package-15',
packageType: 'NPM',
@@ -147,6 +156,7 @@ export const packageData = (extend) => ({
conanUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/conan',
pypiUrl:
'http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple',
+ publicPackage: false,
pypiSetupUrl: 'http://gdk.test:3000/api/v4/projects/1/packages/pypi',
...extend,
});
@@ -209,7 +219,10 @@ export const pagination = (extend) => ({
...extend,
});
-export const packageDetailsQuery = (extendPackage) => ({
+export const packageDetailsQuery = ({
+ extendPackage = {},
+ packageSettings = defaultPackageGroupSettings,
+} = {}) => ({
data: {
package: {
...packageData(),
@@ -225,6 +238,12 @@ export const packageDetailsQuery = (extendPackage) => ({
path: 'projectPath',
name: 'gitlab-test',
fullPath: 'gitlab-test',
+ group: {
+ id: '1',
+ packageSettings,
+ __typename: 'Group',
+ },
+ __typename: 'Project',
},
tags: {
nodes: packageTags(),
@@ -243,14 +262,6 @@ export const packageDetailsQuery = (extendPackage) => ({
},
versions: {
count: packageVersions().length,
- nodes: packageVersions(),
- pageInfo: {
- hasNextPage: true,
- hasPreviousPage: false,
- endCursor: 'endCursor',
- startCursor: 'startCursor',
- },
- __typename: 'PackageConnection',
},
dependencyLinks: {
nodes: dependencyLinks(),
@@ -297,6 +308,41 @@ export const packageMetadataQuery = (packageType) => {
};
};
+export const packageVersionsQuery = (versions = packageVersions()) => ({
+ data: {
+ package: {
+ id: 'gid://gitlab/Packages::Package/111',
+ versions: {
+ count: versions.length,
+ nodes: versions,
+ pageInfo: pagination(),
+ __typename: 'PackageConnection',
+ },
+ __typename: 'PackageDetailsType',
+ },
+ },
+});
+
+export const emptyPackageVersionsQuery = {
+ data: {
+ package: {
+ id: 'gid://gitlab/Packages::Package/111',
+ versions: {
+ count: 0,
+ nodes: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
+ __typename: 'PackageConnection',
+ },
+ __typename: 'PackageDetailsType',
+ },
+ },
+};
+
export const packagesDestroyMutation = () => ({
data: {
destroyPackages: {
@@ -351,7 +397,12 @@ export const packageDestroyFilesMutationError = () => ({
],
});
-export const packagesListQuery = ({ type = 'group', extend = {}, extendPagination = {} } = {}) => ({
+export const packagesListQuery = ({
+ type = 'group',
+ extend = {},
+ extendPagination = {},
+ packageSettings = defaultPackageGroupSettings,
+} = {}) => ({
data: {
[type]: {
id: '1',
@@ -378,6 +429,14 @@ export const packagesListQuery = ({ type = 'group', extend = {}, extendPaginatio
pageInfo: pagination(extendPagination),
__typename: 'PackageConnection',
},
+ ...(type === 'group' && { packageSettings }),
+ ...(type === 'project' && {
+ group: {
+ id: '1',
+ packageSettings,
+ __typename: 'Group',
+ },
+ }),
...extend,
__typename: capitalize(type),
},
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index b494965a3cb..0962b4fa757 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlTabs, GlTab, GlSprintf } from '@gitlab/ui';
+import { GlEmptyState, GlModal, GlTabs, GlTab, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -6,8 +6,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-
+import { createAlert } from '~/alert';
+import { stubComponent } from 'helpers/stub_component';
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/pages/details.vue';
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
@@ -18,6 +18,7 @@ import PackageTitle from '~/packages_and_registries/package_registry/components/
import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import {
+ REQUEST_FORWARDING_HELP_PAGE_PATH,
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
@@ -33,6 +34,7 @@ import {
import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql';
+import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql//queries/get_package_versions.query.graphql';
import {
packageDetailsQuery,
packageData,
@@ -42,12 +44,14 @@ import {
packageFiles,
packageDestroyFilesMutation,
packageDestroyFilesMutationError,
- pagination,
+ defaultPackageGroupSettings,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
useMockLocationHelper();
+Vue.use(VueApollo);
+
describe('PackagesApp', () => {
let wrapper;
let apolloProvider;
@@ -57,7 +61,7 @@ describe('PackagesApp', () => {
};
const provide = {
- packageId: '111',
+ packageId: '1',
emptyListIllustration: 'svgPath',
projectListUrl: 'projectListUrl',
groupListUrl: 'groupListUrl',
@@ -66,14 +70,13 @@ describe('PackagesApp', () => {
};
const { __typename, ...packageWithoutTypename } = packageData();
+ const showMock = jest.fn();
function createComponent({
resolver = jest.fn().mockResolvedValue(packageDetailsQuery()),
filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
routeId = '1',
} = {}) {
- Vue.use(VueApollo);
-
const requestHandlers = [
[getPackageDetails, resolver],
[destroyPackageFilesMutation, filesDeleteMutationResolver],
@@ -86,17 +89,11 @@ describe('PackagesApp', () => {
stubs: {
PackageTitle,
DeletePackages,
- GlModal: {
- template: `
- <div>
- <slot name="modal-title"></slot>
- <p><slot></slot></p>
- </div>
- `,
+ GlModal: stubComponent(GlModal, {
methods: {
- show: jest.fn(),
+ show: showMock,
},
- },
+ }),
GlSprintf,
GlTabs,
GlTab,
@@ -130,10 +127,7 @@ describe('PackagesApp', () => {
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1);
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findLink = () => wrapper.findComponent(GlLink);
it('renders an empty state component', async () => {
createComponent({ resolver: jest.fn().mockResolvedValue(emptyPackageDetailsQuery) });
@@ -193,7 +187,9 @@ describe('PackagesApp', () => {
createComponent({
resolver: jest.fn().mockResolvedValue(
packageDetailsQuery({
- packageType,
+ extendPackage: {
+ packageType,
+ },
}),
),
});
@@ -248,16 +244,55 @@ describe('PackagesApp', () => {
});
});
- it('shows the delete confirmation modal when delete is clicked', async () => {
- createComponent();
+ describe('when delete button is clicked', () => {
+ describe('with request forwarding enabled', () => {
+ beforeEach(async () => {
+ const resolver = jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageSettings: {
+ ...defaultPackageGroupSettings,
+ npmPackageRequestsForwarding: true,
+ },
+ }),
+ );
+ createComponent({ resolver });
- await waitForPromises();
+ await waitForPromises();
- await findDeleteButton().trigger('click');
+ await findDeleteButton().trigger('click');
+ });
- expect(findDeleteModal().find('p').text()).toBe(
- 'You are about to delete version 1.0.0 of @gitlab-org/package-15. Are you sure?',
- );
+ it('shows the delete confirmation modal with request forwarding content', () => {
+ expect(findDeleteModal().text()).toBe(
+ 'Deleting this package while request forwarding is enabled for the project can pose a security risk. Do you want to delete @gitlab-org/package-15 version 1.0.0 anyway? What are the risks?',
+ );
+ });
+
+ it('contains link to help page', () => {
+ expect(findLink().exists()).toBe(true);
+ expect(findLink().attributes('href')).toBe(REQUEST_FORWARDING_HELP_PAGE_PATH);
+ });
+ });
+
+ it('shows the delete confirmation modal without request forwarding content', async () => {
+ const resolver = jest.fn().mockResolvedValue(
+ packageDetailsQuery({
+ packageSettings: {
+ ...defaultPackageGroupSettings,
+ npmPackageRequestsForwarding: false,
+ },
+ }),
+ );
+ createComponent({ resolver });
+
+ await waitForPromises();
+
+ await findDeleteButton().trigger('click');
+
+ expect(findDeleteModal().text()).toBe(
+ 'You are about to delete version 1.0.0 of @gitlab-org/package-15. Are you sure?',
+ );
+ });
});
describe('successful request', () => {
@@ -311,7 +346,9 @@ describe('PackagesApp', () => {
createComponent({
resolver: jest
.fn()
- .mockResolvedValue(packageDetailsQuery({ packageType: PACKAGE_TYPE_COMPOSER })),
+ .mockResolvedValue(
+ packageDetailsQuery({ extendPackage: { packageType: PACKAGE_TYPE_COMPOSER } }),
+ ),
});
await waitForPromises();
@@ -322,7 +359,7 @@ describe('PackagesApp', () => {
describe('deleting a file', () => {
const [fileToDelete] = packageFiles();
- const doDeleteFile = async () => {
+ const doDeleteFile = () => {
findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
findDeleteFileModal().vm.$emit('primary');
@@ -335,25 +372,33 @@ describe('PackagesApp', () => {
await waitForPromises();
- const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show');
- const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
-
findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
- expect(showDeletePackageSpy).not.toHaveBeenCalled();
- expect(showDeleteFileSpy).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalledTimes(1);
+
+ await waitForPromises();
+
+ expect(findDeleteFileModal().text()).toBe(
+ 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?',
+ );
});
it('when its the only file opens delete package confirmation modal', async () => {
const [packageFile] = packageFiles();
const resolver = jest.fn().mockResolvedValue(
packageDetailsQuery({
- packageFiles: {
- pageInfo: {
- hasNextPage: false,
+ extendPackage: {
+ packageFiles: {
+ pageInfo: {
+ hasNextPage: false,
+ },
+ nodes: [packageFile],
+ __typename: 'PackageFileConnection',
},
- nodes: [packageFile],
- __typename: 'PackageFileConnection',
+ },
+ packageSettings: {
+ ...defaultPackageGroupSettings,
+ npmPackageRequestsForwarding: false,
},
}),
);
@@ -364,17 +409,13 @@ describe('PackagesApp', () => {
await waitForPromises();
- const showDeleteFileSpy = jest.spyOn(wrapper.vm.$refs.deleteFileModal, 'show');
- const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
-
findPackageFiles().vm.$emit('delete-files', [fileToDelete]);
- expect(showDeletePackageSpy).toHaveBeenCalled();
- expect(showDeleteFileSpy).not.toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalledTimes(1);
await waitForPromises();
- expect(findDeleteModal().find('p').text()).toBe(
+ expect(findDeleteModal().text()).toBe(
'Deleting the last package asset will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
);
});
@@ -444,7 +485,7 @@ describe('PackagesApp', () => {
});
describe('deleting multiple files', () => {
- const doDeleteFiles = async () => {
+ const doDeleteFiles = () => {
findPackageFiles().vm.$emit('delete-files', packageFiles());
findDeleteFilesModal().vm.$emit('primary');
@@ -486,6 +527,8 @@ describe('PackagesApp', () => {
await doDeleteFiles();
+ expect(resolver).toHaveBeenCalledTimes(2);
+
expect(createAlert).toHaveBeenCalledWith(
expect.objectContaining({
message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
@@ -532,11 +575,17 @@ describe('PackagesApp', () => {
it('opens the delete package confirmation modal', async () => {
const resolver = jest.fn().mockResolvedValue(
packageDetailsQuery({
- packageFiles: {
- pageInfo: {
- hasNextPage: false,
+ extendPackage: {
+ packageFiles: {
+ pageInfo: {
+ hasNextPage: false,
+ },
+ nodes: packageFiles(),
},
- nodes: packageFiles(),
+ },
+ packageSettings: {
+ ...defaultPackageGroupSettings,
+ npmPackageRequestsForwarding: false,
},
}),
);
@@ -546,15 +595,13 @@ describe('PackagesApp', () => {
await waitForPromises();
- const showDeletePackageSpy = jest.spyOn(wrapper.vm.$refs.deleteModal, 'show');
-
findPackageFiles().vm.$emit('delete-files', packageFiles());
- expect(showDeletePackageSpy).toHaveBeenCalled();
+ expect(showMock).toHaveBeenCalledTimes(1);
await waitForPromises();
- expect(findDeleteModal().find('p').text()).toBe(
+ expect(findDeleteModal().text()).toBe(
'Deleting all package assets will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?',
);
});
@@ -576,10 +623,10 @@ describe('PackagesApp', () => {
createComponent({
resolver: jest.fn().mockResolvedValue(
packageDetailsQuery({
- versions: {
- count: 0,
- nodes: [],
- pageInfo: pagination({ hasNextPage: false, hasPreviousPage: false }),
+ extendPackage: {
+ versions: {
+ count: 0,
+ },
},
}),
),
@@ -595,61 +642,62 @@ describe('PackagesApp', () => {
});
it('binds the correct props', async () => {
- const versionNodes = packageVersions();
createComponent();
await waitForPromises();
expect(findVersionsList().props()).toMatchObject({
canDestroy: true,
- versions: expect.arrayContaining(versionNodes),
+ count: packageVersions().length,
+ isMutationLoading: false,
+ packageId: 'gid://gitlab/Packages::Package/1',
+ isRequestForwardingEnabled: true,
});
});
describe('delete packages', () => {
- it('exists and has the correct props', async () => {
+ beforeEach(async () => {
createComponent();
-
await waitForPromises();
-
- expect(findDeletePackages().props()).toMatchObject({
- refetchQueries: [{ query: getPackageDetails, variables: {} }],
- showSuccessAlert: true,
- });
});
- it('deletePackages is bound to package-versions-list delete event', async () => {
- createComponent();
-
- await waitForPromises();
+ it('exists and has the correct props', () => {
+ expect(findDeletePackages().props('showSuccessAlert')).toBe(true);
+ expect(findDeletePackages().props('refetchQueries')).toEqual([
+ {
+ query: getPackageVersionsQuery,
+ variables: {
+ first: 20,
+ id: 'gid://gitlab/Packages::Package/1',
+ },
+ },
+ ]);
+ });
+ it('deletePackages is bound to package-versions-list delete event', () => {
findVersionsList().vm.$emit('delete', [{ id: 1 }]);
expect(findDeletePackages().emitted('start')).toEqual([[]]);
});
it('start and end event set loading correctly', async () => {
- createComponent();
-
- await waitForPromises();
-
findDeletePackages().vm.$emit('start');
await nextTick();
- expect(findVersionsList().props('isLoading')).toBe(true);
+ expect(findVersionsList().props('isMutationLoading')).toBe(true);
findDeletePackages().vm.$emit('end');
await nextTick();
- expect(findVersionsList().props('isLoading')).toBe(false);
+ expect(findVersionsList().props('isMutationLoading')).toBe(false);
});
});
});
describe('dependency links', () => {
- it('does not show the dependency links for a non nuget package', async () => {
+ it('does not show the dependency links for a non nuget package', () => {
createComponent();
expect(findDependenciesCountBadge().exists()).toBe(false);
@@ -659,8 +707,10 @@ describe('PackagesApp', () => {
createComponent({
resolver: jest.fn().mockResolvedValue(
packageDetailsQuery({
- packageType: PACKAGE_TYPE_NUGET,
- dependencyLinks: { nodes: [] },
+ extendPackage: {
+ packageType: PACKAGE_TYPE_NUGET,
+ dependencyLinks: { nodes: [] },
+ },
}),
),
});
@@ -676,7 +726,9 @@ describe('PackagesApp', () => {
createComponent({
resolver: jest.fn().mockResolvedValue(
packageDetailsQuery({
- packageType: PACKAGE_TYPE_NUGET,
+ extendPackage: {
+ packageType: PACKAGE_TYPE_NUGET,
+ },
}),
),
});
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 a2ec527ce12..2ee24200ed3 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -1,17 +1,18 @@
-import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { s__ } from '~/locale';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import ListPage from '~/packages_and_registries/package_registry/pages/list.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import {
- PROJECT_RESOURCE_TYPE,
- GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
@@ -21,7 +22,7 @@ import getPackagesQuery from '~/packages_and_registries/package_registry/graphql
import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
import { packagesListQuery, packageData, pagination } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('PackagesListApp', () => {
let wrapper;
@@ -31,6 +32,7 @@ describe('PackagesListApp', () => {
emptyListIllustration: 'emptyListIllustration',
isGroupPage: true,
fullPath: 'gitlab-org',
+ settingsPath: 'settings-path',
};
const PackageList = {
@@ -50,6 +52,7 @@ describe('PackagesListApp', () => {
const findListComponent = () => wrapper.findComponent(PackageList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findDeletePackages = () => wrapper.findComponent(DeletePackages);
+ const findSettingsLink = () => wrapper.findComponent(GlButton);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@@ -72,17 +75,17 @@ describe('PackagesListApp', () => {
GlLoadingIcon,
GlSprintf,
GlLink,
+ PackageTitle,
PackageList,
DeletePackages,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
- const waitForFirstRequest = async () => {
+ const waitForFirstRequest = () => {
// emit a search update so the query is executed
findSearch().vm.$emit('update', { sort: 'NAME_DESC', filters: [] });
return waitForPromises();
@@ -108,6 +111,52 @@ describe('PackagesListApp', () => {
});
});
+ describe('link to settings', () => {
+ describe('when settings path is not provided', () => {
+ beforeEach(() => {
+ mountComponent({
+ provide: {
+ ...defaultProvide,
+ settingsPath: '',
+ },
+ });
+ });
+
+ it('is not rendered', () => {
+ expect(findSettingsLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when settings path is provided', () => {
+ const label = s__('PackageRegistry|Configure in settings');
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('is rendered', () => {
+ expect(findSettingsLink().exists()).toBe(true);
+ });
+
+ it('has the right icon', () => {
+ expect(findSettingsLink().props('icon')).toBe('settings');
+ });
+
+ it('has the right attributes', () => {
+ expect(findSettingsLink().attributes()).toMatchObject({
+ 'aria-label': label,
+ href: defaultProvide.settingsPath,
+ });
+ });
+
+ it('sets tooltip with right label', () => {
+ const tooltip = getBinding(findSettingsLink().element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(label);
+ });
+ });
+ });
+
describe('search component', () => {
it('exists', () => {
mountComponent();
@@ -146,6 +195,11 @@ describe('PackagesListApp', () => {
list: expect.arrayContaining([expect.objectContaining({ id: packageData().id })]),
isLoading: false,
pageInfo: expect.objectContaining({ endCursor: pagination().endCursor }),
+ groupSettings: expect.objectContaining({
+ mavenPackageRequestsForwarding: true,
+ npmPackageRequestsForwarding: true,
+ pypiPackageRequestsForwarding: true,
+ }),
});
});
@@ -171,14 +225,14 @@ describe('PackagesListApp', () => {
});
describe.each`
- type | sortType
- ${PROJECT_RESOURCE_TYPE} | ${'sort'}
- ${GROUP_RESOURCE_TYPE} | ${'groupSort'}
+ type | sortType
+ ${WORKSPACE_PROJECT} | ${'sort'}
+ ${WORKSPACE_GROUP} | ${'groupSort'}
`('$type query', ({ type, sortType }) => {
let provide;
let resolver;
- const isGroupPage = type === GROUP_RESOURCE_TYPE;
+ const isGroupPage = type === WORKSPACE_GROUP;
beforeEach(() => {
provide = { ...defaultProvide, isGroupPage };
@@ -196,11 +250,25 @@ describe('PackagesListApp', () => {
expect.objectContaining({ isGroupPage, [sortType]: 'NAME_DESC' }),
);
});
+
+ it('list component has group settings prop set', () => {
+ expect(findListComponent().props()).toMatchObject({
+ groupSettings: expect.objectContaining({
+ mavenPackageRequestsForwarding: true,
+ npmPackageRequestsForwarding: true,
+ pypiPackageRequestsForwarding: true,
+ }),
+ });
+ });
});
- describe('empty state', () => {
+ describe.each`
+ description | resolverResponse
+ ${'empty response'} | ${packagesListQuery({ extend: { nodes: [] } })}
+ ${'error response'} | ${{ data: { group: null } }}
+ `(`$description renders empty state`, ({ resolverResponse }) => {
beforeEach(() => {
- const resolver = jest.fn().mockResolvedValue(packagesListQuery({ extend: { nodes: [] } }));
+ const resolver = jest.fn().mockResolvedValue(resolverResponse);
mountComponent({ resolver });
return waitForFirstRequest();
diff --git a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
index 796d89231f4..6dd4b9f2d20 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/dependency_proxy_settings_spec.js
@@ -28,7 +28,7 @@ import {
dependencyProxyUpdateTllPolicyMutationMock,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
describe('DependencyProxySettings', () => {
@@ -82,10 +82,6 @@ describe('DependencyProxySettings', () => {
.mockResolvedValue(dependencyProxyUpdateTllPolicyMutationMock());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findEnableProxyToggle = () => wrapper.findByTestId('dependency-proxy-setting-toggle');
const findEnableTtlPoliciesToggle = () =>
diff --git a/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js b/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js
index 86f14961690..dd1edbaa3fd 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js
@@ -23,10 +23,6 @@ describe('Exceptions Input', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findInputGroup = () => wrapper.findComponent(GlFormGroup);
const findInput = () => wrapper.findComponent(GlFormInput);
@@ -102,7 +98,7 @@ describe('Exceptions Input', () => {
});
it('disables the form input', () => {
- expect(findInput().attributes('disabled')).toBe('true');
+ expect(findInput().attributes('disabled')).toBeDefined();
});
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index 7edc321867c..3ce8e91d43d 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -19,7 +19,7 @@ import {
dependencyProxyImageTtlPolicy,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Group Settings App', () => {
let wrapper;
@@ -55,10 +55,6 @@ describe('Group Settings App', () => {
show = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAlert = () => wrapper.findComponent(GlAlert);
const findPackageSettings = () => wrapper.findComponent(PackagesSettings);
const findPackageForwardingSettings = () => wrapper.findComponent(PackagesForwardingSettings);
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 807f332f4d3..49e76cfbae0 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -23,7 +23,7 @@ import {
groupPackageSettingsMutationErrorMock,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
describe('Packages Settings', () => {
@@ -56,10 +56,6 @@ describe('Packages Settings', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
const findDescription = () => wrapper.findByTestId('description');
const findMavenSettings = () => wrapper.findByTestId('maven-settings');
@@ -181,7 +177,7 @@ describe('Packages Settings', () => {
});
});
- it('renders ExceptionsInput and assigns duplication allowness and exception props', async () => {
+ it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
mountComponent({ mountFn: mountExtended });
const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings;
@@ -196,7 +192,7 @@ describe('Packages Settings', () => {
});
});
- it('on update event calls the mutation', async () => {
+ it('on update event calls the mutation', () => {
const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock());
mountComponent({ mountFn: mountExtended, mutationResolver });
diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
index a0b257a9496..8a66a685733 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
@@ -1,12 +1,13 @@
import Vue from 'vue';
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlLink, GlSprintf } 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 { s__ } from '~/locale';
import component from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
import {
- PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ REQUEST_FORWARDING_HELP_PAGE_PATH,
PACKAGE_FORWARDING_SETTINGS_HEADER,
} from '~/packages_and_registries/settings/group/constants';
@@ -25,7 +26,7 @@ import {
mavenProps,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
describe('Packages Forwarding Settings', () => {
@@ -60,6 +61,7 @@ describe('Packages Forwarding Settings', () => {
forwardSettings,
},
stubs: {
+ GlSprintf,
SettingsBlock,
},
});
@@ -72,6 +74,7 @@ describe('Packages Forwarding Settings', () => {
const findMavenForwardingSettings = () => wrapper.findByTestId('maven');
const findNpmForwardingSettings = () => wrapper.findByTestId('npm');
const findPyPiForwardingSettings = () => wrapper.findByTestId('pypi');
+ const findRequestForwardingDocsLink = () => wrapper.findComponent(GlLink);
const fillApolloCache = () => {
apolloProvider.defaultClient.cache.writeQuery({
@@ -111,8 +114,18 @@ describe('Packages Forwarding Settings', () => {
it('has the correct description text', () => {
mountComponent();
- expect(findDescription().text()).toMatchInterpolatedText(
- PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ expect(findDescription().text()).toBe(
+ s__(
+ 'PackageRegistry|Forward package requests to a public registry if the packages are not found in the GitLab package registry.',
+ ),
+ );
+ });
+
+ it('has the right help link', () => {
+ mountComponent();
+
+ expect(findRequestForwardingDocsLink().attributes('href')).toBe(
+ REQUEST_FORWARDING_HELP_PAGE_PATH,
);
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
index 2bb99fb8e8f..cbe68df5343 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js
@@ -61,10 +61,6 @@ describe('Cleanup image tags project settings', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('isEdited status', () => {
it.each`
description | apiResponse | workingCopy | result
@@ -130,7 +126,7 @@ describe('Cleanup image tags project settings', () => {
});
describe('an admin is visiting the page', () => {
- it('shows the admin part of the alert message', async () => {
+ it('shows the admin part of the alert', async () => {
mountComponentWithApollo({
provide: { ...defaultProvidedValues, isAdmin: true },
resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
index cbb5aa52694..a68087f7f57 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_form_spec.js
@@ -46,7 +46,7 @@ describe('Container Expiration Policy Settings Form', () => {
const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]');
const findRemoveRegexInput = () => wrapper.find('[data-testid="remove-regex-input"]');
- const submitForm = async () => {
+ const submitForm = () => {
findForm().trigger('submit');
return waitForPromises();
};
@@ -124,10 +124,6 @@ describe('Container Expiration Policy Settings Form', () => {
jest.spyOn(Tracking, 'event');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
model | finder | fieldName | type | defaultValue
${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
index 43484d26d76..c9dd9ce7a45 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/container_expiration_policy_spec.js
@@ -63,10 +63,6 @@ describe('Container expiration policy project settings', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the setting form', async () => {
mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
@@ -113,7 +109,7 @@ describe('Container expiration policy project settings', () => {
});
describe('an admin is visiting the page', () => {
- it('shows the admin part of the alert message', async () => {
+ it('shows the admin part of the alert', async () => {
mountComponentWithApollo({
provide: { ...defaultProvidedValues, isAdmin: true },
resolver: jest.fn().mockResolvedValue(nullExpirationPolicyPayload()),
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
index ae41fdf65e0..058fe427106 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_dropdown_spec.js
@@ -32,11 +32,6 @@ describe('ExpirationDropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('structure', () => {
it('has a form-select component', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
index 1cea0704154..be12d108d1e 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_input_spec.js
@@ -38,11 +38,6 @@ describe('ExpirationInput', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('structure', () => {
it('has a label', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
index 653f2a8b40e..f950a9d5add 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_run_text_spec.js
@@ -23,11 +23,6 @@ describe('ExpirationToggle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('structure', () => {
it('has an input component', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
index 55a66cebd83..ec7b89aa927 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/expiration_toggle_spec.js
@@ -23,11 +23,6 @@ describe('ExpirationToggle', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('structure', () => {
it('has a toggle component', () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
index 0fbbf4ae58f..50b72d3ad72 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
@@ -48,7 +48,7 @@ describe('Packages Cleanup Policy Settings Form', () => {
wrapper.findByTestId('keep-n-duplicated-package-files-dropdown');
const findNextRunAt = () => wrapper.findByTestId('next-run-at');
- const submitForm = async () => {
+ const submitForm = () => {
findForm().trigger('submit');
return waitForPromises();
};
@@ -115,7 +115,6 @@ describe('Packages Cleanup Policy Settings Form', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
index 6dfeeca6862..94277d34f30 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_spec.js
@@ -47,7 +47,6 @@ describe('Packages cleanup policy project settings', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
index 07d13839c61..12425909454 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js
@@ -20,11 +20,6 @@ describe('Registry Settings app', () => {
const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy);
const findAlert = () => wrapper.findComponent(GlAlert);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const defaultProvide = {
showContainerRegistrySettings: true,
showPackageRegistrySettings: true,
@@ -84,7 +79,7 @@ describe('Registry Settings app', () => {
${false} | ${true}
${false} | ${false}
`(
- 'container expiration policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings',
+ 'container cleanup policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings',
({ showContainerRegistrySettings, showPackageRegistrySettings }) => {
mountComponent({
showContainerRegistrySettings,
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 e6e89806ce0..e9ee6ebdb5c 100644
--- a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
+++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap
@@ -5,7 +5,6 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
-
<ol
class="breadcrumb gl-breadcrumb-list"
>
@@ -16,29 +15,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
class=""
target="_self"
>
+ <!---->
<span>
</span>
-
- <span
- class="gl-breadcrumb-separator"
- data-testid="separator"
- >
- <span
- class="gl-mx-n5"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s8"
- data-testid="chevron-lg-right-icon"
- role="img"
- >
- <use
- href="#chevron-lg-right"
- />
- </svg>
- </span>
- </span>
</a>
</li>
@@ -52,11 +32,10 @@ exports[`Registry Breadcrumb when is not rootRoute renders 1`] = `
href="#"
target="_self"
>
+ <!---->
<span>
</span>
-
- <!---->
</a>
</li>
@@ -70,7 +49,6 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
aria-label="Breadcrumb"
class="gl-breadcrumbs"
>
-
<ol
class="breadcrumb gl-breadcrumb-list"
>
@@ -82,11 +60,10 @@ exports[`Registry Breadcrumb when is rootRoute renders 1`] = `
class=""
target="_self"
>
+ <!---->
<span>
</span>
-
- <!---->
</a>
</li>
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 18084766db9..41482e6e681 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
@@ -38,11 +38,6 @@ describe('cli_commands', () => {
mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('shows the correct text on the button', () => {
expect(findDropdownButton().text()).toContain(QUICK_START);
});
diff --git a/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
index 357dab593e8..ba5ba8f9884 100644
--- a/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/delete_package_modal_spec.js
@@ -19,11 +19,6 @@ describe('DeletePackageModal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when itemToBeDeleted prop is defined', () => {
beforeEach(() => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
deleted file mode 100644
index a0ff6ca01b5..00000000000
--- a/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import PackageIconAndName from '~/packages_and_registries/shared/components/package_icon_and_name.vue';
-
-describe('PackageIconAndName', () => {
- let wrapper;
-
- const findIcon = () => wrapper.findComponent(GlIcon);
-
- const mountComponent = () => {
- wrapper = shallowMount(PackageIconAndName, {
- slots: {
- default: 'test',
- },
- });
- };
-
- it('has an icon', () => {
- mountComponent();
-
- const icon = findIcon();
-
- expect(icon.exists()).toBe(true);
- expect(icon.props('name')).toBe('package');
- });
-
- it('renders the slot content', () => {
- mountComponent();
-
- expect(wrapper.text()).toBe('test');
- });
-});
diff --git a/spec/frontend/packages_and_registries/shared/components/package_path_spec.js b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
index 93425d4f399..3ffbb6f435c 100644
--- a/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js
@@ -9,7 +9,7 @@ describe('PackagePath', () => {
wrapper = shallowMount(PackagePath, {
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -24,11 +24,6 @@ describe('PackagePath', () => {
const findItem = (name) => wrapper.find(`[data-testid="${name}"]`);
const findTooltip = (w) => getBinding(w.element, 'gl-tooltip');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
path | rootUrl | shouldExist | shouldNotExist
${'foo/bar'} | ${'/foo/bar'} | ${[]} | ${[ROOT_CHEVRON, ELLIPSIS_ICON, ELLIPSIS_CHEVRON, LEAF_LINK]}
@@ -91,12 +86,12 @@ describe('PackagePath', () => {
});
it('root link is disabled', () => {
- expect(findItem(ROOT_LINK).attributes('disabled')).toBe('true');
+ expect(findItem(ROOT_LINK).attributes('disabled')).toBeDefined();
});
if (shouldExist.includes(LEAF_LINK)) {
it('the last link is disabled', () => {
- expect(findItem(LEAF_LINK).attributes('disabled')).toBe('true');
+ expect(findItem(LEAF_LINK).attributes('disabled')).toBeDefined();
});
}
});
diff --git a/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js b/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js
index 33e96c0775e..b025517ae47 100644
--- a/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js
@@ -20,10 +20,6 @@ describe('PackageTags', () => {
const tagBadges = () => wrapper.findAll('[data-testid="tagBadge"]');
const moreBadge = () => wrapper.find('[data-testid="moreBadge"]');
- afterEach(() => {
- if (wrapper) wrapper.destroy();
- });
-
describe('tag label', () => {
it('shows the tag label by default', () => {
createComponent();
diff --git a/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
index 0005162e0bb..e43a9f57255 100644
--- a/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js
@@ -17,11 +17,6 @@ describe('PackagesListLoader', () => {
beforeEach(createComponent);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('desktop loader', () => {
it('produces the right loader', () => {
expect(findDesktopShapes().findAll('rect[width="1000"]')).toHaveLength(20);
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 db9f96bff39..c1e86080d29 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
@@ -43,10 +43,6 @@ describe('Persisted Search', () => {
extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a registry search component', async () => {
mountComponent();
@@ -55,7 +51,7 @@ describe('Persisted Search', () => {
expect(findRegistrySearch().exists()).toBe(true);
});
- it('registry search is mounted after mount', async () => {
+ it('registry search is mounted after mount', () => {
mountComponent();
expect(findRegistrySearch().exists()).toBe(false);
diff --git a/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
index fa8f8f7641a..167599a54ea 100644
--- a/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js
@@ -20,11 +20,6 @@ describe('publish_method', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders', () => {
mountComponent(packageWithPipeline);
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
index 15db454ac68..c1f1a25d53b 100644
--- a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js
@@ -31,10 +31,6 @@ describe('Registry Breadcrumb', () => {
nameGenerator.mockClear();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when is rootRoute', () => {
beforeEach(() => {
mountComponent(routes[0]);
diff --git a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
index 2e2d5e26d33..66fca2ce12e 100644
--- a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
@@ -36,10 +36,6 @@ describe('Registry List', () => {
const findScopedSlotFirstValue = (index) => findScopedSlots().at(index).find('span');
const findScopedSlotIsSelectedValue = (index) => findScopedSlots().at(index).find('p');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('header', () => {
it('renders the title passed in the prop', () => {
mountComponent();
@@ -62,7 +58,7 @@ describe('Registry List', () => {
it('sets disabled prop to true when items length is 0', () => {
mountComponent({ propsData: { ...defaultPropsData, items: [] } });
- expect(findSelectAll().attributes('disabled')).toBe('true');
+ expect(findSelectAll().attributes('disabled')).toBeDefined();
});
it('when few are selected, sets indeterminate prop to true', async () => {
@@ -111,10 +107,21 @@ describe('Registry List', () => {
expect(findDeleteSelected().text()).toBe(component.i18n.deleteSelected);
});
- it('is hidden when hiddenDelete is true', () => {
- mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } });
+ describe('when hiddenDelete is true', () => {
+ beforeEach(() => {
+ mountComponent({ propsData: { ...defaultPropsData, hiddenDelete: true } });
+ });
- expect(findDeleteSelected().exists()).toBe(false);
+ it('is hidden', () => {
+ expect(findDeleteSelected().exists()).toBe(false);
+ });
+
+ it('populates the first slot prop correctly', () => {
+ expect(findScopedSlots().at(0).exists()).toBe(true);
+
+ // it's the first slot
+ expect(findScopedSlotFirstValue(0).text()).toBe('false');
+ });
});
it('is disabled when isLoading is true', () => {
diff --git a/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js b/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js
index a4c1b989dac..664a821c275 100644
--- a/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/settings_block_spec.js
@@ -15,10 +15,6 @@ describe('SettingsBlock', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDefaultSlot = () => wrapper.findByTestId('default-slot');
const findTitleSlot = () => wrapper.findByTestId('title-slot');
const findDescriptionSlot = () => wrapper.findByTestId('description-slot');
diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index 6edfe9641b9..6cf30e84288 100644
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+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 FIXTURE = 'abuse_reports/abuse_reports_list.html';
const MAX_MESSAGE_LENGTH = 500;
let $messages;
@@ -15,7 +15,7 @@ describe('Abuse Reports', () => {
$messages.filter((index, element) => element.innerText.indexOf(searchText) > -1).first();
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(htmlAbuseReportsList);
new AbuseReports(); // eslint-disable-line no-new
$messages = $('.abuse-reports .message');
});
diff --git a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
index d422f5dade3..176ec36fffc 100644
--- a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
@@ -1,14 +1,13 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlApplicationSettingsAccountsAndLimit from 'test_fixtures/application_settings/accounts_and_limit.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initAccountAndLimitsSection, {
PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE,
PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE,
} from '~/pages/admin/application_settings/account_and_limits';
describe('AccountAndLimits', () => {
- const FIXTURE = 'application_settings/accounts_and_limit.html';
-
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(htmlApplicationSettingsAccountsAndLimit);
initAccountAndLimitsSection();
});
diff --git a/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
index 3c512cfd6ae..72d2bb0f983 100644
--- a/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
+++ b/spec/frontend/pages/admin/application_settings/metrics_and_profiling/usage_statistics_spec.js
@@ -1,18 +1,18 @@
+import htmlApplicationSettingsUsage from 'test_fixtures/application_settings/usage.html';
import initSetHelperText, {
HELPER_TEXT_SERVICE_PING_DISABLED,
HELPER_TEXT_SERVICE_PING_ENABLED,
} from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
describe('UsageStatistics', () => {
- const FIXTURE = 'application_settings/usage.html';
let servicePingCheckBox;
let servicePingFeaturesCheckBox;
let servicePingFeaturesLabel;
let servicePingFeaturesHelperText;
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(htmlApplicationSettingsUsage);
initSetHelperText();
servicePingCheckBox = document.getElementById('application_setting_usage_ping_enabled');
servicePingFeaturesCheckBox = document.getElementById(
diff --git a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
index 366d148a608..b1d2e443d54 100644
--- a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
@@ -3,8 +3,8 @@ import { mount } from '@vue/test-utils';
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';
-import CancelJobsModal from '~/pages/admin/jobs/index/components/cancel_jobs_modal.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import CancelJobsModal from '~/pages/admin/jobs/components/cancel_jobs_modal.vue';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -41,7 +41,7 @@ describe('Cancel jobs modal', () => {
wrapper.findComponent(GlModal).vm.$emit('primary');
await nextTick();
- expect(redirectTo).toHaveBeenCalledWith(responseURL);
+ expect(redirectTo).toHaveBeenCalledWith(responseURL); // eslint-disable-line import/no-deprecated
});
it('displays error if canceling jobs failed', async () => {
@@ -60,7 +60,7 @@ describe('Cancel jobs modal', () => {
wrapper.findComponent(GlModal).vm.$emit('primary');
await nextTick();
- expect(redirectTo).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js b/spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js
index ec6369e7119..d94de48f238 100644
--- a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/cancel_jobs_spec.js
@@ -2,12 +2,12 @@ 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/index/components/cancel_jobs.vue';
-import CancelJobsModal from '~/pages/admin/jobs/index/components/cancel_jobs_modal.vue';
+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/index/components/constants';
+} from '~/pages/admin/jobs/components/constants';
describe('CancelJobs component', () => {
let wrapper;
@@ -19,8 +19,8 @@ describe('CancelJobs component', () => {
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(CancelJobs, {
directives: {
- GlModal: createMockDirective(),
- GlTooltip: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
url: `${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`,
diff --git a/spec/frontend/pages/admin/jobs/components/jobs_skeleton_loader_spec.js b/spec/frontend/pages/admin/jobs/components/jobs_skeleton_loader_spec.js
new file mode 100644
index 00000000000..03e5cd75420
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/jobs_skeleton_loader_spec.js
@@ -0,0 +1,28 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue';
+
+describe('jobs_skeleton_loader.vue', () => {
+ let wrapper;
+
+ const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const WIDTH = '1248';
+ const HEIGHT = '73';
+
+ beforeEach(() => {
+ wrapper = shallowMount(JobsSkeletonLoader);
+ });
+
+ it('renders a GlSkeletonLoader', () => {
+ expect(findGlSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('has correct width', () => {
+ expect(findGlSkeletonLoader().attributes('width')).toBe(WIDTH);
+ });
+
+ it('has correct height', () => {
+ expect(findGlSkeletonLoader().attributes('height')).toBe(HEIGHT);
+ });
+});
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
new file mode 100644
index 00000000000..dad7308ac0a
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
@@ -0,0 +1,394 @@
+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 '~/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 { createAlert } from '~/alert';
+import { TEST_HOST } from 'spec/test_constants';
+import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.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 '~/pages/admin/jobs/components/constants';
+import {
+ mockAllJobsResponsePaginated,
+ mockCancelableJobsCountResponse,
+ mockAllJobsResponseEmpty,
+ statuses,
+ mockFailedSearchToken,
+ mockAllJobsCountResponse,
+} from '../../../../../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 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 = {},
+ } = {}) => {
+ wrapper = mountFn(AdminJobsTableApp, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ provide: {
+ jobStatuses: statuses,
+ },
+ 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 () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findTabs().vm.$emit('fetchJobsByStatus');
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('avoids refetch jobs query when scope has not changed', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findTabs().vm.$emit('fetchJobsByStatus', null);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ });
+
+ it('should refetch jobs count query when the amount jobs and count do not match', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0);
+
+ // after applying filter a new count is fetched
+ findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1);
+
+ // 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(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(2);
+ });
+
+ 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);
+ },
+ );
+
+ it('refetches jobs query when filtering', async () => {
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('refetches jobs count query when filtering', async () => {
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0);
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', [mockFailedSearchToken]);
+
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('shows raw text warning when user inputs raw text', async () => {
+ const expectedWarning = {
+ message: RAW_TEXT_WARNING_ADMIN,
+ type: 'warning',
+ };
+
+ createComponent();
+
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+ jest.spyOn(wrapper.vm.$apollo.queries.jobsCount, 'refetch').mockImplementation(jest.fn());
+
+ await findFilteredSearch().vm.$emit('filterJobsBySearch', ['raw text']);
+
+ expect(createAlert).toHaveBeenCalledWith(expectedWarning);
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ expect(wrapper.vm.$apollo.queries.jobsCount.refetch).toHaveBeenCalledTimes(0);
+ });
+
+ 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',
+ });
+ 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,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js b/spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js
new file mode 100644
index 00000000000..3366d60d9f3
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js
@@ -0,0 +1,32 @@
+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';
+
+const mockJob = mockAllJobsNodes[0];
+
+describe('Project cell', () => {
+ let wrapper;
+
+ const findProjectLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ProjectCell, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('Project Link', () => {
+ beforeEach(() => {
+ createComponent({ job: mockJob });
+ });
+
+ it('shows and links to the project', () => {
+ expect(findProjectLink().exists()).toBe(true);
+ expect(findProjectLink().text()).toBe(mockJob.pipeline.project.fullPath);
+ expect(findProjectLink().attributes('href')).toBe(mockJob.pipeline.project.webUrl);
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js b/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js
new file mode 100644
index 00000000000..2f76ad66dd5
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/cells/runner_cell_spec.js
@@ -0,0 +1,64 @@
+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';
+
+const mockRunner = allRunnersData.data.runners.nodes[0];
+
+const mockJobWithRunner = {
+ id: 'gid://gitlab/Ci::Build/2264',
+ runner: mockRunner,
+};
+
+const mockJobWithoutRunner = {
+ id: 'gid://gitlab/Ci::Build/2265',
+};
+
+describe('Runner Cell', () => {
+ let wrapper;
+
+ const findRunnerLink = () => wrapper.findComponent(GlLink);
+ const findEmptyRunner = () => wrapper.find('[data-testid="empty-runner-text"]');
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(RunnerCell, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('Runner Link', () => {
+ describe('Job with runner', () => {
+ beforeEach(() => {
+ createComponent({ job: mockJobWithRunner });
+ });
+
+ it('shows and links to the runner', () => {
+ expect(findRunnerLink().exists()).toBe(true);
+ expect(findRunnerLink().text()).toBe(mockRunner.description);
+ expect(findRunnerLink().attributes('href')).toBe(mockRunner.adminUrl);
+ });
+
+ it('hides the empty runner text', () => {
+ expect(findEmptyRunner().exists()).toBe(false);
+ });
+ });
+
+ describe('Job without runner', () => {
+ beforeEach(() => {
+ createComponent({ job: mockJobWithoutRunner });
+ });
+
+ it('shows default `empty` text', () => {
+ expect(findEmptyRunner().exists()).toBe(true);
+ expect(findEmptyRunner().text()).toBe(RUNNER_EMPTY_TEXT);
+ });
+
+ it('hides the runner link', () => {
+ expect(findRunnerLink().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js b/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js
new file mode 100644
index 00000000000..59e9eda6343
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/graphql/cache_config_spec.js
@@ -0,0 +1,106 @@
+import cacheConfig from '~/pages/admin/jobs/components/table/graphql/cache_config';
+import {
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCache,
+ CIJobConnectionIncomingCacheRunningStatus,
+} from '../../../../../../jobs/mock_data';
+
+const firstLoadArgs = { first: 3, statuses: 'PENDING' };
+const runningArgs = { first: 3, statuses: 'RUNNING' };
+
+describe('jobs/components/table/graphql/cache_config', () => {
+ describe('when fetching data with the same statuses', () => {
+ it('should contain cache nodes and a status when merging caches on first load', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, {
+ args: firstLoadArgs,
+ });
+
+ expect(res.nodes).toHaveLength(CIJobConnectionIncomingCache.nodes.length);
+ expect(res.statuses).toBe('PENDING');
+ });
+
+ it('should add to existing caches when merging caches after first load', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCache,
+ {
+ args: firstLoadArgs,
+ },
+ );
+
+ expect(res.nodes).toHaveLength(
+ CIJobConnectionIncomingCache.nodes.length + CIJobConnectionExistingCache.nodes.length,
+ );
+ });
+
+ it('should not add to existing cache if the incoming elements are the same', () => {
+ // simulate that this is the last page
+ const finalExistingCache = {
+ ...CIJobConnectionExistingCache,
+ pageInfo: {
+ hasNextPage: false,
+ },
+ };
+
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ finalExistingCache,
+ {
+ args: firstLoadArgs,
+ },
+ );
+
+ expect(res.nodes).toHaveLength(CIJobConnectionExistingCache.nodes.length);
+ });
+
+ it('should contain the pageInfo key as part of the result', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge({}, CIJobConnectionIncomingCache, {
+ args: firstLoadArgs,
+ });
+
+ expect(res.pageInfo).toEqual(
+ expect.objectContaining({
+ __typename: 'PageInfo',
+ endCursor: 'eyJpZCI6IjIwNTEifQ',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'eyJpZCI6IjIxNzMifQ',
+ }),
+ );
+ });
+ });
+
+ describe('when fetching data with different statuses', () => {
+ it('should reset cache when a cache already exists', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ CIJobConnectionIncomingCacheRunningStatus,
+ {
+ args: runningArgs,
+ },
+ );
+
+ expect(res.nodes).not.toEqual(CIJobConnectionExistingCache.nodes);
+ expect(res.nodes).toHaveLength(CIJobConnectionIncomingCacheRunningStatus.nodes.length);
+ });
+ });
+
+ describe('when incoming data has no nodes', () => {
+ it('should return existing cache', () => {
+ const res = cacheConfig.typePolicies.CiJobConnection.merge(
+ CIJobConnectionExistingCache,
+ { __typename: 'CiJobConnection', count: 500 },
+ {
+ args: { statuses: 'SUCCESS' },
+ },
+ );
+
+ const expectedResponse = {
+ ...CIJobConnectionExistingCache,
+ statuses: 'SUCCESS',
+ };
+
+ expect(res).toEqual(expectedResponse);
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
index 834d14e0fb3..c00dbc0ec02 100644
--- a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -45,7 +45,7 @@ describe('NamespaceSelect', () => {
expect(findNamespaceInput().exists()).toBe(false);
});
- it('sets appropriate props', async () => {
+ it('sets appropriate props', () => {
expect(findListbox().props()).toMatchObject({
items: [
{ text: 'user: Administrator', value: '10' },
@@ -84,7 +84,7 @@ describe('NamespaceSelect', () => {
expect(findNamespaceInput().attributes('value')).toBe(selectId);
});
- it('updates the listbox value', async () => {
+ it('updates the listbox value', () => {
expect(findListbox().props()).toMatchObject({
selected: selectId,
toggleText: expectToggleText,
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 70d7cb9c839..52091d45ada 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlTodos from 'test_fixtures/todos/todos.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
@@ -18,7 +19,7 @@ describe('Todos', () => {
let mock;
beforeEach(() => {
- loadHTMLFixture('todos/todos.html');
+ setHTMLFixture(htmlTodos);
mock = new MockAdapter(axios);
return new Todos();
diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js
index ab483316086..19240f1a044 100644
--- a/spec/frontend/pages/groups/new/components/app_spec.js
+++ b/spec/frontend/pages/groups/new/components/app_spec.js
@@ -6,7 +6,9 @@ describe('App component', () => {
let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(App, { propsData });
+ wrapper = shallowMount(App, {
+ propsData: { rootPath: '/', groupsUrl: '/dashboard/groups', ...propsData },
+ });
};
const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
@@ -16,24 +18,32 @@ describe('App component', () => {
.props('panels')
.find((panel) => panel.name === 'create-group-pane');
- afterEach(() => {
- wrapper.destroy();
- });
-
it('creates correct component for group creation', () => {
createComponent();
- expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('New group');
+ expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/', text: 'Your work' },
+ { href: '/dashboard/groups', text: 'Groups' },
+ { href: '#', text: 'New group' },
+ ]);
expect(findCreateGroupPanel().title).toBe('Create group');
});
it('creates correct component for subgroup creation', () => {
- const props = { parentGroupName: 'parent', importExistingGroupPath: '/path' };
+ const detailProps = {
+ parentGroupName: 'parent',
+ importExistingGroupPath: '/path',
+ };
+
+ const props = { ...detailProps, parentGroupUrl: '/parent' };
createComponent(props);
- expect(findNewNamespacePage().props('initialBreadcrumb')).toBe('parent');
+ expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/parent', text: 'parent' },
+ { href: '#', text: 'New subgroup' },
+ ]);
expect(findCreateGroupPanel().title).toBe('Create subgroup');
- expect(findCreateGroupPanel().detailProps).toEqual(props);
+ expect(findCreateGroupPanel().detailProps).toEqual(detailProps);
});
});
diff --git a/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js b/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js
index 56a1fd03f71..35015d84085 100644
--- a/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js
+++ b/spec/frontend/pages/groups/new/components/create_group_description_details_spec.js
@@ -15,10 +15,6 @@ describe('CreateGroupDescriptionDetails component', () => {
const findLinkHref = (at) => wrapper.findAllComponents(GlLink).at(at);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('creates correct component for group creation', () => {
createComponent();
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 b020caa3010..40d5dff9d06 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
@@ -18,13 +18,6 @@ describe('BitbucketServerStatusTable', () => {
.filter((w) => w.props().variant === 'info')
.at(0);
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
function createComponent(bitbucketStatusTableStub = true) {
wrapper = shallowMount(BitbucketServerStatusTable, {
propsData: { providerTitle: 'Test', reconfigurePath: '/reconfigure' },
@@ -39,7 +32,7 @@ describe('BitbucketServerStatusTable', () => {
expect(wrapper.findComponent(BitbucketStatusTable).exists()).toBe(true);
});
- it('renders Reconfigure button', async () => {
+ it('renders Reconfigure button', () => {
createComponent(BitbucketStatusTableStub);
expect(findReconfigureButton().attributes().href).toBe('/reconfigure');
expect(findReconfigureButton().text()).toBe('Reconfigure');
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index da3954b4918..8a7fc57c409 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -68,15 +68,10 @@ describe('BulkImportsHistoryApp', () => {
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const originalApiVersion = gon.api_version;
- beforeAll(() => {
+ beforeEach(() => {
gon.api_version = 'v4';
});
- afterAll(() => {
- gon.api_version = originalApiVersion;
- });
-
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
@@ -84,7 +79,6 @@ describe('BulkImportsHistoryApp', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('general behavior', () => {
@@ -198,7 +192,7 @@ describe('BulkImportsHistoryApp', () => {
return axios.waitForAll();
});
- it('renders details button if relevant item has failures', async () => {
+ it('renders details button if relevant item has failures', () => {
expect(
extendedWrapper(wrapper.find('tbody').findAll('tr').at(1)).findByText('Details').exists(),
).toBe(true);
diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js
index 628ee8d7999..239826c1458 100644
--- a/spec/frontend/pages/import/history/components/import_error_details_spec.js
+++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js
@@ -21,22 +21,13 @@ describe('ImportErrorDetails', () => {
});
}
- const originalApiVersion = gon.api_version;
- beforeAll(() => {
- gon.api_version = 'v4';
- });
-
- afterAll(() => {
- gon.api_version = originalApiVersion;
- });
-
beforeEach(() => {
+ gon.api_version = 'v4';
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('general behavior', () => {
diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js
index 7d79583be19..8e14b5a24f8 100644
--- a/spec/frontend/pages/import/history/components/import_history_app_spec.js
+++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js
@@ -59,23 +59,13 @@ describe('ImportHistoryApp', () => {
});
}
- const originalApiVersion = gon.api_version;
- beforeAll(() => {
- gon.api_version = 'v4';
- gon.features = { fullPathProjectSearch: true };
- });
-
- afterAll(() => {
- gon.api_version = originalApiVersion;
- });
-
beforeEach(() => {
+ gon.api_version = 'v4';
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('general behavior', () => {
@@ -176,7 +166,7 @@ describe('ImportHistoryApp', () => {
return axios.waitForAll();
});
- it('renders details button if relevant item has failed', async () => {
+ it('renders details button if relevant item has failed', () => {
expect(
extendedWrapper(wrapper.find('tbody').findAll('tr').at(1)).findByText('Details').exists(),
).toBe(true);
diff --git a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
index c30b996437d..18a0098a715 100644
--- a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
+++ b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
@@ -25,8 +25,7 @@ describe('Password prompt modal', () => {
const findField = () => wrapper.findByTestId('password-prompt-field');
const findModal = () => wrapper.findComponent(GlModal);
const findConfirmBtn = () => findModal().props('actionPrimary');
- const findConfirmBtnDisabledState = () =>
- findModal().props('actionPrimary').attributes[2].disabled;
+ const findConfirmBtnDisabledState = () => findModal().props('actionPrimary').attributes.disabled;
const findCancelBtn = () => findModal().props('actionCancel');
@@ -41,10 +40,6 @@ describe('Password prompt modal', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the password field', () => {
expect(findField().exists()).toBe(true);
});
diff --git a/spec/frontend/pages/projects/forks/new/components/app_spec.js b/spec/frontend/pages/projects/forks/new/components/app_spec.js
index 0342b94a44d..e9a94878867 100644
--- a/spec/frontend/pages/projects/forks/new/components/app_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/app_spec.js
@@ -22,10 +22,6 @@ describe('App component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the correct svg illustration', () => {
expect(wrapper.find('img').attributes('src')).toBe('illustrations/project-create-new-sm.svg');
});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index f0593a854b2..722857a1420 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -6,7 +6,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as urlUtility from '~/lib/utils/url_utility';
import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -14,7 +14,7 @@ import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_name
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('ForkForm component', () => {
@@ -111,7 +111,6 @@ describe('ForkForm component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -462,7 +461,7 @@ describe('ForkForm component', () => {
await submitForm();
- expect(urlUtility.redirectTo).not.toHaveBeenCalled();
+ expect(urlUtility.redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
});
it('does not make POST request if no visibility is checked', async () => {
@@ -550,10 +549,10 @@ describe('ForkForm component', () => {
setupComponent();
await submitForm();
- expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl);
+ expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl); // eslint-disable-line import/no-deprecated
});
- it('display flash when POST is unsuccessful', async () => {
+ it('displays an alert when POST is unsuccessful', async () => {
const dummyError = 'Fork project failed';
jest.spyOn(axios, 'post').mockRejectedValue(dummyError);
@@ -561,7 +560,7 @@ describe('ForkForm component', () => {
setupComponent();
await submitForm();
- expect(urlUtility.redirectTo).not.toHaveBeenCalled();
+ expect(urlUtility.redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while forking the project. Please try again.',
});
diff --git a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
index 82f451ed6ef..b308d6305da 100644
--- a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
@@ -3,15 +3,14 @@ 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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql';
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('ProjectNamespace component', () => {
let wrapper;
- let originalGon;
const data = {
project: {
@@ -85,14 +84,8 @@ describe('ProjectNamespace component', () => {
findListBox().vm.$emit('shown');
};
- beforeAll(() => {
- originalGon = window.gon;
- window.gon = { gitlab_url: gitlabUrl };
- });
-
- afterAll(() => {
- window.gon = originalGon;
- wrapper.destroy();
+ beforeEach(() => {
+ gon.gitlab_url = gitlabUrl;
});
describe('Initial state', () => {
@@ -152,7 +145,7 @@ describe('ProjectNamespace component', () => {
await nextTick();
});
- it('creates a flash message and captures the error', () => {
+ it('creates an alert and captures the error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while loading data. Please refresh the page to try again.',
captureError: true,
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 5356953060a..882730d90ae 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlListbox, GlListboxItem } from '@gitlab/ui';
+import { GlAlert, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -22,7 +22,7 @@ describe('Code Coverage', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findAreaChart = () => wrapper.findComponent(GlAreaChart);
- const findListBox = () => wrapper.findComponent(GlListbox);
+ const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListBoxItems = () => wrapper.findAllComponents(GlListboxItem);
const findFirstListBoxItem = () => findListBoxItems().at(0);
const findSecondListBoxItem = () => findListBoxItems().at(1);
@@ -37,15 +37,10 @@ describe('Code Coverage', () => {
graphRef,
graphCsvPath,
},
- stubs: { GlListbox },
+ stubs: { GlCollapsibleListbox },
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when fetching data is successful', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 2d3b9afa8f6..07d05293a3c 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -37,10 +37,6 @@ describe('Interval Pattern Input Component', () => {
const selectCustomRadio = () => findCustomRadio().setChecked(true);
const createWrapper = (props = {}, data = {}) => {
- if (wrapper) {
- throw new Error('A wrapper already exists');
- }
-
wrapper = mount(IntervalPatternInput, {
propsData: { ...props },
data() {
@@ -64,8 +60,6 @@ describe('Interval Pattern Input Component', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
window.gl = oldWindowGl;
});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index a633332ab65..e20c2fa47a7 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -31,10 +31,6 @@ describe('Pipeline Schedule Callout', () => {
await nextTick();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not render the callout', () => {
expect(findInnerContentOfCallout().exists()).toBe(false);
});
@@ -46,10 +42,6 @@ describe('Pipeline Schedule Callout', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the callout container', () => {
expect(findInnerContentOfCallout().exists()).toBe(true);
});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
index 5771e1b88e8..03c65ab4c9c 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_settings_spec.js
@@ -31,11 +31,6 @@ describe('Project Feature Settings', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Hidden name input', () => {
it('should set the hidden name input if the name exists', () => {
wrapper = mountComponent();
diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
index 6230809a6aa..91d3057aec5 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/project_setting_row_spec.js
@@ -15,10 +15,6 @@ describe('Project Setting Row', () => {
wrapper = mountComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should show the label if it is set', async () => {
wrapper.setProps({ label: 'Test label' });
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index ff20b72c72c..a7a1e649cd0 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -140,11 +140,6 @@ describe('Settings Panel', () => {
const findMonitorVisibilityInput = () =>
findMonitorSettings().findComponent(ProjectFeatureSetting);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('Project Visibility', () => {
it('should set the project visibility help path', () => {
wrapper = mountComponent();
@@ -163,7 +158,7 @@ describe('Settings Panel', () => {
it('should disable the visibility level dropdown', () => {
wrapper = mountComponent({ canChangeVisibilityLevel: false });
- expect(findProjectVisibilityLevelInput().attributes('disabled')).toBe('disabled');
+ expect(findProjectVisibilityLevelInput().attributes('disabled')).toBeDefined();
});
it.each`
@@ -765,7 +760,7 @@ describe('Settings Panel', () => {
expect(findEnvironmentsSettings().exists()).toBe(true);
});
});
- describe('Feature Flags', () => {
+ describe('Feature flags', () => {
it('should show the feature flags toggle', () => {
wrapper = mountComponent({});
diff --git a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
index 4c4a0fbea11..6ff2bb42d8d 100644
--- a/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
+++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlSessionsNew from 'test_fixtures/sessions/new.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment';
describe('preserve_url_fragment', () => {
@@ -8,7 +9,7 @@ describe('preserve_url_fragment', () => {
};
beforeEach(() => {
- loadHTMLFixture('sessions/new.html');
+ setHTMLFixture(htmlSessionsNew);
});
afterEach(() => {
@@ -30,8 +31,6 @@ describe('preserve_url_fragment', () => {
it('does not add an empty query parameter to OmniAuth login buttons', () => {
preserveUrlFragment();
- expect(findFormAction('#oauth-login-cas3')).toBe('http://test.host/users/auth/cas3');
-
expect(findFormAction('#oauth-login-auth0')).toBe('http://test.host/users/auth/auth0');
});
@@ -39,10 +38,6 @@ describe('preserve_url_fragment', () => {
it('when "remember_me" is not present', () => {
preserveUrlFragment('#L65');
- expect(findFormAction('#oauth-login-cas3')).toBe(
- 'http://test.host/users/auth/cas3?redirect_fragment=L65',
- );
-
expect(findFormAction('#oauth-login-auth0')).toBe(
'http://test.host/users/auth/auth0?redirect_fragment=L65',
);
@@ -55,10 +50,6 @@ describe('preserve_url_fragment', () => {
preserveUrlFragment('#L65');
- expect(findFormAction('#oauth-login-cas3')).toBe(
- 'http://test.host/users/auth/cas3?remember_me=1&redirect_fragment=L65',
- );
-
expect(findFormAction('#oauth-login-auth0')).toBe(
'http://test.host/users/auth/auth0?remember_me=1&redirect_fragment=L65',
);
diff --git a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
index f736ce46f9b..cae2615e849 100644
--- a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
+++ b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
@@ -1,4 +1,5 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlStaticSigninTabs from 'test_fixtures_static/signin_tabs.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
@@ -6,7 +7,6 @@ import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
useLocalStorageSpy();
describe('SigninTabsMemoizer', () => {
- const fixtureTemplate = 'static/signin_tabs.html';
const tabSelector = 'ul.new-session-tabs';
const currentTabKey = 'current_signin_tab';
let memo;
@@ -20,7 +20,7 @@ describe('SigninTabsMemoizer', () => {
}
beforeEach(() => {
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(htmlStaticSigninTabs);
jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js
index 6a18473b1a7..1858a56b0e1 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_alert_spec.js
@@ -15,11 +15,6 @@ describe('WikiAlert', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlLink = () => wrapper.findComponent(GlLink);
const findGlSprintf = () => wrapper.findComponent(GlSprintf);
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
index c8e9a31b526..8e26453b564 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
@@ -31,11 +31,6 @@ describe('pages/shared/wikis/components/wiki_content', () => {
mock = new MockAdapter(axios);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findGlAlert = () => wrapper.findComponent(GlAlert);
const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findContent = () => wrapper.find('[data-testid="wiki-page-content"]');
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index ffcfd1d9f78..ddaa3df71e8 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -103,8 +103,6 @@ describe('WikiForm', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
- wrapper = null;
});
it('displays markdown editor', () => {
@@ -116,9 +114,9 @@ describe('WikiForm', () => {
expect.objectContaining({
value: pageInfoPersisted.content,
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
- markdownDocsPath: pageInfoPersisted.markdownHelpPath,
uploadsPath: pageInfoPersisted.uploadsPath,
autofocus: pageInfoPersisted.persisted,
+ markdownDocsPath: pageInfoPersisted.markdownHelpPath,
}),
);
@@ -172,7 +170,7 @@ describe('WikiForm', () => {
nextTick();
- expect(findMarkdownEditor().props('enablePreview')).toBe(enabled);
+ expect(findMarkdownEditor().vm.$attrs['enable-preview']).toBe(enabled);
});
it.each`
@@ -306,7 +304,7 @@ describe('WikiForm', () => {
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
});
- it('sends tracking event when editor loads', async () => {
+ it('sends tracking event when editor loads', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
@@ -320,7 +318,7 @@ describe('WikiForm', () => {
await triggerFormSubmit();
});
- it('triggers tracking events on form submit', async () => {
+ it('triggers tracking events on form submit', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
});
diff --git a/spec/frontend/pdf/index_spec.js b/spec/frontend/pdf/index_spec.js
index 98946412264..23477c73ba0 100644
--- a/spec/frontend/pdf/index_spec.js
+++ b/spec/frontend/pdf/index_spec.js
@@ -7,10 +7,6 @@ describe('PDFLab component', () => {
const mountComponent = ({ pdf }) => shallowMount(PDFLab, { propsData: { pdf } });
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('without PDF data', () => {
beforeEach(() => {
wrapper = mountComponent({ pdf: '' });
diff --git a/spec/frontend/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js
index 4cf83a3252d..1d5c5cd98c4 100644
--- a/spec/frontend/pdf/page_spec.js
+++ b/spec/frontend/pdf/page_spec.js
@@ -9,10 +9,6 @@ jest.mock('pdfjs-dist/webpack', () => {
describe('Page component', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the page when mounting', async () => {
const testPage = {
render: jest.fn().mockReturnValue({ promise: Promise.resolve() }),
diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js
index 5460feb66fe..de9cc1e8008 100644
--- a/spec/frontend/performance_bar/components/add_request_spec.js
+++ b/spec/frontend/performance_bar/components/add_request_spec.js
@@ -13,10 +13,6 @@ describe('add request form', () => {
wrapper = mount(AddRequest);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('hides the input on load', () => {
expect(findGlFormInput().exists()).toBe(false);
});
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index 5ab2c9abe5d..4194639fffe 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -38,10 +38,6 @@ describe('detailedMetric', () => {
const findAllSummaryItems = () =>
wrapper.findAllByTestId('performance-bar-summary-item').wrappers.map((w) => w.text());
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the current request has no details', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/performance_bar/components/performance_bar_app_spec.js b/spec/frontend/performance_bar/components/performance_bar_app_spec.js
index 2c9ab4bf78d..7a018236314 100644
--- a/spec/frontend/performance_bar/components/performance_bar_app_spec.js
+++ b/spec/frontend/performance_bar/components/performance_bar_app_spec.js
@@ -1,18 +1,53 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
import PerformanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
describe('performance bar app', () => {
+ let wrapper;
const store = new PerformanceBarStore();
- const wrapper = shallowMount(PerformanceBarApp, {
- propsData: {
- store,
- env: 'development',
- requestId: '123',
- statsUrl: 'https://log.gprd.gitlab.net/app/dashboards#/view/',
- peekUrl: '/-/peek/results',
- profileUrl: '?lineprofiler=true',
- },
+ store.addRequest('123', 'https://gitlab.com', '', {}, 'GET');
+ const createComponent = () => {
+ wrapper = mount(PerformanceBarApp, {
+ propsData: {
+ store,
+ env: 'development',
+ requestId: '123',
+ requestMethod: 'GET',
+ statsUrl: 'https://log.gprd.gitlab.net/app/dashboards#/view/',
+ peekUrl: '/-/peek/results',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('flamegraph buttons', () => {
+ const flamegraphDiv = () => wrapper.find('#peek-flamegraph');
+ const flamegraphLinks = () => flamegraphDiv().findAllComponents(GlLink);
+
+ it('creates three flamegraph buttons based on the path', () => {
+ expect(flamegraphLinks()).toHaveLength(3);
+
+ ['wall', 'cpu', 'object'].forEach((path, index) => {
+ expect(flamegraphLinks().at(index).attributes('href')).toBe(
+ `https://gitlab.com?performance_bar=flamegraph&stackprof_mode=${path}`,
+ );
+ });
+ });
+ });
+
+ describe('memory report button', () => {
+ const memoryReportDiv = () => wrapper.find('#peek-memory-report');
+ const memoryReportLink = () => memoryReportDiv().findComponent(GlLink);
+
+ it('creates memory report button', () => {
+ expect(memoryReportLink().attributes('href')).toEqual(
+ 'https://gitlab.com?performance_bar=memory',
+ );
+ });
});
it('sets the class to match the environment', () => {
diff --git a/spec/frontend/performance_bar/components/request_warning_spec.js b/spec/frontend/performance_bar/components/request_warning_spec.js
index 9dd8ea9f933..7b6d8ff695d 100644
--- a/spec/frontend/performance_bar/components/request_warning_spec.js
+++ b/spec/frontend/performance_bar/components/request_warning_spec.js
@@ -5,10 +5,6 @@ describe('request warning', () => {
let wrapper;
const htmlId = 'request-123';
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the request has warnings', () => {
beforeEach(() => {
wrapper = shallowMount(RequestWarning, {
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index f09b0cc3df8..1849c373326 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -20,9 +20,9 @@ describe('performance bar wrapper', () => {
peekWrapper.setAttribute('id', 'js-peek');
peekWrapper.dataset.env = 'development';
peekWrapper.dataset.requestId = '123';
+ peekWrapper.dataset.requestMethod = 'GET';
peekWrapper.dataset.peekUrl = '/-/peek/results';
peekWrapper.dataset.statsUrl = 'https://log.gprd.gitlab.net/app/dashboards#/view/';
- peekWrapper.dataset.profileUrl = '?lineprofiler=true';
mock = new MockAdapter(axios);
@@ -70,7 +70,13 @@ describe('performance bar wrapper', () => {
it('adds the request immediately', () => {
vm.addRequest('123', 'https://gitlab.com/');
- expect(vm.store.addRequest).toHaveBeenCalledWith('123', 'https://gitlab.com/', undefined);
+ expect(vm.store.addRequest).toHaveBeenCalledWith(
+ '123',
+ 'https://gitlab.com/',
+ undefined,
+ undefined,
+ undefined,
+ );
});
});
diff --git a/spec/frontend/performance_bar/services/performance_bar_service_spec.js b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
index 1bb70a43a1b..b1f5f4d6982 100644
--- a/spec/frontend/performance_bar/services/performance_bar_service_spec.js
+++ b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
@@ -66,7 +66,7 @@ describe('PerformanceBarService', () => {
describe('operationName', () => {
function requestUrl(response, peekUrl) {
- return PerformanceBarService.callbackParams(response, peekUrl)[3];
+ return PerformanceBarService.callbackParams(response, peekUrl)[4];
}
it('gets the operation name from response.config', () => {
diff --git a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
index 7d5c5031792..170469db6ad 100644
--- a/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
+++ b/spec/frontend/performance_bar/stores/performance_bar_store_spec.js
@@ -46,6 +46,14 @@ describe('PerformanceBarStore', () => {
store.addRequest('id', 'http://localhost:3001/api/graphql', 'someOperation');
expect(findUrl('id')).toBe('graphql (someOperation)');
});
+
+ it('appends the number of batches queries when it is a GraphQL call', () => {
+ store.addRequest('id', 'http://localhost:3001/api/graphql', 'someOperation');
+ store.addRequest('id', 'http://localhost:3001/api/graphql', 'anotherOperation');
+ store.addRequest('id', 'http://localhost:3001/api/graphql', 'anotherOne');
+ store.addRequest('anotherId', 'http://localhost:3001/api/graphql', 'operationName');
+ expect(findUrl('id')).toBe('graphql (someOperation) [3 queries batched]');
+ });
});
describe('setRequestDetailsData', () => {
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 6519989661f..376575a8acb 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+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 PersistentUserCallout from '~/persistent_user_callout';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('PersistentUserCallout', () => {
const dismissEndpoint = '/dismiss';
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index fa30b9c2b97..7095525e948 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -74,10 +74,6 @@ describe('Pipeline Wizard - Commit Page', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows a commit message input with the correct label', () => {
expect(wrapper.findByTestId('commit_message').exists()).toBe(true);
expect(wrapper.find('label[for="commit_message"]').text()).toBe(i18n.commitMessageLabel);
@@ -121,10 +117,6 @@ describe('Pipeline Wizard - Commit Page', () => {
expect(wrapper.findByTestId('load-error').exists()).toBe(true);
expect(wrapper.findByTestId('load-error').text()).toBe(i18n.errors.loadError);
});
-
- afterEach(() => {
- wrapper.destroy();
- });
});
describe('commit result handling', () => {
@@ -136,7 +128,7 @@ describe('Pipeline Wizard - Commit Page', () => {
await waitForPromises();
});
- it('will not show an error', async () => {
+ it('will not show an error', () => {
expect(wrapper.findByTestId('commit-error').exists()).not.toBe(true);
});
@@ -151,7 +143,6 @@ describe('Pipeline Wizard - Commit Page', () => {
});
afterEach(() => {
- wrapper.destroy();
jest.clearAllMocks();
});
});
@@ -164,7 +155,7 @@ describe('Pipeline Wizard - Commit Page', () => {
await waitForPromises();
});
- it('will show an error', async () => {
+ it('will show an error', () => {
expect(wrapper.findByTestId('commit-error').exists()).toBe(true);
expect(wrapper.findByTestId('commit-error').text()).toBe(i18n.errors.commitError);
});
@@ -178,7 +169,6 @@ describe('Pipeline Wizard - Commit Page', () => {
});
afterEach(() => {
- wrapper.destroy();
jest.clearAllMocks();
});
});
@@ -246,15 +236,11 @@ describe('Pipeline Wizard - Commit Page', () => {
await waitForPromises();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('sets up without error', async () => {
+ it('sets up without error', () => {
expect(consoleSpy).not.toHaveBeenCalled();
});
- it('does not show a load error', async () => {
+ it('does not show a load error', () => {
expect(wrapper.findByTestId('load-error').exists()).not.toBe(true);
});
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
index dd0a609043a..6d7d4363189 100644
--- a/spec/frontend/pipeline_wizard/components/editor_spec.js
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -9,10 +9,6 @@ describe('Pages Yaml Editor wrapper', () => {
propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' },
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('mount hook', () => {
beforeEach(() => {
wrapper = mount(YamlEditor, defaultOptions);
diff --git a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
index f288264a11e..7f521e2523e 100644
--- a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js
@@ -33,10 +33,6 @@ describe('Pipeline Wizard -- Input Wrapper', () => {
inputChild = wrapper.findComponent(TextWidget);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('will replace its value in compiled', async () => {
await inputChild.vm.$emit('input', inputValue);
const expected = new Document({
@@ -54,10 +50,6 @@ describe('Pipeline Wizard -- Input Wrapper', () => {
});
describe('Target Path Discovery', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
scenario | template | target | expected
${'simple nested object'} | ${{ foo: { bar: { baz: '$BOO' } } }} | ${'$BOO'} | ${['foo', 'bar', 'baz']}
diff --git a/spec/frontend/pipeline_wizard/components/step_nav_spec.js b/spec/frontend/pipeline_wizard/components/step_nav_spec.js
index c6eac1386fa..8a94f58523a 100644
--- a/spec/frontend/pipeline_wizard/components/step_nav_spec.js
+++ b/spec/frontend/pipeline_wizard/components/step_nav_spec.js
@@ -19,17 +19,13 @@ describe('Pipeline Wizard - Step Navigation Component', () => {
nextButton = wrapper.findByTestId('next-button');
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
scenario | showBackButton | showNextButton
${'does not show prev button'} | ${false} | ${false}
${'has prev, but not next'} | ${true} | ${false}
${'has next, but not prev'} | ${false} | ${true}
${'has both next and prev'} | ${true} | ${true}
- `('$scenario', async ({ showBackButton, showNextButton }) => {
+ `('$scenario', ({ showBackButton, showNextButton }) => {
createComponent({ showBackButton, showNextButton });
expect(prevButton.exists()).toBe(showBackButton);
@@ -57,16 +53,16 @@ describe('Pipeline Wizard - Step Navigation Component', () => {
expect(wrapper.emitted().next.length).toBe(1);
});
- it('enables the next button if nextButtonEnabled ist set to true', async () => {
+ it('enables the next button if nextButtonEnabled ist set to true', () => {
createComponent({ nextButtonEnabled: true });
- expect(nextButton.attributes('disabled')).not.toBe('disabled');
+ expect(nextButton.attributes('disabled')).toBeUndefined();
});
- it('disables the next button if nextButtonEnabled ist set to false', async () => {
+ it('disables the next button if nextButtonEnabled ist set to false', () => {
createComponent({ nextButtonEnabled: false });
- expect(nextButton.attributes('disabled')).toBe('disabled');
+ expect(nextButton.attributes('disabled')).toBeDefined();
});
it('does not emit "next" event when clicking next button while nextButtonEnabled ist set to false', async () => {
diff --git a/spec/frontend/pipeline_wizard/components/step_spec.js b/spec/frontend/pipeline_wizard/components/step_spec.js
index 00b57f95ccc..99a7eff7acc 100644
--- a/spec/frontend/pipeline_wizard/components/step_spec.js
+++ b/spec/frontend/pipeline_wizard/components/step_spec.js
@@ -56,10 +56,6 @@ describe('Pipeline Wizard - Step Page', () => {
});
};
- afterEach(async () => {
- await wrapper.destroy();
- });
-
describe('input children', () => {
beforeEach(() => {
createComponent();
@@ -207,7 +203,7 @@ describe('Pipeline Wizard - Step Page', () => {
findInputWrappers();
});
- it('injects the template when an input wrapper emits a beforeUpdate:compiled event', async () => {
+ it('injects the template when an input wrapper emits a beforeUpdate:compiled event', () => {
input1.vm.$emit('beforeUpdate:compiled');
expect(wrapper.vm.compiled.toString()).toBe(compiledYamlAfterInitialLoad);
diff --git a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
index b8e194015b0..52e5d49ec99 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/checklist_spec.js
@@ -39,10 +39,6 @@ describe('Pipeline Wizard - Checklist Widget', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('creates the component', () => {
createComponent();
expect(wrapper.exists()).toBe(true);
diff --git a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
index c9e9f5caebe..df8841e6ad3 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js
@@ -39,10 +39,6 @@ describe('Pipeline Wizard - List Widget', () => {
};
describe('component setup and interface', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('prints the label inside the legend', () => {
createComponent();
@@ -55,7 +51,7 @@ describe('Pipeline Wizard - List Widget', () => {
expect(findGlFormGroup().attributes('labeldescription')).toBe(defaultProps.description);
});
- it('sets the input field type attribute to "text"', async () => {
+ it('sets the input field type attribute to "text"', () => {
createComponent();
expect(findFirstGlFormInputGroup().attributes('type')).toBe('text');
@@ -168,11 +164,7 @@ describe('Pipeline Wizard - List Widget', () => {
});
describe('form validation', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('does not show validation state when untouched', async () => {
+ it('does not show validation state when untouched', () => {
createComponent({}, mountExtended);
expect(findGlFormGroup().classes()).not.toContain('is-valid');
expect(findGlFormGroup().classes()).not.toContain('is-invalid');
diff --git a/spec/frontend/pipeline_wizard/components/widgets/text_spec.js b/spec/frontend/pipeline_wizard/components/widgets/text_spec.js
index a11c0214d15..041ca05fd2c 100644
--- a/spec/frontend/pipeline_wizard/components/widgets/text_spec.js
+++ b/spec/frontend/pipeline_wizard/components/widgets/text_spec.js
@@ -27,12 +27,6 @@ describe('Pipeline Wizard - Text Widget', () => {
});
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
it('creates an input element with the correct label', () => {
createComponent();
@@ -123,7 +117,7 @@ describe('Pipeline Wizard - Text Widget', () => {
expect(findGlFormGroup().classes()).toContain('is-invalid');
});
- it('does not update validation if not required', async () => {
+ it('does not update validation if not required', () => {
createComponent({
pattern: null,
validate: true,
diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
index 33c6394eb41..b8d84572873 100644
--- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
@@ -48,10 +48,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
wrapper.find(`[data-input-target="${target}"]`).find('input');
describe('display', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('shows the steps', () => {
createComponent();
@@ -86,7 +82,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
expect(wrapper.findByTestId('editor-header').text()).toBe(expectedMessage);
});
- it('shows the editor header with a custom filename', async () => {
+ it('shows the editor header with a custom filename', () => {
const filename = 'my-file.yml';
createComponent({
filename,
@@ -145,12 +141,8 @@ describe('Pipeline Wizard - wrapper.vue', () => {
}
});
- afterEach(() => {
- wrapper.destroy();
- });
-
if (expectCommitStepShown) {
- it('does not show the step wrapper', async () => {
+ it('does not show the step wrapper', () => {
expect(wrapper.findComponent(WizardStep).isVisible()).toBe(false);
});
@@ -158,7 +150,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
expect(wrapper.findComponent(CommitStep).isVisible()).toBe(true);
});
} else {
- it('passes the correct step config to the step component', async () => {
+ it('passes the correct step config to the step component', () => {
expect(getStepWrapper().props('inputs')).toMatchObject(expectStepDef.inputs);
});
@@ -188,10 +180,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('initially shows a placeholder', async () => {
const editorContent = getEditorContent();
@@ -223,10 +211,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
createComponent();
});
- afterAll(() => {
- wrapper.destroy();
- });
-
it('editor reflects changes', async () => {
const newCompiledDoc = new Document({ faa: 'bur' });
await getStepWrapper().vm.$emit('update:compiled', newCompiledDoc);
@@ -240,10 +224,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('highlight requests by the step get passed on to the editor', async () => {
const highlight = 'foo';
@@ -266,7 +246,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
describe('integration test', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({}, mountExtended);
});
@@ -309,7 +289,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
});
afterEach(() => {
- wrapper.destroy();
inputField = undefined;
});
@@ -331,10 +310,6 @@ describe('Pipeline Wizard - wrapper.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits done', () => {
expect(wrapper.emitted('done')).toBeUndefined();
diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
index 13234525159..e7bd7f686b6 100644
--- a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
+++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js
@@ -24,10 +24,6 @@ describe('PipelineWizard', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('mounts without error', () => {
const consoleSpy = jest.spyOn(console, 'error');
diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
index 28a08b6da0f..124f02bcec7 100644
--- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js
@@ -14,10 +14,6 @@ describe('The DAG annotations', () => {
const getToggleButton = () => wrapper.findComponent(GlButton);
const createComponent = (propsData = {}, method = shallowMount) => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
-
wrapper = method(DagAnnotations, {
propsData,
data() {
@@ -28,11 +24,6 @@ describe('The DAG annotations', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when there is one annotation', () => {
const currentNote = singleNote['dag-link103'];
diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
index 4619548d1bb..6b46be3dd49 100644
--- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
@@ -36,11 +36,6 @@ describe('The DAG graph', () => {
createComponent({ graphData: parsedData });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('in the basic case', () => {
beforeEach(() => {
/*
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index b0c26976c85..53719065611 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -30,10 +30,6 @@ describe('Pipeline DAG graph wrapper', () => {
provideOverride = {},
method = shallowMount,
} = {}) => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
-
wrapper = method(Dag, {
provide: {
pipelineProjectPath: 'root/abc-dag',
@@ -51,11 +47,6 @@ describe('Pipeline DAG graph wrapper', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when a query argument is undefined', () => {
beforeEach(() => {
createComponent({
@@ -64,7 +55,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
- it('does not render the graph', async () => {
+ it('does not render the graph', () => {
expect(getGraph().exists()).toBe(false);
});
@@ -75,7 +66,7 @@ describe('Pipeline DAG graph wrapper', () => {
describe('when all query variables are defined', () => {
describe('but the parse fails', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
graphData: unparseableGraph,
});
@@ -93,7 +84,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('parse succeeds', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({ method: mount });
});
@@ -107,7 +98,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('parse succeeds, but the resulting graph is too small', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
graphData: tooSmallGraph,
});
@@ -125,7 +116,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('the returned data is empty', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
method: mount,
graphData: graphWithoutDependencies,
@@ -144,7 +135,7 @@ describe('Pipeline DAG graph wrapper', () => {
});
describe('annotations', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
index d1da7cb3acf..6a2453704db 100644
--- a/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_app_spec.js
@@ -4,15 +4,15 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+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, mockFailedJobsSummaryData } from '../../mock_data';
+import { mockFailedJobsQueryResponse } from '../../mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Failed Jobs App', () => {
let wrapper;
@@ -27,15 +27,12 @@ describe('Failed Jobs App', () => {
return createMockApollo(requestHandlers);
};
- const createComponent = (resolver, failedJobsSummaryData = mockFailedJobsSummaryData) => {
+ const createComponent = (resolver) => {
wrapper = shallowMount(FailedJobsApp, {
provide: {
fullPath: 'root/ci-project',
pipelineIid: 1,
},
- propsData: {
- failedJobsSummary: failedJobsSummaryData,
- },
apolloProvider: createMockApolloProvider(resolver),
});
};
@@ -44,10 +41,6 @@ describe('Failed Jobs App', () => {
resolverSpy = jest.fn().mockResolvedValue(mockFailedJobsQueryResponse);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading spinner', () => {
it('displays loading spinner when fetching failed jobs', () => {
createComponent(resolverSpy);
diff --git a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
index 0df15afd70d..d5307b87a11 100644
--- a/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
+++ b/spec/frontend/pipelines/components/jobs/failed_jobs_table_spec.js
@@ -4,18 +4,18 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+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 {
successRetryMutationResponse,
failedRetryMutationResponse,
- mockPreparedFailedJobsData,
- mockPreparedFailedJobsDataNoPermission,
+ mockFailedJobsData,
+ mockFailedJobsDataNoPermission,
} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);
@@ -30,13 +30,15 @@ describe('Failed Jobs Table', () => {
const findRetryButton = () => wrapper.findComponent(GlButton);
const findJobLink = () => wrapper.findComponent(GlLink);
const findJobLog = () => wrapper.findByTestId('job-log');
+ const findSummary = (index) => wrapper.findAllByTestId('job-trace-summary').at(index);
+ const findFirstFailureMessage = () => wrapper.findAllByTestId('job-failure-message').at(0);
const createMockApolloProvider = (resolver) => {
const requestHandlers = [[RetryFailedJobMutation, resolver]];
return createMockApollo(requestHandlers);
};
- const createComponent = (resolver, failedJobsData = mockPreparedFailedJobsData) => {
+ const createComponent = (resolver, failedJobsData = mockFailedJobsData) => {
wrapper = mountExtended(FailedJobsTable, {
propsData: {
failedJobs: failedJobsData,
@@ -45,23 +47,37 @@ describe('Failed Jobs Table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the failed jobs table', () => {
createComponent();
expect(findJobsTable().exists()).toBe(true);
});
+ it('displays failed job summary', () => {
+ createComponent();
+
+ expect(findSummary(0).text()).toBe('Html Summary');
+ });
+
+ it('displays no job log when no trace', () => {
+ createComponent();
+
+ expect(findSummary(1).text()).toBe('No job log');
+ });
+
+ it('displays failure reason', () => {
+ createComponent();
+
+ expect(findFirstFailureMessage().text()).toBe('Job failed');
+ });
+
it('calls the retry failed job mutation correctly', () => {
createComponent(successRetryMutationHandler);
findRetryButton().trigger('click');
expect(successRetryMutationHandler).toHaveBeenCalledWith({
- id: mockPreparedFailedJobsData[0].id,
+ id: mockFailedJobsData[0].id,
});
});
@@ -78,7 +94,7 @@ describe('Failed Jobs Table', () => {
await waitForPromises();
- expect(redirectTo).toHaveBeenCalledWith(job.detailedStatus.detailsPath);
+ expect(redirectTo).toHaveBeenCalledWith(job.detailedStatus.detailsPath); // eslint-disable-line import/no-deprecated
});
it('shows error message if the retry failed job mutation fails', async () => {
@@ -94,7 +110,7 @@ describe('Failed Jobs Table', () => {
});
it('hides the job log and retry button if a user does not have permission', () => {
- createComponent([[]], mockPreparedFailedJobsDataNoPermission);
+ createComponent([[]], mockFailedJobsDataNoPermission);
expect(findJobLog().exists()).toBe(false);
expect(findRetryButton().exists()).toBe(false);
@@ -110,8 +126,6 @@ describe('Failed Jobs Table', () => {
it('job name links to the correct job', () => {
createComponent();
- expect(findJobLink().attributes('href')).toBe(
- mockPreparedFailedJobsData[0].detailedStatus.detailsPath,
- );
+ expect(findJobLink().attributes('href')).toBe(mockFailedJobsData[0].detailedStatus.detailsPath);
});
});
diff --git a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
index 9bc14266593..39475788fe2 100644
--- a/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
+++ b/spec/frontend/pipelines/components/jobs/jobs_app_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+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';
@@ -12,7 +12,7 @@ import { mockPipelineJobsQueryResponse } from '../../mock_data';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Jobs app', () => {
let wrapper;
@@ -34,7 +34,7 @@ describe('Jobs app', () => {
const createComponent = (resolver) => {
wrapper = shallowMount(JobsApp, {
provide: {
- fullPath: 'root/ci-project',
+ projectPath: 'root/ci-project',
pipelineIid: 1,
},
apolloProvider: createMockApolloProvider(resolver),
@@ -45,10 +45,6 @@ describe('Jobs app', () => {
resolverSpy = jest.fn().mockResolvedValue(mockPipelineJobsQueryResponse);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading spinner', () => {
const setup = async () => {
createComponent(resolverSpy);
diff --git a/spec/frontend/pipelines/components/jobs/utils_spec.js b/spec/frontend/pipelines/components/jobs/utils_spec.js
deleted file mode 100644
index 720446cfda3..00000000000
--- a/spec/frontend/pipelines/components/jobs/utils_spec.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { prepareFailedJobs } from '~/pipelines/components/jobs/utils';
-import {
- mockFailedJobsData,
- mockFailedJobsSummaryData,
- mockPreparedFailedJobsData,
-} from '../../mock_data';
-
-describe('Utils', () => {
- it('prepares failed jobs data correctly', () => {
- expect(prepareFailedJobs(mockFailedJobsData, mockFailedJobsSummaryData)).toEqual(
- mockPreparedFailedJobsData,
- );
- });
-});
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
index 5ea57c51e70..a4ecb9041c9 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
@@ -19,7 +19,7 @@ describe('Linked pipeline mini list', () => {
const createComponent = (props = {}) => {
wrapper = mount(LinkedPipelinesMiniList, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
...props,
@@ -34,11 +34,6 @@ describe('Linked pipeline mini list', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render one linked pipeline item', () => {
expect(findLinkedPipelineMiniItem().exists()).toBe(true);
});
@@ -102,11 +97,6 @@ describe('Linked pipeline mini list', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render three linked pipeline items', () => {
expect(findLinkedPipelineMiniItems().exists()).toBe(true);
expect(findLinkedPipelineMiniItems().length).toBe(3);
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
index 036b82530d5..e7415a6c596 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js
@@ -33,11 +33,6 @@ describe('Pipeline Mini Graph', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render the pipeline stages', () => {
expect(findPipelineStages().exists()).toBe(true);
});
@@ -71,11 +66,6 @@ describe('Pipeline Mini Graph', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should have the correct props', () => {
expect(findPipelineMiniGraph().props()).toMatchObject({
downstreamPipelines: [],
@@ -118,11 +108,6 @@ describe('Pipeline Mini Graph', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render the downstream linked pipelines mini list only', () => {
expect(findLinkedPipelineDownstream().exists()).toBe(true);
expect(findLinkedPipelineUpstream().exists()).toBe(false);
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
index ab2056b4035..21d92fec9bf 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
@@ -45,11 +45,10 @@ describe('Pipelines stage component', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
eventHub.$emit.mockRestore();
mock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
+ wrapper.destroy();
});
const findCiActionBtn = () => wrapper.find('.js-ci-action');
@@ -130,7 +129,7 @@ describe('Pipelines stage component', () => {
await axios.waitForAll();
});
- it('renders the received data and emits the correct events', async () => {
+ it('renders the received data and emits the correct events', () => {
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
expect(findDropdownMenuTitle().text()).toContain(stageReply.name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
index c123f53886e..73e810bde99 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js
@@ -60,9 +60,4 @@ describe('Pipeline Stages', () => {
expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(true);
expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(true);
});
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
});
diff --git a/spec/frontend/pipelines/components/pipeline_tabs_spec.js b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
index c2cb95d4320..fde13128662 100644
--- a/spec/frontend/pipelines/components/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_tabs_spec.js
@@ -19,7 +19,6 @@ describe('The Pipeline Tabs', () => {
const defaultProvide = {
defaultTabValue: '',
failedJobsCount: 1,
- failedJobsSummary: [],
totalJobCount: 10,
testsCount: 123,
};
@@ -39,10 +38,6 @@ describe('The Pipeline Tabs', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Tabs', () => {
it.each`
tabName | tabComponent
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index ba7262353f0..51a4487a3ef 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -51,8 +51,6 @@ describe('Pipelines filtered search', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
- wrapper = null;
});
it('displays UI elements', () => {
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
index 6531a15ab8e..b560eea4882 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ci_templates_spec.js
@@ -29,11 +29,6 @@ describe('CI Templates', () => {
const findTemplateName = () => wrapper.findByTestId('template-name');
const findTemplateLogo = () => wrapper.findByTestId('template-logo');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('renders template list', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
index 0c2938921d6..700be076e0c 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/ios_templates_spec.js
@@ -37,11 +37,6 @@ describe('iOS Templates', () => {
const findSetupRunnerLink = () => wrapper.findByText('Set up a runner');
const configurePipelineLink = () => wrapper.findByTestId('configure-pipeline-link');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when ios runners are not available', () => {
beforeEach(() => {
wrapper = createWrapper({ iosRunnersAvailable: false });
diff --git a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
index f255e0d857f..4bf4257f462 100644
--- a/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates_spec.js
@@ -1,24 +1,10 @@
import '~/commons';
-import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { stubExperiments } from 'helpers/experimentation_helper';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
-import ExperimentTracking from '~/experimentation/experiment_tracking';
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 {
- RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
- RUNNERS_SETTINGS_LINK_CLICKED_EVENT,
- RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
- RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
- I18N,
-} from '~/ci/pipeline_editor/constants';
const pipelineEditorPath = '/-/ci/editor';
-const ciRunnerSettingsPath = '/-/settings/ci_cd';
-
-jest.mock('~/experimentation/experiment_tracking');
describe('Pipelines CI Templates', () => {
let wrapper;
@@ -28,8 +14,6 @@ describe('Pipelines CI Templates', () => {
return shallowMountExtended(PipelinesCiTemplates, {
provide: {
pipelineEditorPath,
- ciRunnerSettingsPath,
- anyRunnersAvailable: true,
...propsData,
},
stubs,
@@ -38,24 +22,17 @@ describe('Pipelines CI Templates', () => {
const findTestTemplateLink = () => wrapper.findByTestId('test-template-link');
const findCiTemplates = () => wrapper.findComponent(CiTemplates);
- const findSettingsLink = () => wrapper.findByTestId('settings-link');
- const findDocumentationLink = () => wrapper.findByTestId('documentation-link');
- const findSettingsButton = () => wrapper.findByTestId('settings-button');
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
- describe('renders test template', () => {
+ describe('templates', () => {
beforeEach(() => {
wrapper = createWrapper();
});
- it('links to the getting started template', () => {
+ it('renders test template and Ci templates', () => {
expect(findTestTemplateLink().attributes('href')).toBe(
pipelineEditorPath.concat('?template=Getting-Started'),
);
+ expect(findCiTemplates().exists()).toBe(true);
});
});
@@ -78,84 +55,4 @@ describe('Pipelines CI Templates', () => {
});
});
});
-
- describe('when the runners_availability_section experiment is active', () => {
- beforeEach(() => {
- stubExperiments({ runners_availability_section: 'candidate' });
- });
-
- describe('when runners are available', () => {
- beforeEach(() => {
- wrapper = createWrapper({ anyRunnersAvailable: true }, { GitlabExperiment, GlSprintf });
- });
-
- it('show the runners available section', () => {
- expect(wrapper.text()).toContain(I18N.runners.title);
- });
-
- it('tracks an event when clicking the settings link', () => {
- findSettingsLink().vm.$emit('click');
-
- expect(ExperimentTracking).toHaveBeenCalledWith(
- RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
- );
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- RUNNERS_SETTINGS_LINK_CLICKED_EVENT,
- );
- });
-
- it('tracks an event when clicking the documentation link', () => {
- findDocumentationLink().vm.$emit('click');
-
- expect(ExperimentTracking).toHaveBeenCalledWith(
- RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
- );
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
- );
- });
- });
-
- describe('when runners are not available', () => {
- beforeEach(() => {
- wrapper = createWrapper({ anyRunnersAvailable: false }, { GitlabExperiment, GlButton });
- });
-
- it('show the no runners available section', () => {
- expect(wrapper.text()).toContain(I18N.noRunners.title);
- });
-
- it('tracks an event when clicking the settings button', () => {
- findSettingsButton().trigger('click');
-
- expect(ExperimentTracking).toHaveBeenCalledWith(
- RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
- );
- expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
- RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
- );
- });
- });
- });
-
- describe.each`
- experimentVariant | anyRunnersAvailable | templatesRendered
- ${'control'} | ${true} | ${true}
- ${'control'} | ${false} | ${true}
- ${'candidate'} | ${true} | ${true}
- ${'candidate'} | ${false} | ${false}
- `(
- 'when the runners_availability_section experiment variant is $experimentVariant and runners are available: $anyRunnersAvailable',
- ({ experimentVariant, anyRunnersAvailable, templatesRendered }) => {
- beforeEach(() => {
- stubExperiments({ runners_availability_section: experimentVariant });
- wrapper = createWrapper({ anyRunnersAvailable });
- });
-
- it(`renders the templates: ${templatesRendered}`, () => {
- expect(findTestTemplateLink().exists()).toBe(templatesRendered);
- expect(findCiTemplates().exists()).toBe(templatesRendered);
- });
- },
- );
});
diff --git a/spec/frontend/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index 0abf7f59717..5465e4d77da 100644
--- a/spec/frontend/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
@@ -35,11 +35,6 @@ describe('Pipelines Empty State', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when user can configure CI', () => {
describe('when the ios_specific_templates experiment is active', () => {
beforeEach(() => {
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index e3eea503b46..890255f225e 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -33,7 +33,6 @@ describe('pipeline graph action component', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('render', () => {
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 99bccd21656..cc952eac1d7 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -1,10 +1,10 @@
-import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubPerformanceWebAPI } from 'helpers/performance';
import waitForPromises from 'helpers/wait_for_promises';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
@@ -48,23 +48,25 @@ describe('Pipeline graph wrapper', () => {
useLocalStorageSpy();
let wrapper;
- const getAlert = () => wrapper.findComponent(GlAlert);
- const getDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]');
- const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const getLinksLayer = () => wrapper.findComponent(LinksLayer);
- const getGraph = () => wrapper.findComponent(PipelineGraph);
- const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]');
- const getAllStageColumnGroupsInColumn = () =>
+ let requestHandlers;
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findDependenciesToggle = () => wrapper.findByTestId('show-links-toggle');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findLinksLayer = () => wrapper.findComponent(LinksLayer);
+ const findGraph = () => wrapper.findComponent(PipelineGraph);
+ const findStageColumnTitle = () => wrapper.findByTestId('stage-column-title');
+ const findAllStageColumnGroupsInColumn = () =>
wrapper.findComponent(StageColumnComponent).findAll('[data-testid="stage-column-group"]');
- const getViewSelector = () => wrapper.findComponent(GraphViewSelector);
- const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert);
+ const findViewSelector = () => wrapper.findComponent(GraphViewSelector);
+ const findViewSelectorToggle = () => findViewSelector().findComponent(GlToggle);
+ const findViewSelectorTrip = () => findViewSelector().findComponent(GlAlert);
const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const createComponent = ({
apolloProvider,
data = {},
provide = {},
- mountFn = shallowMount,
+ mountFn = shallowMountExtended,
} = {}) => {
wrapper = mountFn(PipelineGraphWrapper, {
provide: {
@@ -84,41 +86,41 @@ describe('Pipeline graph wrapper', () => {
calloutsList = [],
data = {},
getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
- mountFn = shallowMount,
+ mountFn = shallowMountExtended,
provide = {},
} = {}) => {
const callouts = mapCallouts(calloutsList);
- const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse(callouts));
- const getPipelineHeaderDataHandler = jest.fn().mockResolvedValue(mockRunningPipelineHeaderData);
- const requestHandlers = [
- [getPipelineHeaderData, getPipelineHeaderDataHandler],
- [getPipelineDetails, getPipelineDetailsHandler],
- [getUserCallouts, getUserCalloutsHandler],
+ requestHandlers = {
+ getUserCalloutsHandler: jest.fn().mockResolvedValue(mockCalloutsResponse(callouts)),
+ getPipelineHeaderDataHandler: jest.fn().mockResolvedValue(mockRunningPipelineHeaderData),
+ getPipelineDetailsHandler,
+ };
+
+ const handlers = [
+ [getPipelineHeaderData, requestHandlers.getPipelineHeaderDataHandler],
+ [getPipelineDetails, requestHandlers.getPipelineDetailsHandler],
+ [getUserCallouts, requestHandlers.getUserCalloutsHandler],
];
- const apolloProvider = createMockApollo(requestHandlers);
+ const apolloProvider = createMockApollo(handlers);
createComponent({ apolloProvider, data, provide, mountFn });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when data is loading', () => {
it('displays the loading icon', () => {
createComponentWithApollo();
- expect(getLoadingIcon().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
it('does not display the alert', () => {
createComponentWithApollo();
- expect(getAlert().exists()).toBe(false);
+ expect(findAlert().exists()).toBe(false);
});
it('does not display the graph', () => {
createComponentWithApollo();
- expect(getGraph().exists()).toBe(false);
+ expect(findGraph().exists()).toBe(false);
});
it('skips querying headerPipeline', () => {
@@ -134,19 +136,19 @@ describe('Pipeline graph wrapper', () => {
});
it('does not display the loading icon', () => {
- expect(getLoadingIcon().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('does not display the alert', () => {
- expect(getAlert().exists()).toBe(false);
+ expect(findAlert().exists()).toBe(false);
});
it('displays the graph', () => {
- expect(getGraph().exists()).toBe(true);
+ expect(findGraph().exists()).toBe(true);
});
it('passes the etag resource and metrics path to the graph', () => {
- expect(getGraph().props('configPaths')).toMatchObject({
+ expect(findGraph().props('configPaths')).toMatchObject({
graphqlResourceEtag: defaultProvide.graphqlResourceEtag,
metricsPath: defaultProvide.metricsPath,
});
@@ -162,15 +164,15 @@ describe('Pipeline graph wrapper', () => {
});
it('does not display the loading icon', () => {
- expect(getLoadingIcon().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('displays the alert', () => {
- expect(getAlert().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(true);
});
it('does not display the graph', () => {
- expect(getGraph().exists()).toBe(false);
+ expect(findGraph().exists()).toBe(false);
});
});
@@ -185,18 +187,18 @@ describe('Pipeline graph wrapper', () => {
});
it('does not display the loading icon', () => {
- expect(getLoadingIcon().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('displays the no iid alert', () => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(
'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
);
});
it('does not display the graph', () => {
- expect(getGraph().exists()).toBe(false);
+ expect(findGraph().exists()).toBe(false);
});
});
@@ -207,11 +209,11 @@ describe('Pipeline graph wrapper', () => {
});
describe('when receiving `setSkipRetryModal` event', () => {
it('passes down `skipRetryModal` value as true', async () => {
- expect(getGraph().props('skipRetryModal')).toBe(false);
+ expect(findGraph().props('skipRetryModal')).toBe(false);
- await getGraph().vm.$emit('setSkipRetryModal');
+ await findGraph().vm.$emit('setSkipRetryModal');
- expect(getGraph().props('skipRetryModal')).toBe(true);
+ expect(findGraph().props('skipRetryModal')).toBe(true);
});
});
});
@@ -220,36 +222,37 @@ describe('Pipeline graph wrapper', () => {
beforeEach(async () => {
createComponentWithApollo();
await waitForPromises();
- await getGraph().vm.$emit('error', { type: ACTION_FAILURE });
+ await findGraph().vm.$emit('error', { type: ACTION_FAILURE });
});
it('does not display the loading icon', () => {
- expect(getLoadingIcon().exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('displays the action error alert', () => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe('An error occurred while performing this action.');
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe('An error occurred while performing this action.');
});
it('displays the graph', () => {
- expect(getGraph().exists()).toBe(true);
+ expect(findGraph().exists()).toBe(true);
});
});
describe('when refresh action is emitted', () => {
beforeEach(async () => {
createComponentWithApollo();
- jest.spyOn(wrapper.vm.$apollo.queries.headerPipeline, 'refetch');
- jest.spyOn(wrapper.vm.$apollo.queries.pipeline, 'refetch');
await waitForPromises();
- getGraph().vm.$emit('refreshPipelineGraph');
+ findGraph().vm.$emit('refreshPipelineGraph');
});
it('calls refetch', () => {
- expect(wrapper.vm.$apollo.queries.headerPipeline.skip).toBe(false);
- expect(wrapper.vm.$apollo.queries.headerPipeline.refetch).toHaveBeenCalled();
- expect(wrapper.vm.$apollo.queries.pipeline.refetch).toHaveBeenCalled();
+ expect(requestHandlers.getPipelineHeaderDataHandler).toHaveBeenCalledWith({
+ fullPath: 'frog/amphibirama',
+ iid: '22',
+ });
+ expect(requestHandlers.getPipelineDetailsHandler).toHaveBeenCalledTimes(2);
+ expect(requestHandlers.getUserCalloutsHandler).toHaveBeenCalledWith({});
});
});
@@ -281,18 +284,18 @@ describe('Pipeline graph wrapper', () => {
it('shows correct errors and does not overwrite populated data when data is empty', async () => {
/* fails at first, shows error, no data yet */
- expect(getAlert().exists()).toBe(true);
- expect(getGraph().exists()).toBe(false);
+ expect(findAlert().exists()).toBe(true);
+ expect(findGraph().exists()).toBe(false);
/* succeeds, clears error, shows graph */
await advanceApolloTimers();
- expect(getAlert().exists()).toBe(false);
- expect(getGraph().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(false);
+ expect(findGraph().exists()).toBe(true);
/* fails again, alert returns but data persists */
await advanceApolloTimers();
- expect(getAlert().exists()).toBe(true);
- expect(getGraph().exists()).toBe(true);
+ expect(findAlert().exists()).toBe(true);
+ expect(findGraph().exists()).toBe(true);
});
});
@@ -302,38 +305,38 @@ describe('Pipeline graph wrapper', () => {
beforeEach(async () => {
layersFn = jest.spyOn(parsingUtils, 'listByLayers');
createComponentWithApollo({
- mountFn: mount,
+ mountFn: mountExtended,
});
await waitForPromises();
});
it('appears when pipeline uses needs', () => {
- expect(getViewSelector().exists()).toBe(true);
+ expect(findViewSelector().exists()).toBe(true);
});
it('switches between views', async () => {
const groupsInFirstColumn =
mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes.length;
- expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn);
- expect(getStageColumnTitle().text()).toBe('build');
- await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
- expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn + 1);
- expect(getStageColumnTitle().text()).toBe('');
+ expect(findAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn);
+ expect(findStageColumnTitle().text()).toBe('build');
+ await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ expect(findAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn + 1);
+ expect(findStageColumnTitle().text()).toBe('');
});
it('saves the view type to local storage', async () => {
- await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(localStorage.setItem.mock.calls).toEqual([[VIEW_TYPE_KEY, LAYER_VIEW]]);
});
it('calls listByLayers only once no matter how many times view is switched', async () => {
expect(layersFn).not.toHaveBeenCalled();
- await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
expect(layersFn).toHaveBeenCalledTimes(1);
- await getViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
- await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
- await getViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', LAYER_VIEW);
+ await findViewSelector().vm.$emit('updateViewType', STAGE_VIEW);
expect(layersFn).toHaveBeenCalledTimes(1);
});
});
@@ -344,7 +347,7 @@ describe('Pipeline graph wrapper', () => {
data: {
currentViewType: LAYER_VIEW,
},
- mountFn: mount,
+ mountFn: mountExtended,
});
jest.runOnlyPendingTimers();
@@ -353,10 +356,10 @@ describe('Pipeline graph wrapper', () => {
it('sets showLinks to true', async () => {
/* This spec uses .props for performance reasons. */
- expect(getLinksLayer().exists()).toBe(true);
- expect(getLinksLayer().props('showLinks')).toBe(false);
- expect(getViewSelector().props('type')).toBe(LAYER_VIEW);
- await getDependenciesToggle().vm.$emit('change', true);
+ expect(findLinksLayer().exists()).toBe(true);
+ expect(findLinksLayer().props('showLinks')).toBe(false);
+ expect(findViewSelector().props('type')).toBe(LAYER_VIEW);
+ await findDependenciesToggle().vm.$emit('change', true);
jest.runOnlyPendingTimers();
await waitForPromises();
@@ -371,15 +374,15 @@ describe('Pipeline graph wrapper', () => {
currentViewType: LAYER_VIEW,
showLinks: true,
},
- mountFn: mount,
+ mountFn: mountExtended,
});
await waitForPromises();
});
it('shows the hover tip in the view selector', async () => {
- await getViewSelector().setData({ showLinksActive: true });
- expect(getViewSelectorTrip().exists()).toBe(true);
+ await findViewSelectorToggle().vm.$emit('change', true);
+ expect(findViewSelectorTrip().exists()).toBe(true);
});
});
@@ -390,7 +393,7 @@ describe('Pipeline graph wrapper', () => {
currentViewType: LAYER_VIEW,
showLinks: true,
},
- mountFn: mount,
+ mountFn: mountExtended,
calloutsList: ['pipeline_needs_hover_tip'.toUpperCase()],
});
@@ -399,8 +402,8 @@ describe('Pipeline graph wrapper', () => {
});
it('does not show the hover tip', async () => {
- await getViewSelector().setData({ showLinksActive: true });
- expect(getViewSelectorTrip().exists()).toBe(false);
+ await findViewSelectorToggle().vm.$emit('change', true);
+ expect(findViewSelectorTrip().exists()).toBe(false);
});
});
@@ -409,7 +412,7 @@ describe('Pipeline graph wrapper', () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
createComponentWithApollo({
- mountFn: mount,
+ mountFn: mountExtended,
});
await waitForPromises();
@@ -440,7 +443,7 @@ describe('Pipeline graph wrapper', () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
createComponentWithApollo({
- mountFn: mount,
+ mountFn: mountExtended,
getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
@@ -452,7 +455,7 @@ describe('Pipeline graph wrapper', () => {
});
it('still passes stage type to graph', () => {
- expect(getGraph().props('viewType')).toBe(STAGE_VIEW);
+ expect(findGraph().props('viewType')).toBe(STAGE_VIEW);
});
});
@@ -462,7 +465,7 @@ describe('Pipeline graph wrapper', () => {
nonNeedsResponse.data.project.pipeline.usesNeeds = false;
createComponentWithApollo({
- mountFn: mount,
+ mountFn: mountExtended,
getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
@@ -471,7 +474,7 @@ describe('Pipeline graph wrapper', () => {
});
it('does not appear when pipeline does not use needs', () => {
- expect(getViewSelector().exists()).toBe(false);
+ expect(findViewSelector().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
index 43587bebedf..65ae9d19978 100644
--- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js
+++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js
@@ -42,10 +42,6 @@ describe('the graph view selector component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when showing stage view', () => {
beforeEach(() => {
createComponent({ mountFn: mount });
@@ -148,6 +144,7 @@ describe('the graph view selector component', () => {
createComponent({
props: {
showLinks: true,
+ type: LAYER_VIEW,
},
data: {
showLinksActive: true,
@@ -166,6 +163,18 @@ describe('the graph view selector component', () => {
await findHoverTip().find('button').trigger('click');
expect(wrapper.emitted().dismissHoverTip).toHaveLength(1);
});
+
+ it('is displayed at first then hidden on swith to STAGE_VIEW then displayed on switch to LAYER_VIEW', async () => {
+ expect(findHoverTip().exists()).toBe(true);
+ expect(findHoverTip().text()).toBe(wrapper.vm.$options.i18n.hoverTipText);
+
+ await findStageViewButton().trigger('click');
+ expect(findHoverTip().exists()).toBe(false);
+
+ await findLayerViewButton().trigger('click');
+ expect(findHoverTip().exists()).toBe(true);
+ expect(findHoverTip().text()).toBe(wrapper.vm.$options.i18n.hoverTipText);
+ });
});
describe('when links are live and it has been previously dismissed', () => {
@@ -174,6 +183,7 @@ describe('the graph view selector component', () => {
props: {
showLinks: true,
tipPreviouslyDismissed: true,
+ type: LAYER_VIEW,
},
data: {
showLinksActive: true,
@@ -191,6 +201,7 @@ describe('the graph view selector component', () => {
createComponent({
props: {
showLinks: true,
+ type: LAYER_VIEW,
},
data: {
showLinksActive: false,
diff --git a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js
index d8afb33e148..1419a7b9982 100644
--- a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js
+++ b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js
@@ -69,10 +69,6 @@ describe('job group dropdown component', () => {
wrapper = mountFn(JobGroupDropdown, { propsData: { group } });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
createComponent({ mountFn: mount });
});
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 3224c87ab6b..2a5dfd7e0ee 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -1,10 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { GlBadge, GlModal } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import { GlBadge, GlModal, GlToast } from '@gitlab/ui';
import JobItem from '~/pipelines/components/graph/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 { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
@@ -19,12 +20,14 @@ import {
describe('pipeline graph job item', () => {
useLocalStorageSpy();
+ Vue.use(GlToast);
let wrapper;
let mockAxios;
const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
const findJobWithLink = () => wrapper.findByTestId('job-with-link');
+ const findActionVueComponent = () => wrapper.findComponent(ActionComponent);
const findActionComponent = () => wrapper.findByTestId('ci-action-component');
const findBadge = () => wrapper.findComponent(GlBadge);
const findJobLink = () => wrapper.findByTestId('job-with-link');
@@ -41,9 +44,9 @@ describe('pipeline graph job item', () => {
job: mockJob,
};
- const createWrapper = ({ props, data } = {}) => {
+ const createWrapper = ({ props, data, mountFn = mount, mocks = {} } = {}) => {
wrapper = extendedWrapper(
- mount(JobItem, {
+ mountFn(JobItem, {
data() {
return {
...data,
@@ -53,6 +56,9 @@ describe('pipeline graph job item', () => {
...defaultProps,
...props,
},
+ mocks: {
+ ...mocks,
+ },
}),
);
};
@@ -115,7 +121,7 @@ describe('pipeline graph job item', () => {
expect(actionComponent.exists()).toBe(true);
expect(actionComponent.props('actionIcon')).toBe('retry');
- expect(actionComponent.attributes('disabled')).not.toBe('disabled');
+ expect(actionComponent.attributes('disabled')).toBeUndefined();
});
it('should render disabled action icon when user cannot run the action', () => {
@@ -129,7 +135,7 @@ describe('pipeline graph job item', () => {
expect(actionComponent.exists()).toBe(true);
expect(actionComponent.props('actionIcon')).toBe('stop');
- expect(actionComponent.attributes('disabled')).toBe('disabled');
+ expect(actionComponent.attributes('disabled')).toBeDefined();
});
it('action icon tooltip text when job has passed but can be ran again', () => {
@@ -238,6 +244,37 @@ describe('pipeline graph job item', () => {
});
});
+ describe('when retrying', () => {
+ const mockToastShow = jest.fn();
+
+ beforeEach(async () => {
+ createWrapper({
+ mountFn: shallowMount,
+ data: {
+ currentSkipModalValue: true,
+ },
+ props: {
+ skipRetryModal: true,
+ job: triggerJobWithRetryAction,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ });
+
+ jest.spyOn(wrapper.vm.$toast, 'show');
+
+ await findActionVueComponent().vm.$emit('pipelineActionRequestComplete');
+ await nextTick();
+ });
+
+ it('shows a toast message that the downstream is being created', () => {
+ expect(mockToastShow).toHaveBeenCalledTimes(1);
+ });
+ });
+
describe('highlighting', () => {
it.each`
job | jobName | expanded | link
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index f396fe2aff4..bf92cd585d9 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,11 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlButton, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { createWrapper } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
+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';
@@ -14,10 +13,9 @@ import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import mockPipeline from './linked_pipelines_mock_data';
-Vue.use(VueApollo);
-
describe('Linked pipeline', () => {
let wrapper;
+ let requestHandlers;
const downstreamProps = {
pipeline: {
@@ -47,20 +45,29 @@ describe('Linked pipeline', () => {
const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
- const createWrapper = ({ propsData }) => {
- const mockApollo = createMockApollo();
+ const defaultHandlers = {
+ cancelPipeline: jest.fn().mockResolvedValue({ data: { pipelineCancel: { errors: [] } } }),
+ retryPipeline: jest.fn().mockResolvedValue({ data: { pipelineRetry: { errors: [] } } }),
+ };
- wrapper = extendedWrapper(
- mount(LinkedPipelineComponent, {
- propsData,
- apolloProvider: mockApollo,
- }),
- );
+ const createMockApolloProvider = (handlers) => {
+ Vue.use(VueApollo);
+
+ requestHandlers = handlers;
+ return createMockApollo([
+ [CancelPipelineMutation, requestHandlers.cancelPipeline],
+ [RetryPipelineMutation, requestHandlers.retryPipeline],
+ ]);
};
- afterEach(() => {
- wrapper.destroy();
- });
+ const createComponent = ({ propsData, handlers = defaultHandlers }) => {
+ const mockApollo = createMockApolloProvider(handlers);
+
+ wrapper = mountExtended(LinkedPipelineComponent, {
+ propsData,
+ apolloProvider: mockApollo,
+ });
+ };
describe('rendered output', () => {
const props = {
@@ -72,7 +79,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper({ propsData: props });
+ createComponent({ propsData: props });
});
it('should render the project name', () => {
@@ -113,7 +120,7 @@ describe('Linked pipeline', () => {
describe('upstream pipelines', () => {
beforeEach(() => {
- createWrapper({ propsData: upstreamProps });
+ createComponent({ propsData: upstreamProps });
});
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
@@ -133,7 +140,7 @@ describe('Linked pipeline', () => {
describe('downstream pipelines', () => {
describe('styling', () => {
beforeEach(() => {
- createWrapper({ propsData: downstreamProps });
+ createComponent({ propsData: downstreamProps });
});
it('parent/child label container should exist', () => {
@@ -168,7 +175,7 @@ describe('Linked pipeline', () => {
pipeline: { ...mockPipeline, retryable: true },
};
- createWrapper({ propsData: retryablePipeline });
+ createComponent({ propsData: retryablePipeline });
});
it('does not show the retry or cancel button', () => {
@@ -179,14 +186,14 @@ describe('Linked pipeline', () => {
});
describe('on a downstream', () => {
+ const retryablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, retryable: true },
+ };
+
describe('when retryable', () => {
beforeEach(() => {
- const retryablePipeline = {
- ...downstreamProps,
- pipeline: { ...mockPipeline, retryable: true },
- };
-
- createWrapper({ propsData: retryablePipeline });
+ createComponent({ propsData: retryablePipeline });
});
it('shows only the retry button', () => {
@@ -209,50 +216,51 @@ describe('Linked pipeline', () => {
describe('and the retry button is clicked', () => {
describe('on success', () => {
beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- jest.spyOn(wrapper.vm, '$emit');
await findRetryButton().trigger('click');
});
it('calls the retry mutation', () => {
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: RetryPipelineMutation,
- variables: {
- id: convertToGraphQLId(TYPENAME_CI_PIPELINE, mockPipeline.id),
- },
+ expect(requestHandlers.retryPipeline).toHaveBeenCalledTimes(1);
+ expect(requestHandlers.retryPipeline).toHaveBeenCalledWith({
+ id: 'gid://gitlab/Ci::Pipeline/195',
});
});
- it('emits the refreshPipelineGraph event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ it('emits the refreshPipelineGraph event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('refreshPipelineGraph')).toHaveLength(1);
});
});
describe('on failure', () => {
beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
- jest.spyOn(wrapper.vm, '$emit');
+ createComponent({
+ propsData: retryablePipeline,
+ handlers: {
+ retryPipeline: jest.fn().mockRejectedValue({ errors: [] }),
+ cancelPipeline: jest.fn().mockRejectedValue({ errors: [] }),
+ },
+ });
+
await findRetryButton().trigger('click');
});
- it('emits an error event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
- type: ACTION_FAILURE,
- });
+ it('emits an error event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('error')).toEqual([[{ type: ACTION_FAILURE }]]);
});
});
});
});
describe('when cancelable', () => {
- beforeEach(() => {
- const cancelablePipeline = {
- ...downstreamProps,
- pipeline: { ...mockPipeline, cancelable: true },
- };
+ const cancelablePipeline = {
+ ...downstreamProps,
+ pipeline: { ...mockPipeline, cancelable: true },
+ };
- createWrapper({ propsData: cancelablePipeline });
+ beforeEach(() => {
+ createComponent({ propsData: cancelablePipeline });
});
it('shows only the cancel button', () => {
@@ -275,34 +283,37 @@ describe('Linked pipeline', () => {
describe('and the cancel button is clicked', () => {
describe('on success', () => {
beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
- jest.spyOn(wrapper.vm, '$emit');
await findCancelButton().trigger('click');
});
it('calls the cancel mutation', () => {
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: CancelPipelineMutation,
- variables: {
- id: convertToGraphQLId(TYPENAME_CI_PIPELINE, mockPipeline.id),
- },
+ expect(requestHandlers.cancelPipeline).toHaveBeenCalledTimes(1);
+ expect(requestHandlers.cancelPipeline).toHaveBeenCalledWith({
+ id: 'gid://gitlab/Ci::Pipeline/195',
});
});
- it('emits the refreshPipelineGraph event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('refreshPipelineGraph');
+ it('emits the refreshPipelineGraph event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('refreshPipelineGraph')).toHaveLength(1);
});
});
+
describe('on failure', () => {
beforeEach(async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({ errors: [] });
- jest.spyOn(wrapper.vm, '$emit');
+ createComponent({
+ propsData: cancelablePipeline,
+ handlers: {
+ retryPipeline: jest.fn().mockRejectedValue({ errors: [] }),
+ cancelPipeline: jest.fn().mockRejectedValue({ errors: [] }),
+ },
+ });
+
await findCancelButton().trigger('click');
});
- it('emits an error event', () => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('error', {
- type: ACTION_FAILURE,
- });
+
+ it('emits an error event', async () => {
+ await waitForPromises();
+ expect(wrapper.emitted('error')).toEqual([[{ type: ACTION_FAILURE }]]);
});
});
});
@@ -315,7 +326,7 @@ describe('Linked pipeline', () => {
pipeline: { ...mockPipeline, cancelable: true, retryable: true },
};
- createWrapper({ propsData: pipelineWithTwoActions });
+ createComponent({ propsData: pipelineWithTwoActions });
});
it('only shows the cancel button', () => {
@@ -338,7 +349,7 @@ describe('Linked pipeline', () => {
},
};
- createWrapper({ propsData: pipelineWithTwoActions });
+ createComponent({ propsData: pipelineWithTwoActions });
});
it('does not show any action button', () => {
@@ -359,7 +370,7 @@ describe('Linked pipeline', () => {
`(
'$pipelineType.columnTitle pipeline button icon should be $chevronPosition with $buttonBorderClasses if expanded state is $expanded',
({ pipelineType, chevronPosition, buttonBorderClasses, expanded }) => {
- createWrapper({ propsData: { ...pipelineType, expanded } });
+ createComponent({ propsData: { ...pipelineType, expanded } });
expect(findExpandButton().props('icon')).toBe(chevronPosition);
expect(findExpandButton().classes()).toContain(buttonBorderClasses);
},
@@ -367,7 +378,7 @@ describe('Linked pipeline', () => {
describe('shadow border', () => {
beforeEach(() => {
- createWrapper({ propsData: downstreamProps });
+ createComponent({ propsData: downstreamProps });
});
it.each`
@@ -401,7 +412,7 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper({ propsData: props });
+ createComponent({ propsData: props });
});
it('loading icon is visible', () => {
@@ -419,36 +430,35 @@ describe('Linked pipeline', () => {
};
beforeEach(() => {
- createWrapper({ propsData: props });
+ createComponent({ propsData: props });
});
it('emits `pipelineClicked` event', () => {
- jest.spyOn(wrapper.vm, '$emit');
findButton().trigger('click');
- expect(wrapper.emitted().pipelineClicked).toHaveLength(1);
+ expect(wrapper.emitted('pipelineClicked')).toHaveLength(1);
});
- it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
- jest.spyOn(wrapper.vm.$root, '$emit');
- findButton().trigger('click');
+ it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, async () => {
+ const root = createWrapper(wrapper.vm.$root);
+ await findButton().vm.$emit('click');
- expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([BV_HIDE_TOOLTIP]);
+ expect(root.emitted(BV_HIDE_TOOLTIP)).toHaveLength(1);
});
it('should emit downstreamHovered with job name on mouseover', () => {
findLinkedPipeline().trigger('mouseover');
- expect(wrapper.emitted().downstreamHovered).toStrictEqual([['test_c']]);
+ expect(wrapper.emitted('downstreamHovered')).toStrictEqual([['test_c']]);
});
it('should emit downstreamHovered with empty string on mouseleave', () => {
findLinkedPipeline().trigger('mouseleave');
- expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]);
+ expect(wrapper.emitted('downstreamHovered')).toStrictEqual([['']]);
});
it('should emit pipelineExpanded with job name and expanded state on click', () => {
findExpandButton().trigger('click');
- expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['test_c', true]]);
+ expect(wrapper.emitted('pipelineExpandToggle')).toStrictEqual([['test_c', true]]);
});
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
index 63e2d8707ea..6e4b9498918 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js
@@ -65,10 +65,6 @@ describe('Linked Pipelines Column', () => {
createComponent({ apolloProvider, mountFn, props });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('it renders correctly', () => {
beforeEach(() => {
createComponentWithApollo();
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index 19f597a7267..d4d7f1618c5 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -54,10 +54,6 @@ describe('stage column component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when mounted', () => {
beforeEach(() => {
createComponent({ method: mount });
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index 2c6d126e12c..50f754393fe 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -81,7 +81,6 @@ describe('Links Inner component', () => {
afterEach(() => {
jest.restoreAllMocks();
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index e2699d6ff2e..9d39c86ed5e 100644
--- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
@@ -35,10 +35,6 @@ describe('links layer component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with show links off', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index e583c0798f5..18def4ab62c 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -6,7 +6,7 @@ import HeaderComponent from '~/pipelines/components/header_component.vue';
import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import { BUTTON_TOOLTIP_RETRY } from '~/pipelines/constants';
+import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants';
import {
mockCancelledPipelineHeader,
mockFailedPipelineHeader,
@@ -71,11 +71,6 @@ describe('Pipeline details header', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('initial loading', () => {
beforeEach(() => {
wrapper = createComponent(null, { isLoading: true });
@@ -174,6 +169,10 @@ describe('Pipeline details header', () => {
});
});
+ it('should render cancel action tooltip', () => {
+ expect(findCancelButton().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL);
+ });
+
it('should display error message on failure', async () => {
const failureMessage = 'failure message';
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index dd7e81f3f22..a4b8d223a0c 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -1190,6 +1190,10 @@ export const mockFailedJobsQueryResponse = {
readBuild: true,
updateBuild: true,
},
+ trace: {
+ htmlSummary: '<span>Html Summary</span>',
+ },
+ failureMessage: 'Failed',
},
{
__typename: 'CiJob',
@@ -1218,6 +1222,8 @@ export const mockFailedJobsQueryResponse = {
readBuild: true,
updateBuild: true,
},
+ trace: null,
+ failureMessage: 'Failed',
},
],
},
@@ -1226,18 +1232,8 @@ export const mockFailedJobsQueryResponse = {
},
};
-export const mockFailedJobsSummaryData = [
- {
- id: 1848,
- failure: null,
- failure_summary:
- '<span>Pulling docker image node:latest ...<br/></span><span>Using docker image sha256:738d733448be00c72cb6618b7a06a1424806c6d239d8885e92f9b1e8727092b5 for node:latest with digest node@sha256:e5b7b349d517159246070bf14242027a9e220ffa8bd98a67ba1495d969c06c01 ...<br/></span><div class="section-start" data-timestamp="1651175313" data-section="prepare-script" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-prepare-script">Preparing environment</span><span class="section section-header js-s-prepare-script"><br/></span><span class="section line js-s-prepare-script">Running on runner-kvkqh24-project-20-concurrent-0 via 0706719b1b8d...<br/></span><div class="section-end" data-section="prepare-script"></div><div class="section-start" data-timestamp="1651175313" data-section="get-sources" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-get-sources">Getting source from Git repository</span><span class="section section-header js-s-get-sources"><br/></span><span class="term-fg-l-green term-bold section line js-s-get-sources">Fetching changes with git depth set to 50...</span><span class="section line js-s-get-sources"><br/>Reinitialized existing Git repository in /builds/root/ci-project/.git/<br/>fatal: couldn\'t find remote ref refs/heads/test<br/></span><div class="section-end" data-section="get-sources"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit code 1<br/></span>',
- },
-];
-
export const mockFailedJobsData = [
{
- normalizedId: 1848,
__typename: 'CiJob',
status: 'FAILED',
detailedStatus: {
@@ -1260,13 +1256,25 @@ export const mockFailedJobsData = [
},
},
id: 'gid://gitlab/Ci::Build/1848',
- stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/358',
+ name: 'build',
+ },
name: 'wait_job',
retryable: true,
- userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
+ userPermissions: {
+ __typename: 'JobPermissions',
+ readBuild: true,
+ updateBuild: true,
+ },
+ trace: {
+ htmlSummary: '<span>Html Summary</span>',
+ },
+ failureMessage: 'Job failed',
+ _showDetails: true,
},
{
- normalizedId: 1710,
__typename: 'CiJob',
status: 'FAILED',
detailedStatus: {
@@ -1281,52 +1289,27 @@ export const mockFailedJobsData = [
action: null,
},
id: 'gid://gitlab/Ci::Build/1710',
- stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
+ stage: {
+ __typename: 'CiStage',
+ id: 'gid://gitlab/Ci::Stage/358',
+ name: 'build',
+ },
name: 'wait_job',
retryable: false,
- userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
- },
-];
-
-export const mockPreparedFailedJobsData = [
- {
- __typename: 'CiJob',
- _showDetails: true,
- detailedStatus: {
- __typename: 'DetailedStatus',
- action: {
- __typename: 'StatusAction',
- buttonTitle: 'Retry this job',
- icon: 'retry',
- id: 'Ci::Build-failed-1848',
- method: 'post',
- path: '/root/ci-project/-/jobs/1848/retry',
- title: 'Retry',
- },
- detailsPath: '/root/ci-project/-/jobs/1848',
- group: 'failed',
- icon: 'status_failed',
- id: 'failed-1848-1848',
- label: 'failed',
- text: 'failed',
- tooltip: 'failed - (script failure)',
+ userPermissions: {
+ __typename: 'JobPermissions',
+ readBuild: true,
+ updateBuild: true,
},
- failure: null,
- failureSummary:
- '<span>Pulling docker image node:latest ...<br/></span><span>Using docker image sha256:738d733448be00c72cb6618b7a06a1424806c6d239d8885e92f9b1e8727092b5 for node:latest with digest node@sha256:e5b7b349d517159246070bf14242027a9e220ffa8bd98a67ba1495d969c06c01 ...<br/></span><div class="section-start" data-timestamp="1651175313" data-section="prepare-script" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-prepare-script">Preparing environment</span><span class="section section-header js-s-prepare-script"><br/></span><span class="section line js-s-prepare-script">Running on runner-kvkqh24-project-20-concurrent-0 via 0706719b1b8d...<br/></span><div class="section-end" data-section="prepare-script"></div><div class="section-start" data-timestamp="1651175313" data-section="get-sources" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-get-sources">Getting source from Git repository</span><span class="section section-header js-s-get-sources"><br/></span><span class="term-fg-l-green term-bold section line js-s-get-sources">Fetching changes with git depth set to 50...</span><span class="section line js-s-get-sources"><br/>Reinitialized existing Git repository in /builds/root/ci-project/.git/<br/>fatal: couldn\'t find remote ref refs/heads/test<br/></span><div class="section-end" data-section="get-sources"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit code 1<br/></span>',
- id: 'gid://gitlab/Ci::Build/1848',
- name: 'wait_job',
- normalizedId: 1848,
- retryable: true,
- stage: { __typename: 'CiStage', id: 'gid://gitlab/Ci::Stage/358', name: 'build' },
- status: 'FAILED',
- userPermissions: { __typename: 'JobPermissions', readBuild: true, updateBuild: true },
+ trace: null,
+ failureMessage: 'Job failed',
+ _showDetails: true,
},
];
-export const mockPreparedFailedJobsDataNoPermission = [
+export const mockFailedJobsDataNoPermission = [
{
- ...mockPreparedFailedJobsData[0],
+ ...mockFailedJobsData[0],
userPermissions: { __typename: 'JobPermissions', readBuild: false, updateBuild: false },
},
];
diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js
index 2c4740df174..15de7dc51f1 100644
--- a/spec/frontend/pipelines/nav_controls_spec.js
+++ b/spec/frontend/pipelines/nav_controls_spec.js
@@ -1,23 +1,20 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NavControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
describe('Pipelines Nav Controls', () => {
let wrapper;
const createComponent = (props) => {
- wrapper = shallowMount(NavControls, {
+ wrapper = shallowMountExtended(NavControls, {
propsData: {
...props,
},
});
};
- const findRunPipeline = () => wrapper.find('.js-run-pipeline');
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
+ const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
+ const findClearCacheButton = () => wrapper.findByTestId('clear-cache-button');
it('should render link to create a new pipeline', () => {
const mockData = {
@@ -28,9 +25,9 @@ describe('Pipelines Nav Controls', () => {
createComponent(mockData);
- const runPipeline = findRunPipeline();
- expect(runPipeline.text()).toContain('Run pipeline');
- expect(runPipeline.attributes('href')).toBe(mockData.newPipelinePath);
+ const runPipelineButton = findRunPipelineButton();
+ expect(runPipelineButton.text()).toContain('Run pipeline');
+ expect(runPipelineButton.attributes('href')).toBe(mockData.newPipelinePath);
});
it('should not render link to create pipeline if no path is provided', () => {
@@ -42,7 +39,7 @@ describe('Pipelines Nav Controls', () => {
createComponent(mockData);
- expect(findRunPipeline().exists()).toBe(false);
+ expect(findRunPipelineButton().exists()).toBe(false);
});
it('should render link for CI lint', () => {
@@ -54,9 +51,10 @@ describe('Pipelines Nav Controls', () => {
};
createComponent(mockData);
+ const ciLintButton = findCiLintButton();
- expect(wrapper.find('.js-ci-lint').text().trim()).toContain('CI lint');
- expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(mockData.ciLintPath);
+ expect(ciLintButton.text()).toContain('CI lint');
+ expect(ciLintButton.attributes('href')).toBe(mockData.ciLintPath);
});
describe('Reset Runners Cache', () => {
@@ -70,16 +68,13 @@ describe('Pipelines Nav Controls', () => {
});
it('should render button for resetting runner caches', () => {
- expect(wrapper.find('.js-clear-cache').text().trim()).toContain('Clear runner caches');
+ expect(findClearCacheButton().text()).toContain('Clear runner caches');
});
- it('should emit postAction event when reset runner cache button is clicked', async () => {
- jest.spyOn(wrapper.vm, '$emit').mockImplementation(() => {});
-
- wrapper.find('.js-clear-cache').vm.$emit('click');
- await nextTick();
+ it('should emit postAction event when reset runner cache button is clicked', () => {
+ findClearCacheButton().vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo');
+ expect(wrapper.emitted('resetRunnersCache')).toEqual([['foo']]);
});
});
});
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index df10742fd93..123f2e011c3 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -39,10 +39,6 @@ describe('pipeline graph component', () => {
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with `VALID` status', () => {
beforeEach(() => {
wrapper = createComponent({
diff --git a/spec/frontend/pipelines/pipeline_labels_spec.js b/spec/frontend/pipelines/pipeline_labels_spec.js
index ca0229b1cbe..6a37e36352b 100644
--- a/spec/frontend/pipelines/pipeline_labels_spec.js
+++ b/spec/frontend/pipelines/pipeline_labels_spec.js
@@ -30,10 +30,6 @@ describe('Pipeline label component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should not render tags when flags are not set', () => {
createComponent();
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index bedde71c48d..e3c9983aa52 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -67,8 +67,6 @@ describe('Pipeline Multi Actions Dropdown', () => {
afterEach(() => {
mockAxios.restore();
-
- wrapper.destroy();
});
it('should render the dropdown', () => {
diff --git a/spec/frontend/pipelines/pipeline_operations_spec.js b/spec/frontend/pipelines/pipeline_operations_spec.js
new file mode 100644
index 00000000000..b2191453824
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_operations_spec.js
@@ -0,0 +1,77 @@
+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';
+
+describe('Pipeline operations', () => {
+ let wrapper;
+
+ const defaultProps = {
+ pipeline: {
+ id: 329,
+ iid: 234,
+ details: {
+ has_manual_actions: true,
+ has_scheduled_actions: false,
+ },
+ flags: {
+ retryable: true,
+ cancelable: true,
+ },
+ cancel_path: '/root/ci-project/-/pipelines/329/cancel',
+ retry_path: '/root/ci-project/-/pipelines/329/retry',
+ },
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMountExtended(PipelineOperations, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findManualActions = () => wrapper.findComponent(PipelinesManualActions);
+ const findMultiActions = () => wrapper.findComponent(PipelineMultiActions);
+ const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
+ const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
+
+ it('should display pipeline manual actions', () => {
+ createComponent();
+
+ expect(findManualActions().exists()).toBe(true);
+ });
+
+ it('should display pipeline multi actions', () => {
+ createComponent();
+
+ expect(findMultiActions().exists()).toBe(true);
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent();
+
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ });
+
+ it('should emit retryPipeline event', () => {
+ findRetryBtn().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'retryPipeline',
+ defaultProps.pipeline.retry_path,
+ );
+ });
+
+ it('should emit openConfirmationModal event', () => {
+ findCancelBtn().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('openConfirmationModal', {
+ pipeline: defaultProps.pipeline,
+ endpoint: defaultProps.pipeline.cancel_path,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipeline_tabs_spec.js b/spec/frontend/pipelines/pipeline_tabs_spec.js
index 099748a5cca..8d1cd98e981 100644
--- a/spec/frontend/pipelines/pipeline_tabs_spec.js
+++ b/spec/frontend/pipelines/pipeline_tabs_spec.js
@@ -25,7 +25,6 @@ describe('~/pipelines/pipeline_tabs.js', () => {
el.dataset.exposeSecurityDashboard = 'true';
el.dataset.exposeLicenseScanningData = 'true';
el.dataset.failedJobsCount = 1;
- el.dataset.failedJobsSummary = '[]';
el.dataset.graphqlResourceEtag = 'graphqlResourceEtag';
el.dataset.pipelineIid = '123';
el.dataset.pipelineProjectPath = 'pipelineProjectPath';
@@ -50,7 +49,6 @@ describe('~/pipelines/pipeline_tabs.js', () => {
exposeSecurityDashboard: true,
exposeLicenseScanningData: true,
failedJobsCount: '1',
- failedJobsSummary: [],
graphqlResourceEtag: 'graphqlResourceEtag',
pipelineIid: '123',
pipelineProjectPath: 'pipelineProjectPath',
diff --git a/spec/frontend/pipelines/pipeline_triggerer_spec.js b/spec/frontend/pipelines/pipeline_triggerer_spec.js
index 58bfb68e85c..856c0484075 100644
--- a/spec/frontend/pipelines/pipeline_triggerer_spec.js
+++ b/spec/frontend/pipelines/pipeline_triggerer_spec.js
@@ -22,15 +22,11 @@ describe('Pipelines Triggerer', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findAvatar = () => wrapper.findComponent(GlAvatar);
const findTriggerer = () => wrapper.findByText('API');
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index c62898f0c83..f00ee4a6367 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -35,10 +35,6 @@ describe('Pipeline Url Component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render pipeline url table cell', () => {
createComponent();
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
deleted file mode 100644
index e034d52a33c..00000000000
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { TEST_HOST } from 'spec/test_constants';
-import { createAlert } from '~/flash';
-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 GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import { TRACKING_CATEGORIES } from '~/pipelines/constants';
-
-jest.mock('~/flash');
-jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
-
-describe('Pipelines Actions dropdown', () => {
- let wrapper;
- let mock;
-
- const createComponent = (props, mountFn = shallowMount) => {
- wrapper = mountFn(PipelinesManualActions, {
- propsData: {
- ...props,
- },
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findAllCountdowns = () => wrapper.findAllComponents(GlCountdown);
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
- mock.restore();
- confirmAction.mockReset();
- });
-
- describe('manual actions', () => {
- const mockActions = [
- {
- name: 'stop_review',
- path: `${TEST_HOST}/root/review-app/builds/1893/play`,
- },
- {
- name: 'foo',
- path: `${TEST_HOST}/disabled/pipeline/action`,
- playable: false,
- },
- ];
-
- beforeEach(() => {
- createComponent({ actions: mockActions });
- });
-
- it('renders a dropdown with the provided actions', () => {
- expect(findAllDropdownItems()).toHaveLength(mockActions.length);
- });
-
- it("renders a disabled action when it's not playable", () => {
- expect(findAllDropdownItems().at(1).attributes('disabled')).toBe('true');
- });
-
- describe('on click', () => {
- it('makes a request and toggles the loading state', async () => {
- mock.onPost(mockActions.path).reply(HTTP_STATUS_OK);
-
- findAllDropdownItems().at(0).vm.$emit('click');
-
- await nextTick();
- expect(findDropdown().props('loading')).toBe(true);
-
- await waitForPromises();
- expect(findDropdown().props('loading')).toBe(false);
- });
-
- it('makes a failed request and toggles the loading state', async () => {
- mock.onPost(mockActions.path).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- findAllDropdownItems().at(0).vm.$emit('click');
-
- await nextTick();
- expect(findDropdown().props('loading')).toBe(true);
-
- await waitForPromises();
- expect(findDropdown().props('loading')).toBe(false);
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('tracking', () => {
- afterEach(() => {
- unmockTracking();
- });
-
- it('tracks manual actions click', () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- findDropdown().vm.$emit('shown');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_manual_actions', {
- label: TRACKING_CATEGORIES.table,
- });
- });
- });
- });
-
- describe('scheduled jobs', () => {
- const scheduledJobAction = {
- name: 'scheduled action',
- path: `${TEST_HOST}/scheduled/job/action`,
- playable: true,
- scheduled_at: '2063-04-05T00:42:00Z',
- };
- const expiredJobAction = {
- name: 'expired action',
- path: `${TEST_HOST}/expired/job/action`,
- playable: true,
- scheduled_at: '2018-10-05T08:23:00Z',
- };
-
- beforeEach(() => {
- jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
- createComponent({ actions: [scheduledJobAction, expiredJobAction] });
- });
-
- it('makes post request after confirming', async () => {
- mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
- confirmAction.mockResolvedValueOnce(true);
-
- findAllDropdownItems().at(0).vm.$emit('click');
-
- expect(confirmAction).toHaveBeenCalled();
-
- await waitForPromises();
-
- expect(mock.history.post).toHaveLength(1);
- });
-
- it('does not make post request if confirmation is cancelled', async () => {
- mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
- confirmAction.mockResolvedValueOnce(false);
-
- findAllDropdownItems().at(0).vm.$emit('click');
-
- expect(confirmAction).toHaveBeenCalled();
-
- await waitForPromises();
-
- expect(mock.history.post).toHaveLength(0);
- });
-
- it('displays the remaining time in the dropdown', () => {
- expect(findAllCountdowns().at(0).props('endDateString')).toBe(
- scheduledJobAction.scheduled_at,
- );
- });
-
- it('displays 00:00:00 for expired jobs in the dropdown', () => {
- expect(findAllCountdowns().at(1).props('endDateString')).toBe(expiredJobAction.scheduled_at);
- });
- });
-});
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index e3e54716a7b..9fedbaf9b56 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -34,11 +34,6 @@ describe('Pipelines Artifacts dropdown', () => {
const findAllGlDropdownItems = () =>
wrapper.findComponent(GlDropdown).findAllComponents(GlDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render a dropdown with all the provided artifacts', () => {
createComponent();
diff --git a/spec/frontend/pipelines/pipelines_manual_actions_spec.js b/spec/frontend/pipelines/pipelines_manual_actions_spec.js
new file mode 100644
index 00000000000..82cab88c9eb
--- /dev/null
+++ b/spec/frontend/pipelines/pipelines_manual_actions_spec.js
@@ -0,0 +1,216 @@
+import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import mockPipelineActionsQueryResponse from 'test_fixtures/graphql/pipelines/get_pipeline_actions.query.graphql.json';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/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 GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+
+describe('Pipeline manual actions', () => {
+ let wrapper;
+ let mock;
+
+ const queryHandler = jest.fn().mockResolvedValue(mockPipelineActionsQueryResponse);
+ const {
+ data: {
+ project: {
+ pipeline: {
+ jobs: { nodes },
+ },
+ },
+ },
+ } = mockPipelineActionsQueryResponse;
+
+ const mockPath = nodes[2].playPath;
+
+ const createComponent = (limit = 50) => {
+ wrapper = shallowMountExtended(PipelinesManualActions, {
+ provide: {
+ fullPath: 'root/ci-project',
+ manualActionsLimit: limit,
+ },
+ propsData: {
+ iid: 100,
+ },
+ stubs: {
+ GlDropdown,
+ },
+ apolloProvider: createMockApollo([[getPipelineActionsQuery, queryHandler]]),
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findAllCountdowns = () => wrapper.findAllComponents(GlCountdown);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findLimitMessage = () => wrapper.findByTestId('limit-reached-msg');
+
+ it('skips calling query on mount', () => {
+ createComponent();
+
+ expect(queryHandler).not.toHaveBeenCalled();
+ });
+
+ describe('loading', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findDropdown().vm.$emit('shown');
+ });
+
+ it('display loading state while actions are being fetched', () => {
+ expect(findAllDropdownItems().at(0).text()).toBe('Loading...');
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findAllDropdownItems()).toHaveLength(1);
+ });
+ });
+
+ describe('loaded', () => {
+ beforeEach(async () => {
+ mock = new MockAdapter(axios);
+
+ createComponent();
+
+ findDropdown().vm.$emit('shown');
+
+ await waitForPromises();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ confirmAction.mockReset();
+ });
+
+ it('displays dropdown with the provided actions', () => {
+ expect(findAllDropdownItems()).toHaveLength(3);
+ });
+
+ it("displays a disabled action when it's not playable", () => {
+ expect(findAllDropdownItems().at(0).attributes('disabled')).toBeDefined();
+ });
+
+ describe('on action click', () => {
+ it('makes a request and toggles the loading state', async () => {
+ mock.onPost(mockPath).reply(HTTP_STATUS_OK);
+
+ findAllDropdownItems().at(1).vm.$emit('click');
+
+ await nextTick();
+
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findDropdown().props('loading')).toBe(false);
+ });
+
+ it('makes a failed request and toggles the loading state', async () => {
+ mock.onPost(mockPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+
+ findAllDropdownItems().at(1).vm.$emit('click');
+
+ await nextTick();
+
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findDropdown().props('loading')).toBe(false);
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('tracking', () => {
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks manual actions click', () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findDropdown().vm.$emit('shown');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_manual_actions', {
+ label: TRACKING_CATEGORIES.table,
+ });
+ });
+ });
+
+ describe('scheduled jobs', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(Date, 'now')
+ .mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+ });
+
+ it('makes post request after confirming', async () => {
+ mock.onPost(mockPath).reply(HTTP_STATUS_OK);
+
+ confirmAction.mockResolvedValueOnce(true);
+
+ findAllDropdownItems().at(2).vm.$emit('click');
+
+ expect(confirmAction).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(1);
+ });
+
+ it('does not make post request if confirmation is cancelled', async () => {
+ mock.onPost(mockPath).reply(HTTP_STATUS_OK);
+
+ confirmAction.mockResolvedValueOnce(false);
+
+ findAllDropdownItems().at(2).vm.$emit('click');
+
+ expect(confirmAction).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(0);
+ });
+
+ it('displays the remaining time in the dropdown', () => {
+ expect(findAllCountdowns().at(0).props('endDateString')).toBe(nodes[2].scheduledAt);
+ });
+ });
+ });
+
+ describe('limit message', () => {
+ it('limit message does not show', async () => {
+ createComponent();
+
+ findDropdown().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(findLimitMessage().exists()).toBe(false);
+ });
+
+ it('limit message does show', async () => {
+ createComponent(3);
+
+ findDropdown().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(findLimitMessage().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 2523b901506..f0772bce167 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -11,7 +11,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import { createAlert, VARIANT_WARNING } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/alert';
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';
@@ -25,7 +25,7 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import { stageReply, users, mockSearch, branches } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const mockProjectPath = 'twitter/flight';
const mockProjectId = '21';
@@ -42,7 +42,7 @@ describe('Pipelines', () => {
let trackingSpy;
const paths = {
- emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ emptyStateSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
ciLintPath: '/ci/lint',
@@ -53,7 +53,7 @@ describe('Pipelines', () => {
};
const noPermissions = {
- emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ emptyStateSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
};
@@ -114,7 +114,6 @@ describe('Pipelines', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.reset();
window.history.pushState.mockReset();
});
@@ -246,7 +245,7 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('should filter pipelines', async () => {
+ it('should filter pipelines', () => {
expect(findPipelinesTable().exists()).toBe(true);
expect(findPipelineUrlLinks()).toHaveLength(1);
@@ -288,7 +287,7 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('should filter pipelines', async () => {
+ it('should filter pipelines', () => {
expect(findEmptyState().text()).toBe('There are currently no pipelines.');
});
@@ -331,11 +330,11 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('requests data with query params on filter submit', async () => {
+ it('requests data with query params on filter submit', () => {
expect(mock.history.get[1].params).toEqual(expectedParams);
});
- it('renders filtered pipelines', async () => {
+ it('renders filtered pipelines', () => {
expect(findPipelineUrlLinks()).toHaveLength(1);
expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`);
});
@@ -357,7 +356,7 @@ describe('Pipelines', () => {
await waitForPromises();
});
- it('requests data with query params on filter submit', async () => {
+ it('requests data with query params on filter submit', () => {
expect(mock.history.get[1].params).toEqual({ page: '1', scope: 'all' });
});
@@ -517,7 +516,7 @@ describe('Pipelines', () => {
expect(findNavigationTabs().exists()).toBe(true);
});
- it('is loading after a time', async () => {
+ it('is loading after a time', () => {
expect(findPipelineUrlLinks()).toHaveLength(mockPipelinesIds.length);
expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockPipelinesIds[0]}`);
expect(findPipelineUrlLinks().at(1).text()).toBe(`#${mockPipelinesIds[1]}`);
@@ -728,7 +727,7 @@ describe('Pipelines', () => {
});
describe('when pipelines cannot be loaded', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mock.onGet(mockPipelinesEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
});
@@ -751,8 +750,9 @@ describe('Pipelines', () => {
});
it('shows error state', () => {
- expect(findEmptyState().text()).toBe(
- 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ expect(findEmptyState().props('title')).toBe('There was an error fetching the pipelines.');
+ expect(findEmptyState().props('description')).toBe(
+ 'Try again in a few moments or contact your support team.',
);
});
});
@@ -776,8 +776,9 @@ describe('Pipelines', () => {
});
it('shows error state', () => {
- expect(findEmptyState().text()).toBe(
- 'There was an error fetching the pipelines. Try again in a few moments or contact your support team.',
+ expect(findEmptyState().props('title')).toBe('There was an error fetching the pipelines.');
+ expect(findEmptyState().props('description')).toBe(
+ 'Try again in a few moments or contact your support team.',
);
});
});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 6ec8901038b..8d2a52eb6d0 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -69,12 +69,6 @@ describe('Pipelines Table', () => {
pipeline = createMockPipeline();
});
- afterEach(() => {
- wrapper.destroy();
-
- wrapper = null;
- });
-
describe('Pipelines Table', () => {
beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' });
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index f6287107ed0..e05d2151f0a 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -2,13 +2,13 @@ import MockAdapter from 'axios-mock-adapter';
import testReports from 'test_fixtures/pipelines/test_report.json';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+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';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Actions TestReports Store', () => {
let mock;
@@ -49,7 +49,7 @@ describe('Actions TestReports Store', () => {
);
});
- it('should create flash on API error', async () => {
+ it('should create alert on API error', async () => {
await testAction(
actions.fetchSummary,
null,
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index ed0cc71eb97..685ac6ea3e5 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -1,9 +1,9 @@
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 { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Mutations TestReports Store', () => {
let mockState;
@@ -58,7 +58,7 @@ describe('Mutations TestReports Store', () => {
expect(mockState.errorMessage).toBe(message);
});
- it('should show a flash message otherwise', () => {
+ it('should show an alert otherwise', () => {
mutations[types.SET_SUITE_ERROR](mockState, {});
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
index f194864447c..f8663408817 100644
--- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
@@ -45,11 +45,6 @@ describe('Test case details', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('required details', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index 9b9ee4172f9..c8c917a1b9e 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -60,10 +60,6 @@ describe('Test reports app', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when component is created', () => {
it('should call fetchSummary when pipeline has test report', () => {
createComponent();
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index da13df833e7..8eb83f17f4d 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -65,10 +65,6 @@ describe('Test reports suite table', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a message when there are no test cases', () => {
createComponent({ suite: [] });
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index f0da0df2ba6..efb1bf09d20 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -30,11 +30,6 @@ describe('Timeago component', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const duration = () => wrapper.find('.duration');
const finishedAt = () => wrapper.find('.finished-at');
const findInProgress = () => wrapper.findByTestId('pipeline-in-progress');
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
index caa66502e11..d518519a424 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -71,11 +71,6 @@ describe('Pipeline Branch Name Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
diff --git a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
index c090fd353f7..cf4ccb5ce43 100644
--- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js
@@ -45,11 +45,6 @@ describe('Pipeline Status Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
index 7311a5d2f5a..88c88d8f16f 100644
--- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
@@ -53,11 +53,6 @@ describe('Pipeline Branch Name Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
index c763bfe1b27..e9ec684a350 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -52,11 +52,6 @@ describe('Pipeline Trigger Author Token', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes config correctly', () => {
expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
});
diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js
index 1299e7277d1..7f247fbbd4f 100644
--- a/spec/frontend/popovers/components/popovers_spec.js
+++ b/spec/frontend/popovers/components/popovers_spec.js
@@ -33,11 +33,6 @@ describe('popovers/components/popovers.vue', () => {
const allPopovers = () => wrapper.findAllComponents(GlPopover);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('addPopovers', () => {
it('attaches popovers to the targets specified', async () => {
const target = createPopoverTarget();
diff --git a/spec/frontend/profile/account/components/delete_account_modal_spec.js b/spec/frontend/profile/account/components/delete_account_modal_spec.js
index e4a316e1ee7..9a8f82f0028 100644
--- a/spec/frontend/profile/account/components/delete_account_modal_spec.js
+++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js
@@ -40,12 +40,6 @@ describe('DeleteAccountModal component', () => {
vm = wrapper.vm;
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- vm = null;
- });
-
const findElements = () => {
const confirmation = vm.confirmWithPassword ? 'password' : 'username';
return {
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index fa0e86a7b05..fa107600d64 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -1,14 +1,15 @@
import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
-import { createAlert } from '~/flash';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import UpdateUsername from '~/profile/account/components/update_username.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('UpdateUsername component', () => {
const rootUrl = TEST_HOST;
@@ -21,8 +22,10 @@ describe('UpdateUsername component', () => {
let wrapper;
let axiosMock;
+ const findNewUsernameInput = () => wrapper.findByTestId('new-username-input');
+
const createComponent = (props = {}) => {
- wrapper = shallowMount(UpdateUsername, {
+ wrapper = shallowMountExtended(UpdateUsername, {
propsData: {
...defaultProps,
...props,
@@ -39,8 +42,8 @@ describe('UpdateUsername component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
+ Vue.config.errorHandler = null;
});
const findElements = () => {
@@ -56,6 +59,13 @@ describe('UpdateUsername component', () => {
};
};
+ const clickModalWithErrorResponse = () => {
+ Vue.config.errorHandler = jest.fn(); // silence thrown error
+ const { modal } = findElements();
+ modal.vm.$emit('primary');
+ return waitForPromises();
+ };
+
it('has a disabled button if the username was not changed', async () => {
const { openModalBtn } = findElements();
@@ -80,14 +90,10 @@ describe('UpdateUsername component', () => {
beforeEach(async () => {
createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ newUsername });
-
- await nextTick();
+ await findNewUsernameInput().setValue(newUsername);
});
- it('confirmation modal contains proper header and body', async () => {
+ it('confirmation modal contains proper header and body', () => {
const { modal } = findElements();
expect(modal.props('title')).toBe('Change username?');
@@ -100,25 +106,26 @@ describe('UpdateUsername component', () => {
axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]);
jest.spyOn(axios, 'put');
- await wrapper.vm.onConfirm();
- await nextTick();
+ const { modal } = findElements();
+ modal.vm.$emit('primary');
+ await waitForPromises();
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
});
it('sets the username after a successful update', async () => {
- const { input, openModalBtn } = findElements();
+ const { input, openModalBtn, modal } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
- expect(input.attributes('disabled')).toBe('disabled');
+ expect(input.attributes('disabled')).toBeDefined();
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(true);
return [HTTP_STATUS_OK, { message: 'Username changed' }];
});
- await wrapper.vm.onConfirm();
- await nextTick();
+ modal.vm.$emit('primary');
+ await waitForPromises();
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(true);
@@ -129,14 +136,15 @@ describe('UpdateUsername component', () => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
- expect(input.attributes('disabled')).toBe('disabled');
+ expect(input.attributes('disabled')).toBeDefined();
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(true);
return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
- await expect(wrapper.vm.onConfirm()).rejects.toThrow();
+ await clickModalWithErrorResponse();
+
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(false);
@@ -147,7 +155,7 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
- await expect(wrapper.vm.onConfirm()).rejects.toThrow();
+ await clickModalWithErrorResponse();
expect(createAlert).toHaveBeenCalledWith({
message: 'Invalid username',
@@ -159,7 +167,7 @@ describe('UpdateUsername component', () => {
return [HTTP_STATUS_BAD_REQUEST];
});
- await expect(wrapper.vm.onConfirm()).rejects.toThrow();
+ await clickModalWithErrorResponse();
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while updating your username, please try again.',
diff --git a/spec/frontend/profile/components/activity_calendar_spec.js b/spec/frontend/profile/components/activity_calendar_spec.js
new file mode 100644
index 00000000000..fb9dc7b22f7
--- /dev/null
+++ b/spec/frontend/profile/components/activity_calendar_spec.js
@@ -0,0 +1,120 @@
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import * as GitLabUIUtils from '@gitlab/ui/dist/utils';
+
+import ActivityCalendar from '~/profile/components/activity_calendar.vue';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { useFakeDate } from 'helpers/fake_date';
+import { userCalendarResponse } from '../mock_data';
+
+jest.mock('~/lib/utils/ajax_cache');
+jest.mock('@gitlab/ui/dist/utils');
+
+describe('ActivityCalendar', () => {
+ // Feb 21st, 2023
+ useFakeDate(2023, 1, 21);
+
+ let wrapper;
+
+ const defaultProvide = {
+ userCalendarPath: '/users/root/calendar.json',
+ utcOffset: '0',
+ };
+
+ const createComponent = () => {
+ wrapper = mountExtended(ActivityCalendar, { provide: defaultProvide });
+ };
+
+ const mockSuccessfulApiRequest = () =>
+ AjaxCache.retrieve.mockResolvedValueOnce(userCalendarResponse);
+ const mockUnsuccessfulApiRequest = () => AjaxCache.retrieve.mockRejectedValueOnce();
+
+ const findCalendar = () => wrapper.findByTestId('contrib-calendar');
+
+ describe('when API request is loading', () => {
+ beforeEach(() => {
+ AjaxCache.retrieve.mockReturnValueOnce(new Promise(() => {}));
+ });
+
+ it('renders loading icon', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(() => {
+ mockSuccessfulApiRequest();
+ });
+
+ it('renders the calendar', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findCalendar().exists()).toBe(true);
+ expect(wrapper.findByText(ActivityCalendar.i18n.calendarHint).exists()).toBe(true);
+ });
+
+ describe('when window is resized', () => {
+ it('re-renders the calendar', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ mockSuccessfulApiRequest();
+ window.innerWidth = 1200;
+ window.dispatchEvent(new Event('resize'));
+
+ await waitForPromises();
+
+ expect(findCalendar().exists()).toBe(true);
+ expect(AjaxCache.retrieve).toHaveBeenCalledTimes(2);
+ });
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ beforeEach(() => {
+ mockUnsuccessfulApiRequest();
+ });
+
+ it('renders error', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
+ });
+
+ describe('when retry button is clicked', () => {
+ it('retries API request', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ mockSuccessfulApiRequest();
+
+ await wrapper.findByRole('button', { name: ActivityCalendar.i18n.retry }).trigger('click');
+
+ await waitForPromises();
+
+ expect(findCalendar().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when screen is extra small', () => {
+ beforeEach(() => {
+ GitLabUIUtils.GlBreakpointInstance.getBreakpointSize.mockReturnValueOnce('xs');
+ });
+
+ it('does not render the calendar', () => {
+ createComponent();
+
+ expect(findCalendar().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js
index 4af428c4e0c..9cc5bdea9be 100644
--- a/spec/frontend/profile/components/followers_tab_spec.js
+++ b/spec/frontend/profile/components/followers_tab_spec.js
@@ -1,4 +1,4 @@
-import { GlTab } from '@gitlab/ui';
+import { GlBadge, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import FollowersTab from '~/profile/components/followers_tab.vue';
@@ -8,12 +8,25 @@ describe('FollowersTab', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(FollowersTab);
+ wrapper = shallowMountExtended(FollowersTab, {
+ provide: {
+ followers: 2,
+ },
+ });
};
- it('renders `GlTab` and sets `title` prop', () => {
+ it('renders `GlTab` and sets title', () => {
createComponent();
- expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Followers'));
+ expect(wrapper.findComponent(GlTab).element.textContent).toContain(
+ s__('UserProfile|Followers'),
+ );
+ });
+
+ it('renders `GlBadge`, sets size and content', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm');
+ expect(wrapper.findComponent(GlBadge).element.textContent).toBe('2');
});
});
diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js
index 75123274ccb..c9d56360c3e 100644
--- a/spec/frontend/profile/components/following_tab_spec.js
+++ b/spec/frontend/profile/components/following_tab_spec.js
@@ -1,4 +1,4 @@
-import { GlTab } from '@gitlab/ui';
+import { GlBadge, GlTab } from '@gitlab/ui';
import { s__ } from '~/locale';
import FollowingTab from '~/profile/components/following_tab.vue';
@@ -8,12 +8,25 @@ describe('FollowingTab', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(FollowingTab);
+ wrapper = shallowMountExtended(FollowingTab, {
+ provide: {
+ followees: 3,
+ },
+ });
};
- it('renders `GlTab` and sets `title` prop', () => {
+ it('renders `GlTab` and sets title', () => {
createComponent();
- expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Following'));
+ expect(wrapper.findComponent(GlTab).element.textContent).toContain(
+ s__('UserProfile|Following'),
+ );
+ });
+
+ it('renders `GlBadge`, sets size and content', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm');
+ expect(wrapper.findComponent(GlBadge).element.textContent).toBe('3');
});
});
diff --git a/spec/frontend/profile/components/overview_tab_spec.js b/spec/frontend/profile/components/overview_tab_spec.js
index eb27515bca3..aeab24cb730 100644
--- a/spec/frontend/profile/components/overview_tab_spec.js
+++ b/spec/frontend/profile/components/overview_tab_spec.js
@@ -1,14 +1,25 @@
-import { GlTab } from '@gitlab/ui';
+import { GlLoadingIcon, GlTab, GlLink } from '@gitlab/ui';
+import projects from 'test_fixtures/api/users/projects/get.json';
import { s__ } from '~/locale';
import OverviewTab from '~/profile/components/overview_tab.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ActivityCalendar from '~/profile/components/activity_calendar.vue';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('OverviewTab', () => {
let wrapper;
- const createComponent = () => {
- wrapper = shallowMountExtended(OverviewTab);
+ const defaultPropsData = {
+ personalProjects: convertObjectPropsToCamelCase(projects, { deep: true }),
+ personalProjectsLoading: false,
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMountExtended(OverviewTab, {
+ propsData: { ...defaultPropsData, ...propsData },
+ });
};
it('renders `GlTab` and sets `title` prop', () => {
@@ -16,4 +27,47 @@ describe('OverviewTab', () => {
expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Overview'));
});
+
+ it('renders `ActivityCalendar` component', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(ActivityCalendar).exists()).toBe(true);
+ });
+
+ it('renders personal projects section heading and `View all` link', () => {
+ createComponent();
+
+ expect(
+ wrapper.findByRole('heading', { name: OverviewTab.i18n.personalProjects }).exists(),
+ ).toBe(true);
+ expect(wrapper.findComponent(GlLink).text()).toBe(OverviewTab.i18n.viewAll);
+ });
+
+ describe('when personal projects are loading', () => {
+ it('renders loading icon', () => {
+ createComponent({
+ propsData: {
+ personalProjects: [],
+ personalProjectsLoading: true,
+ },
+ });
+
+ expect(
+ wrapper.findByTestId('personal-projects-section').findComponent(GlLoadingIcon).exists(),
+ ).toBe(true);
+ });
+ });
+
+ describe('when projects are done loading', () => {
+ it('renders `ProjectsList` component and passes `projects` prop', () => {
+ createComponent();
+
+ expect(
+ wrapper
+ .findByTestId('personal-projects-section')
+ .findComponent(ProjectsList)
+ .props('projects'),
+ ).toMatchObject(defaultPropsData.personalProjects);
+ });
+ });
});
diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js
index 11ab372f1dd..80a1ff422ab 100644
--- a/spec/frontend/profile/components/profile_tabs_spec.js
+++ b/spec/frontend/profile/components/profile_tabs_spec.js
@@ -1,6 +1,9 @@
+import projects from 'test_fixtures/api/users/projects/get.json';
import ProfileTabs from '~/profile/components/profile_tabs.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-
+import { createAlert } from '~/alert';
+import { getUserProjects } from '~/rest_api';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import OverviewTab from '~/profile/components/overview_tab.vue';
import ActivityTab from '~/profile/components/activity_tab.vue';
import GroupsTab from '~/profile/components/groups_tab.vue';
@@ -10,12 +13,20 @@ import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue';
import SnippetsTab from '~/profile/components/snippets_tab.vue';
import FollowersTab from '~/profile/components/followers_tab.vue';
import FollowingTab from '~/profile/components/following_tab.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/alert');
+jest.mock('~/rest_api');
describe('ProfileTabs', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(ProfileTabs);
+ wrapper = shallowMountExtended(ProfileTabs, {
+ provide: {
+ userId: '1',
+ },
+ });
};
it.each([
@@ -33,4 +44,46 @@ describe('ProfileTabs', () => {
expect(wrapper.findComponent(tab).exists()).toBe(true);
});
+
+ describe('when personal projects API request is loading', () => {
+ beforeEach(() => {
+ getUserProjects.mockReturnValueOnce(new Promise(() => {}));
+ createComponent();
+ });
+
+ it('passes correct props to `OverviewTab` component', () => {
+ expect(wrapper.findComponent(OverviewTab).props()).toEqual({
+ personalProjects: [],
+ personalProjectsLoading: true,
+ });
+ });
+ });
+
+ describe('when personal projects API request is successful', () => {
+ beforeEach(async () => {
+ getUserProjects.mockResolvedValueOnce({ data: projects });
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('passes correct props to `OverviewTab` component', () => {
+ expect(wrapper.findComponent(OverviewTab).props()).toMatchObject({
+ personalProjects: convertObjectPropsToCamelCase(projects, { deep: true }),
+ personalProjectsLoading: false,
+ });
+ });
+ });
+
+ describe('when personal projects API request is not successful', () => {
+ beforeEach(() => {
+ getUserProjects.mockRejectedValueOnce();
+ createComponent();
+ });
+
+ it('calls `createAlert`', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: ProfileTabs.i18n.personalProjectsErrorMessage,
+ });
+ });
+ });
});
diff --git a/spec/frontend/profile/components/user_achievements_spec.js b/spec/frontend/profile/components/user_achievements_spec.js
new file mode 100644
index 00000000000..ff6f323621a
--- /dev/null
+++ b/spec/frontend/profile/components/user_achievements_spec.js
@@ -0,0 +1,122 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getUserAchievementsEmptyResponse from 'test_fixtures/graphql/get_user_achievements_empty_response.json';
+import getUserAchievementsLongResponse from 'test_fixtures/graphql/get_user_achievements_long_response.json';
+import getUserAchievementsResponse from 'test_fixtures/graphql/get_user_achievements_with_avatar_and_description_response.json';
+import getUserAchievementsPrivateGroupResponse from 'test_fixtures/graphql/get_user_achievements_from_private_group.json';
+import getUserAchievementsNoAvatarResponse from 'test_fixtures/graphql/get_user_achievements_without_avatar_or_description_response.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import UserAchievements from '~/profile/components/user_achievements.vue';
+import getUserAchievements from '~/profile/components//graphql/get_user_achievements.query.graphql';
+import { getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+const USER_ID = 123;
+const ROOT_URL = 'https://gitlab.com/';
+const PLACEHOLDER_URL = 'https://gitlab.com/assets/gitlab_logo.png';
+const userAchievement1 = getUserAchievementsResponse.data.user.userAchievements.nodes[0];
+
+Vue.use(VueApollo);
+
+describe('UserAchievements', () => {
+ let wrapper;
+
+ const getUserAchievementsQueryHandler = jest.fn().mockResolvedValue(getUserAchievementsResponse);
+ const achievement = () => wrapper.findByTestId('user-achievement');
+
+ const createComponent = ({ queryHandler = getUserAchievementsQueryHandler } = {}) => {
+ const fakeApollo = createMockApollo([[getUserAchievements, queryHandler]]);
+
+ wrapper = mountExtended(UserAchievements, {
+ apolloProvider: fakeApollo,
+ provide: {
+ rootUrl: ROOT_URL,
+ userId: USER_ID,
+ },
+ });
+ };
+
+ it('renders no achievements on reject', async () => {
+ createComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('user-achievement').length).toBe(0);
+ });
+
+ it('renders no achievements when none are present', async () => {
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsEmptyResponse),
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('user-achievement').length).toBe(0);
+ });
+
+ it('only renders 3 achievements when more are present', async () => {
+ createComponent({ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsLongResponse) });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('user-achievement').length).toBe(3);
+ });
+
+ it('renders correctly if the achievement is from a private namespace', async () => {
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsPrivateGroupResponse),
+ });
+
+ await waitForPromises();
+
+ const userAchievement =
+ getUserAchievementsPrivateGroupResponse.data.user.userAchievements.nodes[0];
+
+ expect(achievement().text()).toContain(userAchievement.achievement.name);
+ expect(achievement().text()).toContain(
+ `Awarded ${getTimeago().format(
+ userAchievement.createdAt,
+ timeagoLanguageCode,
+ )} by a private namespace`,
+ );
+ });
+
+ it('renders achievement correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(achievement().text()).toContain(userAchievement1.achievement.name);
+ expect(achievement().text()).toContain(
+ `Awarded ${getTimeago().format(userAchievement1.createdAt, timeagoLanguageCode)} by`,
+ );
+ expect(achievement().text()).toContain(userAchievement1.achievement.namespace.fullPath);
+ expect(achievement().text()).toContain(userAchievement1.achievement.description);
+ expect(achievement().find('img').attributes('src')).toBe(
+ userAchievement1.achievement.avatarUrl,
+ );
+ });
+
+ it('renders a placeholder when no avatar is present', async () => {
+ gon.gitlab_logo = PLACEHOLDER_URL;
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsNoAvatarResponse),
+ });
+
+ await waitForPromises();
+
+ expect(achievement().find('img').attributes('src')).toBe(PLACEHOLDER_URL);
+ });
+
+ it('does not render a description when none is present', async () => {
+ gon.gitlab_logo = PLACEHOLDER_URL;
+ createComponent({
+ queryHandler: jest.fn().mockResolvedValue(getUserAchievementsNoAvatarResponse),
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('achievement-description').length).toBe(0);
+ });
+});
diff --git a/spec/frontend/profile/mock_data.js b/spec/frontend/profile/mock_data.js
new file mode 100644
index 00000000000..7106ea84619
--- /dev/null
+++ b/spec/frontend/profile/mock_data.js
@@ -0,0 +1,22 @@
+export const userCalendarResponse = {
+ '2022-11-18': 13,
+ '2022-11-19': 21,
+ '2022-11-20': 14,
+ '2022-11-21': 15,
+ '2022-11-22': 20,
+ '2022-11-23': 21,
+ '2022-11-24': 15,
+ '2022-11-25': 14,
+ '2022-11-26': 16,
+ '2022-11-27': 13,
+ '2022-11-28': 4,
+ '2022-11-29': 1,
+ '2022-11-30': 1,
+ '2022-12-13': 1,
+ '2023-01-10': 3,
+ '2023-01-11': 2,
+ '2023-01-20': 1,
+ '2023-02-02': 1,
+ '2023-02-06': 2,
+ '2023-02-07': 2,
+};
diff --git a/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js
index e60602ab336..e69bfad765a 100644
--- a/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js
+++ b/spec/frontend/profile/preferences/components/diffs_colors_preview_spec.js
@@ -12,11 +12,6 @@ describe('DiffsColorsPreview component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders diff colors preview', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/profile/preferences/components/diffs_colors_spec.js b/spec/frontend/profile/preferences/components/diffs_colors_spec.js
index 02f501a0b06..28fc01654b9 100644
--- a/spec/frontend/profile/preferences/components/diffs_colors_spec.js
+++ b/spec/frontend/profile/preferences/components/diffs_colors_spec.js
@@ -29,11 +29,6 @@ describe('DiffsColors component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('mounts', () => {
createComponent();
@@ -65,9 +60,12 @@ describe('DiffsColors component', () => {
});
it.each([
- [{}, '--diff-deletion-color: rgba(255,0,0,0.2); --diff-addition-color: rgba(0,255,0,0.2);'],
- [{ addition: null }, '--diff-deletion-color: rgba(255,0,0,0.2);'],
- [{ deletion: null }, '--diff-addition-color: rgba(0,255,0,0.2);'],
+ [
+ {},
+ '--diff-deletion-color: rgba(255, 0, 0, 0.2); --diff-addition-color: rgba(0, 255, 0, 0.2);',
+ ],
+ [{ addition: null }, '--diff-deletion-color: rgba(255, 0, 0, 0.2);'],
+ [{ deletion: null }, '--diff-addition-color: rgba(0, 255, 0, 0.2);'],
])('should set correct CSS variables', (provide, expectedStyle) => {
createComponent(provide);
diff --git a/spec/frontend/profile/preferences/components/integration_view_spec.js b/spec/frontend/profile/preferences/components/integration_view_spec.js
index f650bee7fda..b809f2f4aed 100644
--- a/spec/frontend/profile/preferences/components/integration_view_spec.js
+++ b/spec/frontend/profile/preferences/components/integration_view_spec.js
@@ -38,11 +38,6 @@ describe('IntegrationView component', () => {
const findHiddenField = () =>
wrapper.findByTestId('profile-preferences-integration-hidden-field');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render the form group legend correctly', () => {
wrapper = createComponent();
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index 91cd868daac..21167dccda9 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/alert';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
import ProfilePreferences from '~/profile/preferences/components/profile_preferences.vue';
import { i18n } from '~/profile/preferences/constants';
@@ -17,7 +17,7 @@ import {
lightModeThemeId2,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const expectedUrl = '/foo';
useMockLocationHelper();
@@ -83,11 +83,6 @@ describe('ProfilePreferences component', () => {
document.body.classList.add('content-wrapper');
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should not render Integrations section', () => {
wrapper = createComponent();
const views = wrapper.findAllComponents(IntegrationView);
diff --git a/spec/frontend/profile/utils_spec.js b/spec/frontend/profile/utils_spec.js
new file mode 100644
index 00000000000..43537afe169
--- /dev/null
+++ b/spec/frontend/profile/utils_spec.js
@@ -0,0 +1,15 @@
+import { getVisibleCalendarPeriod } from '~/profile/utils';
+import { CALENDAR_PERIOD_12_MONTHS, CALENDAR_PERIOD_6_MONTHS } from '~/profile/constants';
+
+describe('getVisibleCalendarPeriod', () => {
+ it.each`
+ width | expected
+ ${1000} | ${CALENDAR_PERIOD_12_MONTHS}
+ ${900} | ${CALENDAR_PERIOD_6_MONTHS}
+ `('returns $expected when container width is $width', ({ width, expected }) => {
+ const container = document.createElement('div');
+ jest.spyOn(container, 'getBoundingClientRect').mockReturnValueOnce({ width });
+
+ expect(getVisibleCalendarPeriod(container)).toBe(expected);
+ });
+});
diff --git a/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js b/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js
index d230b96ad82..68ea3a4dc4d 100644
--- a/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js
+++ b/spec/frontend/projects/clusters_deprecation_slert/components/clusters_deprecation_alert_spec.js
@@ -26,10 +26,6 @@ describe('ClustersDeprecationAlert', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('should render a non-dismissible warning alert', () => {
expect(findAlert().props()).toMatchObject({
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index 6aa5a9a5a3a..bff40c2bc39 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -12,7 +12,7 @@ describe('BranchesDropdown', () => {
let store;
const spyFetchBranches = jest.fn();
- const createComponent = (props, state = { isFetching: false }) => {
+ const createComponent = (props, state = { isFetching: false, branch: '_main_' }) => {
store = new Vuex.Store({
getters: {
joinedBranches: () => ['_main_', '_branch_1_', '_branch_2_'],
@@ -41,8 +41,6 @@ describe('BranchesDropdown', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
spyFetchBranches.mockReset();
});
@@ -61,7 +59,7 @@ describe('BranchesDropdown', () => {
});
describe('Selecting Dropdown Item', () => {
- it('emits event', async () => {
+ it('emits event', () => {
findDropdown().vm.$emit('select', '_anything_');
expect(wrapper.emitted()).toHaveProperty('input');
@@ -70,13 +68,11 @@ describe('BranchesDropdown', () => {
describe('When searching', () => {
it('invokes fetchBranches', async () => {
- const spy = jest.spyOn(wrapper.vm, 'fetchBranches');
-
findDropdown().vm.$emit('search', '_anything_');
await nextTick();
- expect(spy).toHaveBeenCalledWith('_anything_');
+ expect(spyFetchBranches).toHaveBeenCalledWith(expect.any(Object), '_anything_');
});
});
});
diff --git a/spec/frontend/projects/commit/components/commit_options_dropdown_spec.js b/spec/frontend/projects/commit/components/commit_options_dropdown_spec.js
index 70491405986..7df498f597b 100644
--- a/spec/frontend/projects/commit/components/commit_options_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/commit_options_dropdown_spec.js
@@ -85,7 +85,7 @@ describe('BranchesDropdown', () => {
expect(findTagItem().exists()).toBe(false);
});
- it('does not have a email patches options', () => {
+ it('does not have a patches options', () => {
createComponent({ canEmailPatches: false });
expect(findEmailPatchesItem().exists()).toBe(false);
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index c59cf700e0d..d40e2d7a48c 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -1,9 +1,9 @@
import { GlModal, GlForm, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { within } from '@testing-library/dom';
-import { shallowMount, mount, createWrapper } from '@vue/test-utils';
+import { createWrapper } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
@@ -21,21 +21,24 @@ describe('CommitFormModal', () => {
let store;
let axiosMock;
- const createComponent = (method, state = {}, provide = {}, propsData = {}) => {
+ const createComponent = ({
+ method = shallowMountExtended,
+ state = {},
+ provide = {},
+ propsData = {},
+ } = {}) => {
store = createStore({ ...mockData.mockModal, ...state });
- wrapper = extendedWrapper(
- method(CommitFormModal, {
- provide: {
- ...provide,
- },
- propsData: { ...mockData.modalPropsData, ...propsData },
- store,
- attrs: {
- static: true,
- visible: true,
- },
- }),
- );
+ wrapper = method(CommitFormModal, {
+ provide: {
+ ...provide,
+ },
+ propsData: { ...mockData.modalPropsData, ...propsData },
+ store,
+ attrs: {
+ static: true,
+ visible: true,
+ },
+ });
};
const findModal = () => wrapper.findComponent(GlModal);
@@ -55,7 +58,6 @@ describe('CommitFormModal', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
@@ -63,13 +65,13 @@ describe('CommitFormModal', () => {
it('Listens for opening of modal on mount', () => {
jest.spyOn(eventHub, '$on');
- createComponent(shallowMount);
+ createComponent();
expect(eventHub.$on).toHaveBeenCalledWith(mockData.modalPropsData.openModal, wrapper.vm.show);
});
it('Shows modal', () => {
- createComponent(shallowMount);
+ createComponent();
const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
wrapper.vm.show();
@@ -78,25 +80,25 @@ describe('CommitFormModal', () => {
});
it('Clears the modal state once modal is hidden', () => {
- createComponent(shallowMount);
+ createComponent();
jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper.vm.checked = false;
+ findCheckBox().vm.$emit('input', false);
findModal().vm.$emit('hidden');
expect(store.dispatch).toHaveBeenCalledWith('clearModal');
expect(store.dispatch).toHaveBeenCalledWith('setSelectedBranch', '');
- expect(wrapper.vm.checked).toBe(true);
+ expect(findCheckBox().attributes('checked')).toBe('true');
});
it('Shows the checkbox for new merge request', () => {
- createComponent(shallowMount);
+ createComponent();
expect(findCheckBox().exists()).toBe(true);
});
it('Shows the prepended text', () => {
- createComponent(shallowMount, {}, { prependedText: '_prepended_text_' });
+ createComponent({ provide: { prependedText: '_prepended_text_' } });
expect(findPrependedText().exists()).toBe(true);
expect(findPrependedText().findComponent(GlSprintf).attributes('message')).toBe(
@@ -105,25 +107,25 @@ describe('CommitFormModal', () => {
});
it('Does not show prepended text', () => {
- createComponent(shallowMount);
+ createComponent();
expect(findPrependedText().exists()).toBe(false);
});
it('Does not show extra message text', () => {
- createComponent(shallowMount);
+ createComponent();
expect(findModal().find('[data-testid="appended-text"]').exists()).toBe(false);
});
it('Does not show the checkbox for new merge request', () => {
- createComponent(shallowMount, { pushCode: false });
+ createComponent({ state: { pushCode: false } });
expect(findCheckBox().exists()).toBe(false);
});
it('Shows the branch in fork message', () => {
- createComponent(shallowMount, { pushCode: false });
+ createComponent({ state: { pushCode: false } });
expect(findAppendedText().exists()).toBe(true);
expect(findAppendedText().findComponent(GlSprintf).attributes('message')).toContain(
@@ -132,7 +134,7 @@ describe('CommitFormModal', () => {
});
it('Shows the branch collaboration message', () => {
- createComponent(shallowMount, { pushCode: false, branchCollaboration: true });
+ createComponent({ state: { pushCode: false, branchCollaboration: true } });
expect(findAppendedText().exists()).toBe(true);
expect(findAppendedText().findComponent(GlSprintf).attributes('message')).toContain(
@@ -143,17 +145,13 @@ describe('CommitFormModal', () => {
describe('Taking action on the form', () => {
beforeEach(() => {
- createComponent(mount);
+ createComponent({ method: mountExtended });
});
it('Action primary button dispatches submit action', () => {
- const submitSpy = jest.spyOn(findForm().element, 'submit');
-
getByText(mockData.modalPropsData.i18n.actionPrimaryText).trigger('click');
- expect(submitSpy).toHaveBeenCalled();
-
- submitSpy.mockRestore();
+ expect(wrapper.vm.$refs.form.$el.submit).toHaveBeenCalled();
});
it('Changes the start_branch input value', async () => {
@@ -165,8 +163,8 @@ describe('CommitFormModal', () => {
});
it('Changes the target_project_id input value', async () => {
- createComponent(shallowMount, {}, {}, { isCherryPick: true });
- findProjectsDropdown().vm.$emit('selectProject', '_changed_project_value_');
+ createComponent({ propsData: { isCherryPick: true } });
+ findProjectsDropdown().vm.$emit('input', '_changed_project_value_');
await nextTick();
@@ -175,12 +173,9 @@ describe('CommitFormModal', () => {
});
it('action primary button triggers Redis HLL tracking api call', async () => {
- createComponent(mount, {}, {}, { primaryActionEventName: 'test_event' });
-
+ createComponent({ method: mountExtended, propsData: { primaryActionEventName: 'test_event' } });
await nextTick();
- jest.spyOn(findForm().element, 'submit');
-
getByText(mockData.modalPropsData.i18n.actionPrimaryText).trigger('click');
expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith('test_event');
diff --git a/spec/frontend/projects/commit/components/projects_dropdown_spec.js b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
index 0e213ff388a..baf2ea2656f 100644
--- a/spec/frontend/projects/commit/components/projects_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ProjectsDropdown from '~/projects/commit/components/projects_dropdown.vue';
@@ -38,7 +38,6 @@ describe('ProjectsDropdown', () => {
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
afterEach(() => {
- wrapper.destroy();
spyFetchProjects.mockReset();
});
@@ -48,20 +47,24 @@ describe('ProjectsDropdown', () => {
});
describe('Custom events', () => {
- it('should emit selectProject if a project is clicked', () => {
+ it('should emit input if a project is clicked', () => {
findDropdown().vm.$emit('select', '1');
- expect(wrapper.emitted('selectProject')).toEqual([['1']]);
+ expect(wrapper.emitted('input')).toEqual([['1']]);
});
});
});
describe('Case insensitive for search term', () => {
beforeEach(() => {
- createComponent('_PrOjEcT_1_');
+ createComponent('_PrOjEcT_1_', { targetProjectId: '1' });
});
- it('renders only the project searched for', () => {
+ it('renders only the project searched for', async () => {
+ findDropdown().vm.$emit('search', '_project_1_');
+
+ await nextTick();
+
expect(findDropdown().props('items')).toEqual([{ text: '_project_1_', value: '1' }]);
});
});
diff --git a/spec/frontend/projects/commit/store/actions_spec.js b/spec/frontend/projects/commit/store/actions_spec.js
index 008710984b9..adb87142fee 100644
--- a/spec/frontend/projects/commit/store/actions_spec.js
+++ b/spec/frontend/projects/commit/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { PROJECT_BRANCHES_ERROR } from '~/projects/commit/constants';
import * as actions from '~/projects/commit/store/actions';
@@ -8,7 +8,7 @@ import * as types from '~/projects/commit/store/mutation_types';
import getInitialState from '~/projects/commit/store/state';
import mockData from '../mock_data';
-jest.mock('~/flash.js');
+jest.mock('~/alert');
describe('Commit form modal store actions', () => {
let axiosMock;
@@ -63,7 +63,7 @@ describe('Commit form modal store actions', () => {
);
});
- it('should show flash error and set error in state on fetchBranches failure', async () => {
+ it('should show an alert and set error in state on fetchBranches failure', async () => {
jest.spyOn(axios, 'get').mockRejectedValue();
await testAction(actions.fetchBranches, {}, state, [], [{ type: 'requestBranches' }]);
diff --git a/spec/frontend/projects/commit_box/info/init_details_button_spec.js b/spec/frontend/projects/commit_box/info/init_details_button_spec.js
new file mode 100644
index 00000000000..bf9c6a4c998
--- /dev/null
+++ b/spec/frontend/projects/commit_box/info/init_details_button_spec.js
@@ -0,0 +1,47 @@
+import { setHTMLFixture } from 'helpers/fixtures';
+import { initDetailsButton } from '~/projects/commit_box/info/init_details_button';
+
+const htmlFixture = `
+ <span>
+ <a href="#" class="js-details-expand"><span class="sub-element">Expand</span></a>
+ <span class="js-details-content hide">Some branch</span>
+ </span>`;
+
+describe('~/projects/commit_box/info/init_details_button', () => {
+ const findExpandButton = () => document.querySelector('.js-details-expand');
+ const findContent = () => document.querySelector('.js-details-content');
+ const findExpandSubElement = () => document.querySelector('.sub-element');
+
+ beforeEach(() => {
+ setHTMLFixture(htmlFixture);
+ initDetailsButton();
+ });
+
+ describe('when clicking the expand button', () => {
+ it('renders the content by removing the `hide` class', () => {
+ expect(findContent().classList).toContain('hide');
+ findExpandButton().click();
+ expect(findContent().classList).not.toContain('hide');
+ });
+
+ it('hides the expand button by adding the `gl-display-none` class', () => {
+ expect(findExpandButton().classList).not.toContain('gl-display-none');
+ findExpandButton().click();
+ expect(findExpandButton().classList).toContain('gl-display-none');
+ });
+ });
+
+ describe('when user clicks on element inside of expand button', () => {
+ it('renders the content by removing the `hide` class', () => {
+ expect(findContent().classList).toContain('hide');
+ findExpandSubElement().click();
+ expect(findContent().classList).not.toContain('hide');
+ });
+
+ it('hides the expand button by adding the `gl-display-none` class', () => {
+ expect(findExpandButton().classList).not.toContain('gl-display-none');
+ findExpandSubElement().click();
+ expect(findExpandButton().classList).toContain('gl-display-none');
+ });
+ });
+});
diff --git a/spec/frontend/projects/commit_box/info/load_branches_spec.js b/spec/frontend/projects/commit_box/info/load_branches_spec.js
index e49d92188ed..b00a6378e07 100644
--- a/spec/frontend/projects/commit_box/info/load_branches_spec.js
+++ b/spec/frontend/projects/commit_box/info/load_branches_spec.js
@@ -4,6 +4,9 @@ import { setHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { loadBranches } from '~/projects/commit_box/info/load_branches';
+import { initDetailsButton } from '~/projects/commit_box/info/init_details_button';
+
+jest.mock('~/projects/commit_box/info/init_details_button');
const mockCommitPath = '/commit/abcd/branches';
const mockBranchesRes =
@@ -26,6 +29,13 @@ describe('~/projects/commit_box/info/load_branches', () => {
mock.onGet(mockCommitPath).reply(HTTP_STATUS_OK, mockBranchesRes);
});
+ it('initializes the details button', async () => {
+ loadBranches();
+ await waitForPromises();
+
+ expect(initDetailsButton).toHaveBeenCalled();
+ });
+
it('loads and renders branches info', async () => {
loadBranches();
await waitForPromises();
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 907e0e226b6..630b8feafbc 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
@@ -54,7 +55,6 @@ describe('Author Select', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
@@ -65,39 +65,23 @@ describe('Author Select', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
describe('user is searching via "filter by commit message"', () => {
- it('disables dropdown container', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ hasSearchParam: true });
+ beforeEach(() => {
+ setWindowLocation(`?search=foo`);
+ createComponent();
+ });
- await nextTick();
+ it('does not disable dropdown container', () => {
expect(findDropdownContainer().attributes('disabled')).toBeUndefined();
});
- it('has correct tooltip message', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ hasSearchParam: true });
-
- await nextTick();
+ it('has correct tooltip message', () => {
expect(findDropdownContainer().attributes('title')).toBe(
'Searching by both author and message is currently not supported.',
);
});
- it('disables dropdown', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ hasSearchParam: false });
-
- await nextTick();
- expect(findDropdown().attributes('disabled')).toBeUndefined();
- });
-
- it('hasSearchParam if user types a truthy string', () => {
- wrapper.vm.setSearchParam('false');
-
- expect(wrapper.vm.hasSearchParam).toBe(true);
+ it('disables dropdown', () => {
+ expect(findDropdown().attributes('disabled')).toBeDefined();
});
});
@@ -107,9 +91,8 @@ describe('Author Select', () => {
});
it('displays the current selected author', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ currentAuthor });
+ setWindowLocation(`?author=${currentAuthor}`);
+ createComponent();
await nextTick();
expect(findDropdown().attributes('text')).toBe(currentAuthor);
@@ -147,12 +130,14 @@ describe('Author Select', () => {
expect(findDropdownItems().at(0).text()).toBe('Any Author');
});
- it('displays the project authors', async () => {
- await nextTick();
+ it('displays the project authors', () => {
expect(findDropdownItems()).toHaveLength(authors.length + 1);
});
it('has the correct props', async () => {
+ setWindowLocation(`?author=${currentAuthor}`);
+ createComponent();
+
const [{ avatar_url: avatarUrl, username }] = authors;
const result = {
avatarUrl,
@@ -160,16 +145,11 @@ describe('Author Select', () => {
isChecked: true,
};
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ currentAuthor });
-
await nextTick();
expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result));
});
- it("display the author's name", async () => {
- await nextTick();
+ it("display the author's name", () => {
expect(findDropdownItems().at(1).text()).toBe(currentAuthor);
});
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index bae9c48fc1e..8afa2a6fb8f 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -1,13 +1,13 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import actions from '~/projects/commits/store/actions';
import * as types from '~/projects/commits/store/mutation_types';
import createState from '~/projects/commits/store/state';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Project commits actions', () => {
let state;
@@ -34,8 +34,8 @@ describe('Project commits actions', () => {
]));
});
- describe('shows a flash message when there is an error', () => {
- it('creates a flash', () => {
+ describe('shows an alert when there is an error', () => {
+ it('creates an alert', () => {
const mockDispatchContext = { dispatch: () => {}, commit: () => {}, state };
actions.receiveAuthorsError(mockDispatchContext);
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
index 9b052a17caa..ee96f46ea0c 100644
--- a/spec/frontend/projects/compare/components/app_spec.js
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -21,11 +21,6 @@ describe('CompareApp component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
index 21cca857c6a..0b1085470b8 100644
--- a/spec/frontend/projects/compare/components/repo_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js
@@ -16,11 +16,6 @@ describe('RepoDropdown component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
diff --git a/spec/frontend/projects/compare/components/revision_card_spec.js b/spec/frontend/projects/compare/components/revision_card_spec.js
index b23bd91ceda..3c9c61c8903 100644
--- a/spec/frontend/projects/compare/components/revision_card_spec.js
+++ b/spec/frontend/projects/compare/components/revision_card_spec.js
@@ -16,11 +16,6 @@ describe('RepoDropdown component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
index 53763bd7d8f..e289569f8ce 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -1,8 +1,8 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+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';
@@ -14,7 +14,7 @@ const defaultProps = {
paramsBranch: 'main',
};
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RevisionDropdown component', () => {
let wrapper;
@@ -35,11 +35,14 @@ describe('RevisionDropdown component', () => {
});
afterEach(() => {
- wrapper.destroy();
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(
@@ -58,10 +61,21 @@ describe('RevisionDropdown component', () => {
createComponent();
- await axios.waitForAll();
+ expect(findBranchesDropdownItem()).toHaveLength(0);
+ expect(findTagsDropdownItem()).toHaveLength(0);
- expect(wrapper.vm.branches).toEqual(Branches);
- expect(wrapper.vm.tags).toEqual(Tags);
+ 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 () => {
@@ -70,16 +84,17 @@ describe('RevisionDropdown component', () => {
Tags: undefined,
});
- await axios.waitForAll();
+ await waitForPromises();
- expect(wrapper.vm.branches).toEqual([]);
- expect(wrapper.vm.tags).toEqual([]);
+ expect(findBranchesDropdownItem()).toHaveLength(0);
+ expect(findTagsDropdownItem()).toHaveLength(0);
});
- it('shows flash message on error', async () => {
+ it('shows an alert on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
- await wrapper.vm.fetchBranchesAndTags();
+ await waitForPromises();
+
expect(createAlert).toHaveBeenCalled();
});
@@ -102,17 +117,19 @@ describe('RevisionDropdown component', () => {
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';
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ branches: ['some-branch'] });
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
+ Branches: [branchName],
+ });
- await nextTick();
+ createComponent();
+ await waitForPromises();
findFirstGlDropdownItem().vm.$emit('click');
expect(wrapper.emitted()).toEqual({
- selectRevision: [[{ direction: 'from', revision: 'some-branch' }]],
+ selectRevision: [[{ direction: 'from', revision: branchName }]],
});
});
});
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
index db4a1158996..1cf99f16601 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -2,13 +2,14 @@ import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+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.vue';
import { revisionDropdownDefaultProps as defaultProps } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('RevisionDropdown component', () => {
let wrapper;
@@ -32,12 +33,15 @@ describe('RevisionDropdown component', () => {
});
afterEach(() => {
- wrapper.destroy();
axiosMock.restore();
});
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findBranchesDropdownItem = () =>
+ wrapper.findAllComponents('[data-testid="branches-dropdown-item"]');
+ const findTagsDropdownItem = () =>
+ wrapper.findAllComponents('[data-testid="tags-dropdown-item"]');
it('sets hidden input', () => {
createComponent();
@@ -57,17 +61,29 @@ describe('RevisionDropdown component', () => {
createComponent();
- await axios.waitForAll();
- expect(wrapper.vm.branches).toEqual(Branches);
- expect(wrapper.vm.tags).toEqual(Tags);
+ expect(findBranchesDropdownItem()).toHaveLength(0);
+ expect(findTagsDropdownItem()).toHaveLength(0);
+
+ await waitForPromises();
+
+ expect(findBranchesDropdownItem()).toHaveLength(Branches.length);
+ expect(findTagsDropdownItem()).toHaveLength(Tags.length);
+
+ Branches.forEach((branch, index) => {
+ expect(findBranchesDropdownItem().at(index).text()).toBe(branch);
+ });
+
+ Tags.forEach((tag, index) => {
+ expect(findTagsDropdownItem().at(index).text()).toBe(tag);
+ });
});
- it('shows flash message on error', async () => {
+ it('shows an alert on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
+ await waitForPromises();
- await wrapper.vm.fetchBranchesAndTags();
expect(createAlert).toHaveBeenCalled();
});
@@ -83,17 +99,17 @@ describe('RevisionDropdown component', () => {
refsProjectPath: newRefsProjectPath,
});
- await axios.waitForAll();
+ await waitForPromises();
expect(axios.get).toHaveBeenLastCalledWith(newRefsProjectPath);
});
describe('search', () => {
- it('shows flash message on error', async () => {
+ it('shows alert on error', async () => {
axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
+ await waitForPromises();
- await wrapper.vm.searchBranchesAndTags();
expect(createAlert).toHaveBeenCalled();
});
@@ -108,7 +124,7 @@ describe('RevisionDropdown component', () => {
const mockSearchTerm = 'foobar';
createComponent();
findSearchBox().vm.$emit('input', mockSearchTerm);
- await axios.waitForAll();
+ await waitForPromises();
expect(axios.get).toHaveBeenCalledWith(
defaultProps.refsProjectPath,
@@ -141,8 +157,14 @@ describe('RevisionDropdown component', () => {
});
it('emits `selectRevision` event when another revision is selected', async () => {
+ jest.spyOn(axios, 'get').mockResolvedValue({
+ data: {
+ Branches: ['some-branch'],
+ Tags: [],
+ },
+ });
+
createComponent();
- wrapper.vm.branches = ['some-branch'];
await nextTick();
findGlDropdown().findAllComponents(GlDropdownItem).at(0).vm.$emit('click');
diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js
index 49e3218e5bc..bae76e7eeb6 100644
--- a/spec/frontend/projects/components/project_delete_button_spec.js
+++ b/spec/frontend/projects/components/project_delete_button_spec.js
@@ -33,11 +33,6 @@ describe('Project remove modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('initialized', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js
index 097b18025a3..6b4ef341b0c 100644
--- a/spec/frontend/projects/components/shared/delete_button_spec.js
+++ b/spec/frontend/projects/components/shared/delete_button_spec.js
@@ -45,11 +45,6 @@ describe('Project remove modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('intialized', () => {
beforeEach(() => {
createComponent();
@@ -74,7 +69,7 @@ describe('Project remove modal', () => {
});
it('the confirm button is disabled', () => {
- expect(findConfirmButton().attributes('disabled')).toBe('true');
+ expect(findConfirmButton().attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js
index 50638755260..e9b11ce544a 100644
--- a/spec/frontend/projects/details/upload_button_spec.js
+++ b/spec/frontend/projects/details/upload_button_spec.js
@@ -27,10 +27,6 @@ describe('UploadButton', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays an upload button', () => {
expect(wrapper.findComponent(GlButton).exists()).toBe(true);
});
diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js
index f6edbab3cca..60d8385eb91 100644
--- a/spec/frontend/projects/new/components/app_spec.js
+++ b/spec/frontend/projects/new/components/app_spec.js
@@ -6,12 +6,12 @@ describe('Experimental new project creation app', () => {
let wrapper;
const createComponent = (propsData) => {
- wrapper = shallowMount(App, { propsData });
+ wrapper = shallowMount(App, {
+ propsData: { rootPath: '/', projectsUrl: '/dashboard/projects', ...propsData },
+ });
};
- afterEach(() => {
- wrapper.destroy();
- });
+ const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
it('passes custom new project guideline text to underlying component', () => {
const DEMO_GUIDELINES = 'Demo guidelines';
@@ -34,11 +34,45 @@ describe('Experimental new project creation app', () => {
expect(
Boolean(
- wrapper
- .findComponent(NewNamespacePage)
+ findNewNamespacePage()
.props()
.panels.find((p) => p.name === 'cicd_for_external_repo'),
),
).toBe(isCiCdAvailable);
});
+
+ it.each`
+ canImportProjects | outcome
+ ${false} | ${'do not show Import panel'}
+ ${true} | ${'show Import panel'}
+ `('$outcome when canImportProjects is $canImportProjects', ({ canImportProjects }) => {
+ createComponent({
+ canImportProjects,
+ });
+
+ expect(
+ findNewNamespacePage()
+ .props()
+ .panels.some((p) => p.name === 'import_project'),
+ ).toBe(canImportProjects);
+ });
+
+ it('creates correct breadcrumbs for top-level projects', () => {
+ createComponent();
+
+ expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/', text: 'Your work' },
+ { href: '/dashboard/projects', text: 'Projects' },
+ { href: '#', text: 'New project' },
+ ]);
+ });
+
+ it('creates correct breadcrumbs for projects within groups', () => {
+ createComponent({ parentGroupUrl: '/parent-group', parentGroupName: 'Parent Group' });
+
+ expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
+ { href: '/parent-group', text: 'Parent Group' },
+ { href: '#', text: 'New project' },
+ ]);
+ });
});
diff --git a/spec/frontend/projects/new/components/deployment_target_select_spec.js b/spec/frontend/projects/new/components/deployment_target_select_spec.js
index f3b22d4a1b9..57b804b632a 100644
--- a/spec/frontend/projects/new/components/deployment_target_select_spec.js
+++ b/spec/frontend/projects/new/components/deployment_target_select_spec.js
@@ -47,7 +47,6 @@ describe('Deployment target select', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
@@ -57,7 +56,7 @@ describe('Deployment target select', () => {
it('renders a select with the disabled default option', () => {
expect(findSelect().find('option').text()).toBe('Select the deployment target');
- expect(findSelect().find('option').attributes('disabled')).toBe('disabled');
+ expect(findSelect().find('option').attributes('disabled')).toBeDefined();
});
describe.each`
diff --git a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
index 16b4493c622..1a43dcb682b 100644
--- a/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
+++ b/spec/frontend/projects/new/components/new_project_push_tip_popover_spec.js
@@ -37,7 +37,6 @@ describe('New project push tip popover', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/projects/new/components/new_project_url_select_spec.js b/spec/frontend/projects/new/components/new_project_url_select_spec.js
index 67532cea61e..ceac4435282 100644
--- a/spec/frontend/projects/new/components/new_project_url_select_spec.js
+++ b/spec/frontend/projects/new/components/new_project_url_select_spec.js
@@ -3,8 +3,8 @@ import {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
- GlSearchBoxByType,
GlTruncate,
+ GlSearchBoxByType,
} from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -12,6 +12,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { stubComponent } from 'helpers/stub_component';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '~/projects/new/event_hub';
import NewProjectUrlSelect from '~/projects/new/components/new_project_url_select.vue';
@@ -68,6 +69,7 @@ describe('NewProjectUrlSelect component', () => {
};
let mockQueryResponse;
+ let focusInputSpy;
const mountComponent = ({
search = '',
@@ -78,6 +80,7 @@ describe('NewProjectUrlSelect component', () => {
mockQueryResponse = jest.fn().mockResolvedValue({ data: queryResponse });
const requestHandlers = [[searchQuery, mockQueryResponse]];
const apolloProvider = createMockApollo(requestHandlers);
+ focusInputSpy = jest.fn();
return mountFn(NewProjectUrlSelect, {
apolloProvider,
@@ -87,13 +90,17 @@ describe('NewProjectUrlSelect component', () => {
search,
};
},
+ stubs: {
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ methods: { focusInput: focusInputSpy },
+ }),
+ },
});
};
const findButtonLabel = () => wrapper.findComponent(GlButton);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findSelectedPath = () => wrapper.findComponent(GlTruncate);
- const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const findHiddenNamespaceInput = () => wrapper.find(`[name="${defaultProvide.inputName}`);
const findHiddenSelectedNamespaceInput = () =>
@@ -111,10 +118,6 @@ describe('NewProjectUrlSelect component', () => {
await waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the root url as a label', () => {
wrapper = mountComponent();
@@ -177,13 +180,11 @@ describe('NewProjectUrlSelect component', () => {
});
it('focuses on the input when the dropdown is opened', async () => {
- wrapper = mountComponent({ mountFn: mount });
-
- const spy = jest.spyOn(findInput().vm, 'focusInput');
+ wrapper = mountComponent();
await showDropdown();
- expect(spy).toHaveBeenCalledTimes(1);
+ expect(focusInputSpy).toHaveBeenCalledTimes(1);
});
it('renders expected dropdown items', async () => {
@@ -246,7 +247,7 @@ describe('NewProjectUrlSelect component', () => {
eventHub.$emit('select-template', getIdFromGraphQLId(id), fullPath);
});
- it('filters the dropdown items to the selected group and children', async () => {
+ it('filters the dropdown items to the selected group and children', () => {
const listItems = wrapper.findAll('li');
expect(listItems).toHaveLength(3);
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 fc51825f15b..61bcd44724c 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
@@ -8,20 +8,20 @@ exports[`CiCdAnalyticsAreaChart matches the snapshot 1`] = `
Some title
</p>
- <div>
- <glareachart-stub
- annotations=""
- data="[object Object],[object Object]"
- height="300"
- legendaveragetext="Avg"
- legendcurrenttext="Current"
- legendlayout="inline"
- legendmaxtext="Max"
- legendmintext="Min"
- option="[object Object]"
- thresholds=""
- width="0"
- />
- </div>
+ <glareachart-stub
+ annotations=""
+ data="[object Object],[object Object]"
+ height="300"
+ legendaveragetext="Avg"
+ legendcurrenttext="Current"
+ legendlayout="inline"
+ legendmaxtext="Max"
+ legendmintext="Min"
+ legendseriesinfo=""
+ option="[object Object]"
+ responsive=""
+ thresholds=""
+ width="auto"
+ />
</div>
`;
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index d8876349c5e..94f421239da 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -49,10 +49,6 @@ describe('ProjectsPipelinesChartsApp', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findAllGlTabs = () => wrapper.findAllComponents(GlTab);
const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js
index 2b523467379..5fc121b5c9f 100644
--- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_area_chart_spec.js
@@ -28,11 +28,6 @@ describe('CiCdAnalyticsAreaChart', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
index cf28eda5349..38760a724ff 100644
--- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
@@ -47,13 +47,6 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
},
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findMetricsSlot = () => wrapper.findByTestId('metrics-slot');
const findSegmentedControl = () => wrapper.findComponent(SegmentedControlButtonGroup);
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
index 8fb59f38ee1..ab2a12219e5 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -37,11 +37,6 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
await waitForPromises();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('overall statistics', () => {
it('displays the statistics list', () => {
const list = wrapper.findComponent(StatisticsList);
diff --git a/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js b/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js
index 57a864cb2c4..24dbc628ce6 100644
--- a/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/statistics_list_spec.js
@@ -21,10 +21,6 @@ describe('StatisticsList', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the counts data with labels', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/projects/prune_unreachable_objects_button_spec.js b/spec/frontend/projects/prune_unreachable_objects_button_spec.js
index b345f264ca7..012b19ea3d3 100644
--- a/spec/frontend/projects/prune_unreachable_objects_button_spec.js
+++ b/spec/frontend/projects/prune_unreachable_objects_button_spec.js
@@ -22,16 +22,11 @@ describe('Project remove modal', () => {
wrapper = shallowMountExtended(PruneObjectsButton, {
propsData: defaultProps,
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-modal'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('intialized', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js
index 11f219c1f90..6d3317a5f78 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/branch_dropdown_spec.js
@@ -8,10 +8,10 @@ import BranchDropdown, {
import createMockApollo from 'helpers/mock_apollo_helper';
import branchesQuery from '~/projects/settings/branch_rules/queries/branches.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Branch dropdown', () => {
let wrapper;
@@ -46,10 +46,6 @@ describe('Branch dropdown', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a GlDropdown component with the correct props', () => {
expect(findGlDropdown().props()).toMatchObject({ text: value });
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js
index 21e63fdb24d..e9982872e03 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/index_spec.js
@@ -24,10 +24,6 @@ describe('Edit branch rule', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('gets the branch param from url', () => {
expect(getParameterByName).toHaveBeenCalledWith('branch');
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js
index ee90ff8318f..14edaf31a1f 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/index_spec.js
@@ -26,10 +26,6 @@ describe('Branch Protections', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a heading', () => {
expect(findHeading().text()).toBe(i18n.protections);
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js
index b5fdc46d600..ca561ef87ec 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/merge_protections_spec.js
@@ -24,10 +24,6 @@ describe('Merge Protections', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a form group with the correct label', () => {
expect(findFormGroup().text()).toContain(i18n.allowedToMerge);
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js
index 60bb7a51dcb..82998640f17 100644
--- a/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/edit/protections/push_protections_spec.js
@@ -24,10 +24,6 @@ describe('Push Protections', () => {
beforeEach(() => createComponent());
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a form group with the correct label', () => {
expect(findFormGroup().attributes('label')).toBe(i18n.allowedToPush);
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index 714e0df596e..077995ab6e4 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -9,6 +9,10 @@ import Protection from '~/projects/settings/branch_rules/components/view/protect
import {
I18N,
ALL_BRANCHES_WILDCARD,
+ REQUIRED_ICON,
+ NOT_REQUIRED_ICON,
+ REQUIRED_ICON_CLASS,
+ NOT_REQUIRED_ICON_CLASS,
} from '~/projects/settings/branch_rules/components/view/constants';
import branchRulesQuery from 'ee_else_ce/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
import { sprintf } from '~/locale';
@@ -19,7 +23,7 @@ import {
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
- mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=main'),
+ mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=%5Emain%24'),
joinPaths: jest.fn(),
}));
@@ -39,12 +43,13 @@ describe('View branch rules', () => {
let fakeApollo;
const projectPath = 'test/testing';
const protectedBranchesPath = 'protected/branches';
- const branchProtectionsMockRequestHandler = jest
- .fn()
- .mockResolvedValue(branchProtectionsMockResponse);
+ const branchProtectionsMockRequestHandler = (response = branchProtectionsMockResponse) =>
+ jest.fn().mockResolvedValue(response);
- const createComponent = async () => {
- fakeApollo = createMockApollo([[branchRulesQuery, branchProtectionsMockRequestHandler]]);
+ const createComponent = async (mockResponse) => {
+ fakeApollo = createMockApollo([
+ [branchRulesQuery, branchProtectionsMockRequestHandler(mockResponse)],
+ ]);
wrapper = shallowMountExtended(RuleView, {
apolloProvider: fakeApollo,
@@ -57,13 +62,13 @@ describe('View branch rules', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
const findBranchName = () => wrapper.findByTestId('branch');
const findBranchTitle = () => wrapper.findByTestId('branch-title');
const findBranchProtectionTitle = () => wrapper.findByText(I18N.protectBranchTitle);
const findBranchProtections = () => wrapper.findAllComponents(Protection);
- const findForcePushTitle = () => wrapper.findByText(I18N.allowForcePushDescription);
+ const findForcePushIcon = () => wrapper.findByTestId('force-push-icon');
+ const findForcePushTitle = (title) => wrapper.findByText(title);
+ const findForcePushDescription = () => wrapper.findByText(I18N.forcePushDescription);
const findApprovalsTitle = () => wrapper.findByText(I18N.approvalsTitle);
const findStatusChecksTitle = () => wrapper.findByText(I18N.statusChecksTitle);
const findMatchingBranchesLink = () =>
@@ -94,9 +99,12 @@ describe('View branch rules', () => {
});
it('renders matching branches link', () => {
+ const mergeUrlParams = jest.spyOn(util, 'mergeUrlParams');
const matchingBranchesLink = findMatchingBranchesLink();
+
+ expect(mergeUrlParams).toHaveBeenCalledWith({ state: 'all', search: `^main$` }, '');
expect(matchingBranchesLink.exists()).toBe(true);
- expect(matchingBranchesLink.attributes().href).toBe('/branches?state=all&search=main');
+ expect(matchingBranchesLink.attributes().href).toBe('/branches?state=all&search=%5Emain%24');
});
it('renders a branch protection title', () => {
@@ -123,9 +131,23 @@ describe('View branch rules', () => {
});
});
- it('renders force push protection', () => {
- expect(findForcePushTitle().exists()).toBe(true);
- });
+ it.each`
+ allowForcePush | iconName | iconClass | title
+ ${true} | ${REQUIRED_ICON} | ${REQUIRED_ICON_CLASS} | ${I18N.allowForcePushTitle}
+ ${false} | ${NOT_REQUIRED_ICON} | ${NOT_REQUIRED_ICON_CLASS} | ${I18N.doesNotAllowForcePushTitle}
+ `(
+ 'renders force push section with the correct icon, title and description',
+ async ({ allowForcePush, iconName, iconClass, title }) => {
+ const mockResponse = branchProtectionsMockResponse;
+ mockResponse.data.project.branchRules.nodes[0].branchProtection.allowForcePush = allowForcePush;
+ await createComponent(mockResponse);
+
+ expect(findForcePushIcon().props('name')).toBe(iconName);
+ expect(findForcePushIcon().attributes('class')).toBe(iconClass);
+ expect(findForcePushTitle(title).exists()).toBe(true);
+ expect(findForcePushDescription().exists()).toBe(true);
+ },
+ );
it('renders a branch protection component for merge rules', () => {
expect(findBranchProtections().at(1).props()).toMatchObject({
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
index a98b156f94e..1bfd04e10a1 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_row_spec.js
@@ -18,8 +18,6 @@ describe('Branch rule protection row', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
const findTitle = () => wrapper.findByText(protectionRowPropsMock.title);
const findAvatarsInline = () => wrapper.findComponent(GlAvatarsInline);
const findAvatarLinks = () => wrapper.findAllComponents(GlAvatarLink);
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
index caf967b4257..f10d8d6d770 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/protection_spec.js
@@ -16,8 +16,6 @@ describe('Branch rule protection', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
const findCard = () => wrapper.findComponent(GlCard);
const findHeader = () => wrapper.findByText(protectionPropsMock.header);
const findLink = () => wrapper.findComponent(GlLink);
diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
index ca9a72663d2..9baea5c5517 100644
--- a/spec/frontend/projects/settings/components/default_branch_selector_spec.js
+++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
@@ -19,10 +19,6 @@ describe('projects/settings/components/default_branch_selector', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
buildWrapper();
});
@@ -32,7 +28,6 @@ describe('projects/settings/components/default_branch_selector', () => {
value: persistedDefaultBranch,
enabledRefTypes: [REF_TYPE_BRANCHES],
projectId,
- refType: null,
state: true,
toggleButtonClass: null,
translations: {
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 26297d0c3ff..f3e536de703 100644
--- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -89,15 +89,12 @@ describe('Access Level Dropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggleLabel = () => findDropdown().props('text');
const findAllDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem);
const findAllDropdownHeaders = () => findDropdown().findAllComponents(GlDropdownSectionHeader);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDeployKeyDropdownItem = () => wrapper.findByTestId('deploy_key-dropdown-item');
const findDropdownItemWithText = (items, text) =>
items.filter((item) => item.text().includes(text)).at(0);
@@ -142,6 +139,21 @@ describe('Access Level Dropdown', () => {
it('renders dropdown item for each access level type', () => {
expect(findAllDropdownItems()).toHaveLength(12);
});
+
+ it.each`
+ accessLevel | shouldRenderDeployKeyItems
+ ${ACCESS_LEVELS.PUSH} | ${true}
+ ${ACCESS_LEVELS.CREATE} | ${true}
+ ${ACCESS_LEVELS.MERGE} | ${false}
+ `(
+ 'conditionally renders deploy keys based on access levels',
+ async ({ accessLevel, shouldRenderDeployKeyItems }) => {
+ createComponent({ accessLevel });
+ await waitForPromises();
+
+ expect(findDeployKeyDropdownItem().exists()).toBe(shouldRenderDeployKeyItems);
+ },
+ );
});
describe('toggleLabel', () => {
diff --git a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
index f82ad80135e..f28bc13895e 100644
--- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
+++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
@@ -1,7 +1,7 @@
-import { GlAlert, GlToggle, GlTooltip } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlToggle } from '@gitlab/ui';
import MockAxiosAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
@@ -9,14 +9,14 @@ import SharedRunnersToggleComponent from '~/projects/settings/components/shared_
const TEST_UPDATE_PATH = '/test/update_shared_runners';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('projects/settings/components/shared_runners', () => {
let wrapper;
let mockAxios;
const createComponent = (props = {}) => {
- wrapper = shallowMount(SharedRunnersToggleComponent, {
+ wrapper = shallowMountExtended(SharedRunnersToggleComponent, {
propsData: {
isEnabled: false,
isDisabledAndUnoverridable: false,
@@ -28,9 +28,9 @@ describe('projects/settings/components/shared_runners', () => {
});
};
- const findErrorAlert = () => wrapper.findComponent(GlAlert);
+ const findErrorAlert = () => wrapper.findByTestId('error-alert');
+ const findUnoverridableAlert = () => wrapper.findByTestId('unoverridable-alert');
const findSharedRunnersToggle = () => wrapper.findComponent(GlToggle);
- const findToggleTooltip = () => wrapper.findComponent(GlTooltip);
const getToggleValue = () => findSharedRunnersToggle().props('value');
const isToggleLoading = () => findSharedRunnersToggle().props('isLoading');
const isToggleDisabled = () => findSharedRunnersToggle().props('disabled');
@@ -41,8 +41,6 @@ describe('projects/settings/components/shared_runners', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
mockAxios.restore();
});
@@ -57,8 +55,8 @@ describe('projects/settings/components/shared_runners', () => {
expect(isToggleDisabled()).toBe(true);
});
- it('tooltip should exist explaining why the toggle is disabled', () => {
- expect(findToggleTooltip().exists()).toBe(true);
+ it('alert should exist explaining why the toggle is disabled', () => {
+ expect(findUnoverridableAlert().exists()).toBe(true);
});
});
@@ -74,7 +72,7 @@ describe('projects/settings/components/shared_runners', () => {
it('loading icon, error message, and tooltip should not exist', () => {
expect(isToggleLoading()).toBe(false);
expect(findErrorAlert().exists()).toBe(false);
- expect(findToggleTooltip().exists()).toBe(false);
+ expect(findUnoverridableAlert().exists()).toBe(false);
});
describe('with shared runners DISABLED', () => {
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index e091f3e25c3..e12938c3bab 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -31,10 +31,6 @@ describe('Transfer project form', () => {
const findTransferLocations = () => wrapper.findComponent(TransferLocations);
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the namespace selector and passes `groupTransferLocationsApiMethod` prop', () => {
createComponent();
@@ -53,7 +49,7 @@ describe('Transfer project form', () => {
it('disables the confirm button by default', () => {
createComponent();
- expect(findConfirmDanger().attributes('disabled')).toBe('true');
+ expect(findConfirmDanger().attributes('disabled')).toBeDefined();
});
describe('with a selected namespace', () => {
@@ -68,17 +64,17 @@ describe('Transfer project form', () => {
expect(findTransferLocations().props('value')).toEqual(selectedItem);
});
- it('emits the `selectTransferLocation` event when a namespace is selected', async () => {
+ it('emits the `selectTransferLocation` event when a namespace is selected', () => {
const args = [selectedItem.id];
expect(wrapper.emitted('selectTransferLocation')).toEqual([args]);
});
- it('enables the confirm button', async () => {
+ it('enables the confirm button', () => {
expect(findConfirmDanger().attributes('disabled')).toBeUndefined();
});
- it('clicking the confirm button emits the `confirm` event', async () => {
+ it('clicking the confirm button emits the `confirm` event', () => {
findConfirmDanger().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toBeDefined();
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index 56b39f04580..dd534bec25d 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -7,7 +7,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import BranchRules from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import {
branchRulesMockResponse,
appProvideMock,
@@ -22,7 +22,7 @@ import { expandSection } from '~/settings_panels';
import { scrollToElement } from '~/lib/utils/common_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/settings_panels');
jest.mock('~/lib/utils/common_utils');
@@ -41,7 +41,7 @@ describe('Branch rules app', () => {
apolloProvider: fakeApollo,
provide: appProvideMock,
stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) },
- directives: { GlModal: createMockDirective() },
+ directives: { GlModal: createMockDirective('gl-modal') },
});
await waitForPromises();
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
index 8d0fd390e35..8bea84f4429 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -71,8 +71,10 @@ describe('Branch rule', () => {
});
it('renders a detail button with the correct href', () => {
+ const encodedBranchName = encodeURIComponent(branchRulePropsMock.name);
+
expect(findDetailsButton().attributes('href')).toBe(
- `${branchRuleProvideMock.branchRulesPath}?branch=${branchRulePropsMock.name}`,
+ `${branchRuleProvideMock.branchRulesPath}?branch=${encodedBranchName}`,
);
});
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index de7f6c8b88d..d169397241d 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -74,7 +74,7 @@ export const branchRuleProvideMock = {
};
export const branchRulePropsMock = {
- name: 'main',
+ name: 'branch-with-$speci@l-#-chars',
isDefault: true,
matchingBranchesCount: 1,
branchProtection: {
diff --git a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
index 8b8e7d1454d..b2c03352cdc 100644
--- a/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
+++ b/spec/frontend/projects/settings/topics/components/topics_token_selector_spec.js
@@ -64,7 +64,6 @@ describe('TopicsTokenSelector', () => {
});
afterEach(() => {
- wrapper.destroy();
div.remove();
input.remove();
});
@@ -84,7 +83,7 @@ describe('TopicsTokenSelector', () => {
});
});
- it('passes topic title to the avatar', async () => {
+ it('passes topic title to the avatar', () => {
createComponent();
const avatars = findAllAvatars();
diff --git a/spec/frontend/projects/settings/utils_spec.js b/spec/frontend/projects/settings/utils_spec.js
index 319aa4000b5..d85f43778b1 100644
--- a/spec/frontend/projects/settings/utils_spec.js
+++ b/spec/frontend/projects/settings/utils_spec.js
@@ -1,4 +1,5 @@
-import { getAccessLevels } from '~/projects/settings/utils';
+import { getAccessLevels, generateRefDestinationPath } from '~/projects/settings/utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { pushAccessLevelsMockResponse, pushAccessLevelsMockResult } from './mock_data';
describe('Utils', () => {
@@ -8,4 +9,25 @@ describe('Utils', () => {
expect(pushAccessLevels).toEqual(pushAccessLevelsMockResult);
});
});
+
+ describe('generateRefDestinationPath', () => {
+ const projectRootPath = 'http://test.host/root/Project1';
+ const settingsCi = '-/settings/ci_cd';
+
+ it.each`
+ currentPath | selectedRef | result
+ ${`${projectRootPath}`} | ${undefined} | ${`${projectRootPath}`}
+ ${`${projectRootPath}`} | ${'test'} | ${`${projectRootPath}`}
+ ${`${projectRootPath}/${settingsCi}`} | ${'test'} | ${`${projectRootPath}/${settingsCi}?ref=test`}
+ ${`${projectRootPath}/${settingsCi}`} | ${'branch-hyphen'} | ${`${projectRootPath}/${settingsCi}?ref=branch-hyphen`}
+ ${`${projectRootPath}/${settingsCi}`} | ${'test/branch'} | ${`${projectRootPath}/${settingsCi}?ref=test%2Fbranch`}
+ ${`${projectRootPath}/${settingsCi}`} | ${'test/branch-hyphen'} | ${`${projectRootPath}/${settingsCi}?ref=test%2Fbranch-hyphen`}
+ `(
+ 'generates the correct destination path for the `$selectedRef` ref and current url $currentPath by outputting $result',
+ ({ currentPath, selectedRef, result }) => {
+ setWindowLocation(currentPath);
+ expect(generateRefDestinationPath(selectedRef)).toBe(result);
+ },
+ );
+ });
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 5fc9f9ba629..86e4e88e3cf 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -41,7 +41,6 @@ describe('ServiceDeskRoot', () => {
afterEach(() => {
axiosMock.restore();
- wrapper.destroy();
if (spy) {
spy.mockRestore();
}
@@ -79,7 +78,7 @@ describe('ServiceDeskRoot', () => {
const alertBodyLink = alertEl.findComponent(GlLink);
expect(alertBodyLink.exists()).toBe(true);
expect(alertBodyLink.attributes('href')).toBe(
- '/help/user/project/service_desk.html#using-a-custom-email-address',
+ '/help/user/project/service_desk.html#use-a-custom-email-address',
);
expect(alertBodyLink.text()).toBe('How do I create a custom email address?');
});
@@ -148,7 +147,7 @@ describe('ServiceDeskRoot', () => {
await waitForPromises();
});
- it('sends a request to update template', async () => {
+ it('sends a request to update template', () => {
expect(spy).toHaveBeenCalledWith(provideData.endpoint, {
issue_template_key: 'Bug',
outgoing_name: 'GitLab Support Bot',
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index f9762491507..84eafc3d0f3 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -27,12 +27,6 @@ describe('ServiceDeskSetting', () => {
}),
);
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('when isEnabled=true', () => {
describe('only isEnabled', () => {
describe('as project admin', () => {
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
index 6adcfbe8157..7090db5cad7 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_template_dropdown_spec.js
@@ -19,12 +19,6 @@ describe('ServiceDeskTemplateDropdown', () => {
}),
);
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('templates dropdown', () => {
it('renders a dropdown to choose a template', () => {
wrapper = createComponent();
diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
index 6576ce70d60..1d0faebbcb2 100644
--- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
+++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
@@ -41,10 +41,6 @@ describe('TerraformNotificationBanner', () => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when user has already dismissed the banner', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index 3852f2678b7..706f932aa8d 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import prometheusIntegration from 'test_fixtures/integrations/prometheus/prometheus_integration.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PANEL_STATE from '~/prometheus_metrics/constants';
@@ -7,7 +8,6 @@ import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import { metrics1 as metrics } from './mock_data';
describe('PrometheusMetrics', () => {
- const FIXTURE = 'integrations/prometheus/prometheus_integration.html';
const customMetricsEndpoint =
'http://test.host/frontend-fixtures/integrations-project/prometheus/metrics';
let mock;
@@ -17,7 +17,7 @@ describe('PrometheusMetrics', () => {
mock.onGet(customMetricsEndpoint).reply(HTTP_STATUS_OK, {
metrics,
});
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(prometheusIntegration);
});
afterEach(() => {
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index 45654d6a2eb..64cf69b7f5b 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import prometheusIntegration from 'test_fixtures/integrations/prometheus/prometheus_integration.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -8,10 +9,8 @@ import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import { metrics2 as metrics, missingVarMetrics } from './mock_data';
describe('PrometheusMetrics', () => {
- const FIXTURE = 'integrations/prometheus/prometheus_integration.html';
-
beforeEach(() => {
- loadHTMLFixture(FIXTURE);
+ setHTMLFixture(prometheusIntegration);
});
describe('constructor', () => {
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index b4029d94980..e1966908452 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -2,12 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
-import { createAlert } from '~/flash';
+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 ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_URL = `${TEST_HOST}/url`;
const FORCE_PUSH_TOGGLE_TESTID = 'force-push-toggle';
@@ -115,7 +115,7 @@ describe('ProtectedBranchEdit', () => {
});
describe('when clicked', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mock
.onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
.replyOnce(HTTP_STATUS_OK, {});
@@ -149,7 +149,7 @@ describe('ProtectedBranchEdit', () => {
toggle.click();
});
- it('flashes error', async () => {
+ it('alerts error', async () => {
await axios.waitForAll();
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/read_more_spec.js b/spec/frontend/read_more_spec.js
index 9eddc50d50a..5f7bd32e231 100644
--- a/spec/frontend/read_more_spec.js
+++ b/spec/frontend/read_more_spec.js
@@ -1,9 +1,8 @@
-import { loadHTMLFixture, resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import htmlProjectsOverview from 'test_fixtures/projects/overview.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initReadMore from '~/read_more';
describe('Read more click-to-expand functionality', () => {
- const fixtureName = 'projects/overview.html';
-
const findTrigger = () => document.querySelector('.js-read-more-trigger');
afterEach(() => {
@@ -12,7 +11,7 @@ describe('Read more click-to-expand functionality', () => {
describe('expands target element', () => {
beforeEach(() => {
- loadHTMLFixture(fixtureName);
+ setHTMLFixture(htmlProjectsOverview);
});
it('adds "is-expanded" class to target element', () => {
@@ -42,7 +41,7 @@ describe('Read more click-to-expand functionality', () => {
nestedElement.click();
});
- it('removes the trigger element', async () => {
+ it('removes the trigger element', () => {
expect(findTrigger()).toBe(null);
});
});
diff --git a/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap b/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap
deleted file mode 100644
index 5053778369e..00000000000
--- a/spec/frontend/ref/components/__snapshots__/ref_selector_spec.js.snap
+++ /dev/null
@@ -1,80 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Ref selector component footer slot passes the expected slot props 1`] = `
-Object {
- "isLoading": false,
- "matches": Object {
- "branches": Object {
- "error": null,
- "list": Array [
- Object {
- "default": false,
- "name": "add_images_and_changes",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "conflict-contains-conflict-markers",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "deleted-image-test",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "diff-files-image-to-symlink",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "diff-files-symlink-to-image",
- "value": undefined,
- },
- Object {
- "default": false,
- "name": "markdown",
- "value": undefined,
- },
- Object {
- "default": true,
- "name": "master",
- "value": undefined,
- },
- ],
- "totalCount": 123,
- },
- "commits": Object {
- "error": null,
- "list": Array [
- Object {
- "name": "b83d6e39",
- "subtitle": "Merge branch 'branch-merged' into 'master'",
- "value": "b83d6e391c22777fca1ed3012fce84f633d7fed0",
- },
- ],
- "totalCount": 1,
- },
- "tags": Object {
- "error": null,
- "list": Array [
- Object {
- "name": "v1.1.1",
- "value": undefined,
- },
- Object {
- "name": "v1.1.0",
- "value": undefined,
- },
- Object {
- "name": "v1.0.0",
- "value": undefined,
- },
- ],
- "totalCount": 456,
- },
- },
- "query": "abcd1234",
-}
-`;
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 40d3a291074..290cde29866 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -4,9 +4,9 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { merge, last } from 'lodash';
import Vuex from 'vuex';
+import tags from 'test_fixtures/api/tags/tags.json';
import commit from 'test_fixtures/api/commits/commit.json';
import branches from 'test_fixtures/api/branches/branches.json';
-import tags from 'test_fixtures/api/tags/tags.json';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import {
@@ -22,8 +22,6 @@ import {
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
- BRANCH_REF_TYPE,
- TAG_REF_TYPE,
} from '~/ref/constants';
import createStore from '~/ref/stores/';
@@ -33,6 +31,9 @@ describe('Ref selector component', () => {
const fixtures = { branches, tags, commit };
const projectId = '8';
+ const totalBranchesCount = 123;
+ const totalTagsCount = 456;
+ const queryParams = { sort: 'updated_desc' };
let wrapper;
let branchesApiCallSpy;
@@ -69,10 +70,14 @@ describe('Ref selector component', () => {
branchesApiCallSpy = jest
.fn()
- .mockReturnValue([HTTP_STATUS_OK, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
+ .mockReturnValue([
+ HTTP_STATUS_OK,
+ fixtures.branches,
+ { [X_TOTAL_HEADER]: totalBranchesCount },
+ ]);
tagsApiCallSpy = jest
.fn()
- .mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
+ .mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: totalTagsCount }]);
commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, fixtures.commit]);
requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy };
@@ -316,7 +321,7 @@ describe('Ref selector component', () => {
describe('branches', () => {
describe('when the branches search returns results', () => {
beforeEach(() => {
- createComponent({}, { refType: BRANCH_REF_TYPE, useSymbolicRefNames: true });
+ createComponent({}, { useSymbolicRefNames: true });
return waitForRequests();
});
@@ -379,7 +384,7 @@ describe('Ref selector component', () => {
describe('tags', () => {
describe('when the tags search returns results', () => {
beforeEach(() => {
- createComponent({}, { refType: TAG_REF_TYPE, useSymbolicRefNames: true });
+ createComponent({}, { useSymbolicRefNames: true });
return waitForRequests();
});
@@ -690,7 +695,67 @@ describe('Ref selector component', () => {
// is updated. For the sake of this test, we'll just test the last call, which
// represents the final state of the slot props.
const lastCallProps = last(createFooter.mock.calls)[0];
- expect(lastCallProps).toMatchSnapshot();
+ expect(lastCallProps.isLoading).toBe(false);
+ expect(lastCallProps.query).toBe('abcd1234');
+
+ const branchesList = fixtures.branches.map((branch) => {
+ return {
+ default: branch.default,
+ name: branch.name,
+ };
+ });
+
+ const commitsList = [
+ {
+ name: fixtures.commit.short_id,
+ subtitle: fixtures.commit.title,
+ value: fixtures.commit.id,
+ },
+ ];
+
+ const tagsList = fixtures.tags.map((tag) => {
+ return {
+ name: tag.name,
+ };
+ });
+
+ const expectedMatches = {
+ branches: {
+ list: branchesList,
+ totalCount: totalBranchesCount,
+ },
+ commits: {
+ list: commitsList,
+ totalCount: 1,
+ },
+ tags: {
+ list: tagsList,
+ totalCount: totalTagsCount,
+ },
+ };
+
+ expect(lastCallProps.matches).toMatchObject(expectedMatches);
+ });
+ });
+ describe('when queryParam prop is present', () => {
+ it('passes params to a branches API call', () => {
+ createComponent({ propsData: { queryParams } });
+
+ return waitForRequests().then(() => {
+ expect(branchesApiCallSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ params: { per_page: 20, search: '', sort: queryParams.sort } }),
+ );
+ });
+ });
+
+ it('does not pass params to tags API call', () => {
+ createComponent({ propsData: { queryParams } });
+
+ return waitForRequests().then(() => {
+ expect(tagsApiCallSpy).toHaveBeenCalledWith(
+ expect.objectContaining({ params: { per_page: 20, search: '' } }),
+ );
+ });
});
});
});
diff --git a/spec/frontend/ref/stores/actions_spec.js b/spec/frontend/ref/stores/actions_spec.js
index 099ce062a3a..c6aac8c9c98 100644
--- a/spec/frontend/ref/stores/actions_spec.js
+++ b/spec/frontend/ref/stores/actions_spec.js
@@ -52,6 +52,13 @@ describe('Ref selector Vuex store actions', () => {
});
});
+ describe('setParams', () => {
+ it(`commits ${types.SET_PARAMS} with the provided params`, () => {
+ const params = { sort: 'updated_asc' };
+ testAction(actions.setParams, params, state, [{ type: types.SET_PARAMS, payload: params }]);
+ });
+ });
+
describe('search', () => {
it(`commits ${types.SET_QUERY} with the new search query`, () => {
const query = 'hello';
diff --git a/spec/frontend/ref/stores/mutations_spec.js b/spec/frontend/ref/stores/mutations_spec.js
index 37eee18dc10..8f16317b751 100644
--- a/spec/frontend/ref/stores/mutations_spec.js
+++ b/spec/frontend/ref/stores/mutations_spec.js
@@ -34,6 +34,7 @@ describe('Ref selector Vuex store mutations', () => {
error: null,
},
},
+ params: null,
selectedRef: null,
requestCount: 0,
});
@@ -56,6 +57,15 @@ describe('Ref selector Vuex store mutations', () => {
});
});
+ describe(`${types.SET_PARAMS}`, () => {
+ it('sets the additional query params', () => {
+ const params = { sort: 'updated_desc' };
+ mutations[types.SET_PARAMS](state, params);
+
+ expect(state.params).toBe(params);
+ });
+ });
+
describe(`${types.SET_PROJECT_ID}`, () => {
it('updates the project ID', () => {
const newProjectId = '4';
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index 00fc521b716..79792a4a0ea 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -82,7 +82,6 @@ Object {
Object {
"__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-3",
- "external": true,
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
"name": "Image",
@@ -91,7 +90,6 @@ Object {
Object {
"__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-2",
- "external": true,
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
"name": "Package",
@@ -100,7 +98,6 @@ Object {
Object {
"__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-1",
- "external": false,
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
"name": "Runbook",
@@ -109,7 +106,6 @@ Object {
Object {
"__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/linux-amd64",
- "external": true,
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
"name": "linux-amd64 binaries",
@@ -306,7 +302,6 @@ Object {
Object {
"__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-3",
- "external": true,
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
"name": "Image",
@@ -315,7 +310,6 @@ Object {
Object {
"__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-2",
- "external": true,
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
"name": "Package",
@@ -324,7 +318,6 @@ Object {
Object {
"__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-1",
- "external": false,
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
"name": "Runbook",
@@ -333,7 +326,6 @@ Object {
Object {
"__typename": "ReleaseAssetLink",
"directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/linux-amd64",
- "external": true,
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
"name": "linux-amd64 binaries",
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index bd61e4537f9..69d8969f0ad 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -16,6 +16,7 @@ import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { ValidationResult } from '~/lib/utils/ref_validator';
const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.release;
const originalMilestones = originalRelease.milestones;
@@ -30,6 +31,8 @@ describe('Release edit/new component', () => {
let actions;
let getters;
let state;
+ let refActions;
+ let refState;
let mock;
const factory = async ({ featureFlags = {}, store: storeUpdates = {} } = {}) => {
@@ -58,8 +61,23 @@ describe('Release edit/new component', () => {
assets: {
links: [],
},
+ tagNameValidation: new ValidationResult(),
}),
formattedReleaseNotes: () => 'these notes are formatted',
+ isCreating: jest.fn(),
+ isSearching: jest.fn(),
+ isExistingTag: jest.fn(),
+ isNewTag: jest.fn(),
+ };
+
+ refState = {
+ matches: [],
+ };
+
+ refActions = {
+ setEnabledRefTypes: jest.fn(),
+ setProjectId: jest.fn(),
+ search: jest.fn(),
};
const store = new Vuex.Store(
@@ -72,6 +90,11 @@ describe('Release edit/new component', () => {
state,
getters,
},
+ ref: {
+ namespaced: true,
+ actions: refActions,
+ state: refState,
+ },
},
},
storeUpdates,
@@ -101,11 +124,6 @@ describe('Release edit/new component', () => {
release = convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse).data;
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findSubmitButton = () => wrapper.find('button[type=submit]');
const findForm = () => wrapper.find('form');
@@ -291,7 +309,7 @@ describe('Release edit/new component', () => {
});
it('renders the submit button as disabled', () => {
- expect(findSubmitButton().attributes('disabled')).toBe('disabled');
+ expect(findSubmitButton().attributes('disabled')).toBeDefined();
});
it('does not allow the form to be submitted', () => {
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index ef3bd5ca873..b8507dc5fb4 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -6,9 +6,8 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
-import { createAlert } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { historyPushState } from '~/lib/utils/common_utils';
-import { sprintf, __ } from '~/locale';
import ReleasesIndexApp from '~/releases/components/app_index.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
@@ -16,11 +15,11 @@ import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
import ReleasesSort from '~/releases/components/releases_sort.vue';
import { PAGE_SIZE, CREATED_ASC, DEFAULT_SORT } from '~/releases/constants';
-import { deleteReleaseSessionKey } from '~/releases/util';
+import { deleteReleaseSessionKey } from '~/releases/release_notification_service';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
let mockQueryParams;
jest.mock('~/lib/utils/common_utils', () => ({
@@ -114,7 +113,7 @@ describe('app_index.vue', () => {
const toDescription = (bool) => (bool ? 'does' : 'does not');
describe.each`
- description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | flashMessage | releaseCount | pagination
+ description | singleResponseFn | fullResponseFn | loadingIndicator | emptyState | alertMessage | releaseCount | pagination
${'both requests loading'} | ${getInProgressResponse} | ${getInProgressResponse} | ${true} | ${false} | ${false} | ${0} | ${false}
${'both requests failed'} | ${getErrorResponse} | ${getErrorResponse} | ${false} | ${false} | ${true} | ${0} | ${false}
${'both requests loaded'} | ${getSingleRequestLoadedResponse} | ${getFullRequestLoadedResponse} | ${false} | ${false} | ${false} | ${2} | ${true}
@@ -134,7 +133,7 @@ describe('app_index.vue', () => {
fullResponseFn,
loadingIndicator,
emptyState,
- flashMessage,
+ alertMessage,
releaseCount,
pagination,
}) => {
@@ -154,9 +153,9 @@ describe('app_index.vue', () => {
expect(findEmptyState().exists()).toBe(emptyState);
});
- it(`${toDescription(flashMessage)} show a flash message`, async () => {
+ it(`${toDescription(alertMessage)} show a flash message`, async () => {
await waitForPromises();
- if (flashMessage) {
+ if (alertMessage) {
expect(createAlert).toHaveBeenCalledWith({
message: ReleasesIndexApp.i18n.errorMessage,
captureError: true,
@@ -412,15 +411,15 @@ describe('app_index.vue', () => {
await createComponent();
});
- it('shows a toast', async () => {
- expect(toast).toHaveBeenCalledWith(
- sprintf(__('Release %{release} has been successfully deleted.'), {
- release,
- }),
- );
+ it('shows a toast', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Release ${release} has been successfully deleted.`,
+ variant: VARIANT_SUCCESS,
+ });
});
- it('clears session storage', async () => {
+ it('clears session storage', () => {
expect(window.sessionStorage.getItem(key)).toBe(null);
});
});
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index efe72e8000a..942280cb6a2 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo';
import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { popCreateReleaseNotification } from '~/releases/release_notification_service';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphql';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/releases/release_notification_service');
Vue.use(VueApollo);
@@ -33,11 +33,6 @@ describe('Release show component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findLoadingSkeleton = () => wrapper.findComponent(ReleaseSkeletonLoader);
const findReleaseBlock = () => wrapper.findComponent(ReleaseBlock);
@@ -54,13 +49,13 @@ describe('Release show component', () => {
};
const expectNoFlash = () => {
- it('does not show a flash message', () => {
+ it('does not show an alert message', () => {
expect(createAlert).not.toHaveBeenCalled();
});
};
const expectFlashWithMessage = (message) => {
- it(`shows a flash message that reads "${message}"`, () => {
+ it(`shows an alert message that reads "${message}"`, () => {
expect(createAlert).toHaveBeenCalledWith({
message,
captureError: true,
@@ -152,7 +147,7 @@ describe('Release show component', () => {
beforeEach(async () => {
// As we return a release as `null`, Apollo also throws an error to the console
// about the missing field. We need to suppress console.error in order to check
- // that flash message was called
+ // that alert message was called
// eslint-disable-next-line no-console
console.error = jest.fn();
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index b1e9d8d1256..8eee9acd808 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -60,11 +60,6 @@ describe('Release edit component', () => {
release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with a basic store state', () => {
beforeEach(() => {
factory();
diff --git a/spec/frontend/releases/components/confirm_delete_modal_spec.js b/spec/frontend/releases/components/confirm_delete_modal_spec.js
index f7c526c1ced..b4699302779 100644
--- a/spec/frontend/releases/components/confirm_delete_modal_spec.js
+++ b/spec/frontend/releases/components/confirm_delete_modal_spec.js
@@ -42,10 +42,6 @@ describe('~/releases/components/confirm_delete_modal.vue', () => {
factory();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('button', () => {
it('should open the modal on click', async () => {
await wrapper.findByRole('button', { name: 'Delete' }).trigger('click');
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 69443cb7a11..42eac31e5ac 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -27,10 +27,6 @@ describe('Evidence Block', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the evidence icon', () => {
expect(wrapper.findComponent(GlIcon).props('name')).toBe('review-list');
});
diff --git a/spec/frontend/releases/components/issuable_stats_spec.js b/spec/frontend/releases/components/issuable_stats_spec.js
index 3ac75e138ee..c8cdf9cb951 100644
--- a/spec/frontend/releases/components/issuable_stats_spec.js
+++ b/spec/frontend/releases/components/issuable_stats_spec.js
@@ -34,11 +34,6 @@ describe('~/releases/components/issuable_stats.vue', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches snapshot', () => {
createComponent();
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index 6d53bf5a49e..8332e307ce9 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -124,13 +124,12 @@ describe('Release block assets', () => {
});
describe('links', () => {
- const containsExternalSourceIndicator = () =>
- wrapper.find('[data-testid="external-link-indicator"]').exists();
+ const findAllExternalIcons = () => wrapper.findAll('[data-testid="external-link-indicator"]');
beforeEach(() => createComponent(defaultProps));
- it('renders with an external source indicator (except for sections with no title)', () => {
- expect(containsExternalSourceIndicator()).toBe(true);
+ it('renders with an external source indicator', () => {
+ expect(findAllExternalIcons()).toHaveLength(defaultProps.assets.count);
});
});
});
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index 19b41d05a44..12e3807c9fa 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -33,11 +33,6 @@ describe('Release block footer', () => {
release = cloneDeep(originalRelease);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const commitInfoSection = () => wrapper.find('.js-commit-info');
const commitInfoSectionLink = () => commitInfoSection().findComponent(GlLink);
const tagInfoSection = () => wrapper.find('.js-tag-info');
diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js
index fc421776d60..dd39a1bce53 100644
--- a/spec/frontend/releases/components/release_block_header_spec.js
+++ b/spec/frontend/releases/components/release_block_header_spec.js
@@ -25,10 +25,6 @@ describe('Release block header', () => {
release = convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findHeader = () => wrapper.find('h2');
const findHeaderLink = () => findHeader().findComponent(GlLink);
const findEditButton = () => wrapper.find('.js-edit-button');
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 541d487091c..b8030ae1fd2 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -25,11 +25,6 @@ describe('Release block milestone info', () => {
milestones = convertObjectPropsToCamelCase(originalMilestones, { deep: true });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const milestoneProgressBarContainer = () => wrapper.find('.js-milestone-progress-bar-container');
const milestoneListContainer = () => wrapper.find('.js-milestone-list-container');
const issuesContainer = () => wrapper.find('[data-testid="issue-stats"]');
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index f1b8554fbc3..3355b5ab2c3 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -39,10 +39,6 @@ describe('Release block', () => {
release = convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse).data;
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with default props', () => {
beforeEach(() => factory(release));
diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js
index 59be808c802..923d84ae2b3 100644
--- a/spec/frontend/releases/components/releases_pagination_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_spec.js
@@ -29,10 +29,6 @@ describe('releases_pagination.vue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const singlePageInfo = {
hasPreviousPage: false,
hasNextPage: false,
diff --git a/spec/frontend/releases/components/releases_sort_spec.js b/spec/frontend/releases/components/releases_sort_spec.js
index c6e1846d252..76907b4b8bb 100644
--- a/spec/frontend/releases/components/releases_sort_spec.js
+++ b/spec/frontend/releases/components/releases_sort_spec.js
@@ -1,5 +1,6 @@
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { assertProps } from 'helpers/assert_props';
import ReleasesSort from '~/releases/components/releases_sort.vue';
import { RELEASED_AT_ASC, RELEASED_AT_DESC, CREATED_ASC, CREATED_DESC } from '~/releases/constants';
@@ -17,10 +18,6 @@ describe('releases_sort.vue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSorting = () => wrapper.findComponent(GlSorting);
const findSortingItems = () => wrapper.findAllComponents(GlSortingItem);
const findReleasedDateItem = () =>
@@ -96,7 +93,7 @@ describe('releases_sort.vue', () => {
describe('prop validation', () => {
it('validates that the `value` prop is one of the expected sort strings', () => {
expect(() => {
- createComponent('not a valid value');
+ assertProps(ReleasesSort, { value: 'not a valid value' });
}).toThrow('Invalid prop: custom validator check failed');
});
});
diff --git a/spec/frontend/releases/components/tag_create_spec.js b/spec/frontend/releases/components/tag_create_spec.js
new file mode 100644
index 00000000000..0df2483bcf2
--- /dev/null
+++ b/spec/frontend/releases/components/tag_create_spec.js
@@ -0,0 +1,107 @@
+import { GlButton, GlFormInput, GlFormTextarea } from '@gitlab/ui';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { __, s__ } from '~/locale';
+import TagCreate from '~/releases/components/tag_create.vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import createStore from '~/releases/stores';
+import createEditNewModule from '~/releases/stores/modules/edit_new';
+import { createRefModule } from '~/ref/stores';
+
+const TEST_PROJECT_ID = '1234';
+
+const VALUE = 'new-tag';
+
+describe('releases/components/tag_create', () => {
+ let store;
+ let wrapper;
+ let mock;
+
+ const createWrapper = () => {
+ wrapper = shallowMount(TagCreate, {
+ store,
+ propsData: { value: VALUE },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ store = createStore({
+ modules: {
+ editNew: createEditNewModule({
+ projectId: TEST_PROJECT_ID,
+ }),
+ ref: createRefModule(),
+ },
+ });
+ store.state.editNew.release = {
+ tagMessage: 'test',
+ };
+ store.state.editNew.createFrom = 'v1';
+ createWrapper();
+ });
+
+ afterEach(() => mock.restore());
+
+ const findTagInput = () => wrapper.findComponent(GlFormInput);
+ const findTagRef = () => wrapper.findComponent(RefSelector);
+ const findTagMessage = () => wrapper.findComponent(GlFormTextarea);
+ const findSave = () => wrapper.findAllComponents(GlButton).at(-2);
+ const findCancel = () => wrapper.findAllComponents(GlButton).at(-1);
+
+ it('initializes the input with value prop', () => {
+ expect(findTagInput().attributes('value')).toBe(VALUE);
+ });
+
+ it('emits a change event when the tag name chagnes', () => {
+ findTagInput().vm.$emit('input', 'new-value');
+
+ expect(wrapper.emitted('change')).toEqual([['new-value']]);
+ });
+
+ it('binds the store value to the ref selector', () => {
+ const ref = findTagRef();
+ expect(ref.props('value')).toBe('v1');
+
+ ref.vm.$emit('input', 'v2');
+
+ expect(ref.props('value')).toBe('v1');
+ });
+
+ it('passes the project id tot he ref selector', () => {
+ expect(findTagRef().props('projectId')).toBe(TEST_PROJECT_ID);
+ });
+
+ it('binds the store value to the message', async () => {
+ const message = findTagMessage();
+ expect(message.attributes('value')).toBe('test');
+
+ message.vm.$emit('input', 'hello');
+
+ await nextTick();
+
+ expect(message.attributes('value')).toBe('hello');
+ });
+
+ it('emits create event when Save is clicked', () => {
+ const button = findSave();
+
+ expect(button.text()).toBe(__('Save'));
+
+ button.vm.$emit('click');
+
+ expect(wrapper.emitted('create')).toEqual([[]]);
+ });
+
+ it('emits cancel event when Select another tag is clicked', () => {
+ const button = findCancel();
+
+ expect(button.text()).toBe(s__('Release|Select another tag'));
+
+ button.vm.$emit('click');
+
+ expect(wrapper.emitted('cancel')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
index 8105aa4f6f2..0e896eb645c 100644
--- a/spec/frontend/releases/components/tag_field_exsting_spec.js
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -37,11 +37,6 @@ describe('releases/components/tag_field_existing', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('default', () => {
it('shows the tag name', () => {
createComponent();
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index fcba0da3462..3468338b8a7 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,17 +1,19 @@
-import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlDropdown, GlPopover } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
-import { trimText } from 'helpers/text_helper';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
+import TagSearch from '~/releases/components/tag_search.vue';
+import TagCreate from '~/releases/components/tag_create.vue';
import createStore from '~/releases/stores';
import createEditNewModule from '~/releases/stores/modules/edit_new';
+import { CREATE } from '~/releases/stores/modules/edit_new/constants';
+import { createRefModule } from '~/ref/stores';
+import { i18n } from '~/releases/constants';
const TEST_TAG_NAME = 'test-tag-name';
-const TEST_TAG_MESSAGE = 'Test tag message';
const TEST_PROJECT_ID = '1234';
const TEST_CREATE_FROM = 'test-create-from';
const NONEXISTENT_TAG_NAME = 'nonexistent-tag';
@@ -20,38 +22,12 @@ describe('releases/components/tag_field_new', () => {
let store;
let wrapper;
let mock;
- let RefSelectorStub;
-
- const createComponent = (
- mountFn = shallowMount,
- { searchQuery } = { searchQuery: NONEXISTENT_TAG_NAME },
- ) => {
- // A mock version of the RefSelector component that just renders the
- // #footer slot, so that the content inside this slot can be tested.
- RefSelectorStub = Vue.component('RefSelectorStub', {
- data() {
- return {
- footerSlotProps: {
- isLoading: false,
- matches: {
- tags: {
- totalCount: 1,
- list: [{ name: TEST_TAG_NAME }],
- },
- },
- query: searchQuery,
- },
- };
- },
- template: '<div><slot name="footer" v-bind="footerSlotProps"></slot></div>',
- });
- wrapper = mountFn(TagFieldNew, {
+ const createComponent = () => {
+ wrapper = shallowMountExtended(TagFieldNew, {
store,
stubs: {
- RefSelector: RefSelectorStub,
GlFormGroup,
- GlSprintf,
},
});
};
@@ -62,11 +38,12 @@ describe('releases/components/tag_field_new', () => {
editNew: createEditNewModule({
projectId: TEST_PROJECT_ID,
}),
+ ref: createRefModule(),
},
});
store.state.editNew.createFrom = TEST_CREATE_FROM;
- store.state.editNew.showCreateFrom = true;
+ store.state.editNew.step = CREATE;
store.state.editNew.release = {
tagName: TEST_TAG_NAME,
@@ -80,21 +57,13 @@ describe('releases/components/tag_field_new', () => {
gon.api_version = 'v4';
});
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- });
-
- const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]');
- const findTagNameDropdown = () => findTagNameFormGroup().findComponent(RefSelectorStub);
+ afterEach(() => mock.restore());
- const findCreateFromFormGroup = () => wrapper.find('[data-testid="create-from-field"]');
- const findCreateFromDropdown = () => findCreateFromFormGroup().findComponent(RefSelectorStub);
-
- const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem);
-
- const findAnnotatedTagMessageFormGroup = () =>
- wrapper.find('[data-testid="annotated-tag-message-field"]');
+ const findTagNameFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findTagNameInput = () => wrapper.findComponent(GlDropdown);
+ const findTagNamePopover = () => wrapper.findComponent(GlPopover);
+ const findTagNameSearch = () => wrapper.findComponent(TagSearch);
+ const findTagNameCreate = () => wrapper.findComponent(TagCreate);
describe('"Tag name" field', () => {
describe('rendering and behavior', () => {
@@ -102,20 +71,37 @@ describe('releases/components/tag_field_new', () => {
it('renders a label', () => {
expect(findTagNameFormGroup().attributes().label).toBe(__('Tag name'));
- expect(findTagNameFormGroup().props().labelDescription).toBe(__('*Required'));
+ expect(findTagNameFormGroup().props().optionalText).toBe(__('(required)'));
+ });
+
+ it('flips between search and create, passing the searched value', async () => {
+ let create = findTagNameCreate();
+ let search = findTagNameSearch();
+
+ expect(create.exists()).toBe(true);
+ expect(search.exists()).toBe(false);
+
+ await create.vm.$emit('cancel');
+
+ search = findTagNameSearch();
+ expect(create.exists()).toBe(false);
+ expect(search.exists()).toBe(true);
+
+ await search.vm.$emit('create', TEST_TAG_NAME);
+
+ create = findTagNameCreate();
+ expect(create.exists()).toBe(true);
+ expect(create.props('value')).toBe(TEST_TAG_NAME);
+ expect(search.exists()).toBe(false);
});
describe('when the user selects a new tag name', () => {
- beforeEach(async () => {
- findCreateNewTagOption().vm.$emit('click');
- });
+ it("updates the store's release.tagName property", async () => {
+ findTagNameCreate().vm.$emit('change', NONEXISTENT_TAG_NAME);
+ await findTagNameCreate().vm.$emit('create');
- it("updates the store's release.tagName property", () => {
expect(store.state.editNew.release.tagName).toBe(NONEXISTENT_TAG_NAME);
- });
-
- it('hides the "Create from" field', () => {
- expect(findCreateFromFormGroup().exists()).toBe(true);
+ expect(findTagNameInput().props('text')).toBe(NONEXISTENT_TAG_NAME);
});
});
@@ -123,19 +109,17 @@ describe('releases/components/tag_field_new', () => {
const updatedTagName = 'updated-tag-name';
beforeEach(async () => {
- findTagNameDropdown().vm.$emit('input', updatedTagName);
+ await findTagNameCreate().vm.$emit('cancel');
+ findTagNameSearch().vm.$emit('select', updatedTagName);
});
it("updates the store's release.tagName property", () => {
expect(store.state.editNew.release.tagName).toBe(updatedTagName);
+ expect(findTagNameInput().props('text')).toBe(updatedTagName);
});
it('hides the "Create from" field', () => {
- expect(findCreateFromFormGroup().exists()).toBe(false);
- });
-
- it('hides the "Tag message" field', () => {
- expect(findAnnotatedTagMessageFormGroup().exists()).toBe(false);
+ expect(findTagNameCreate().exists()).toBe(false);
});
it('fetches the release notes for the tag', () => {
@@ -145,133 +129,66 @@ describe('releases/components/tag_field_new', () => {
});
});
- describe('"Create tag" option', () => {
- describe('when the search query exactly matches one of the search results', () => {
- beforeEach(async () => {
- createComponent(mount, { searchQuery: TEST_TAG_NAME });
- });
-
- it('does not show the "Create tag" option', () => {
- expect(findCreateNewTagOption().exists()).toBe(false);
- });
- });
-
- describe('when the search query does not exactly match one of the search results', () => {
- beforeEach(async () => {
- createComponent(mount, { searchQuery: NONEXISTENT_TAG_NAME });
- });
-
- it('shows the "Create tag" option', () => {
- expect(findCreateNewTagOption().exists()).toBe(true);
- });
- });
- });
-
describe('validation', () => {
beforeEach(() => {
- createComponent(mount);
+ createComponent();
+ findTagNameCreate().vm.$emit('cancel');
});
/**
* Utility function to test the visibility of the validation message
- * @param {'shown' | 'hidden'} state The expected state of the validation message.
- * Should be passed either 'shown' or 'hidden'
+ * @param {boolean} isShown Whether or not the message is shown.
*/
- const expectValidationMessageToBe = async (state) => {
+ const expectValidationMessageToBeShown = async (isShown) => {
await nextTick();
- expect(findTagNameFormGroup().element).toHaveClass(
- state === 'shown' ? 'is-invalid' : 'is-valid',
- );
- expect(findTagNameFormGroup().element).not.toHaveClass(
- state === 'shown' ? 'is-valid' : 'is-invalid',
- );
+ const state = findTagNameFormGroup().attributes('state');
+
+ if (isShown) {
+ expect(state).toBeUndefined();
+ } else {
+ expect(state).toBe('true');
+ }
};
describe('when the user has not yet interacted with the component', () => {
it('does not display a validation error', async () => {
- findTagNameDropdown().vm.$emit('input', '');
-
- await expectValidationMessageToBe('hidden');
+ await expectValidationMessageToBeShown(false);
});
});
describe('when the user has interacted with the component and the value is not empty', () => {
it('does not display validation error', async () => {
- findTagNameDropdown().vm.$emit('hide');
+ findTagNameSearch().vm.$emit('select', 'vTest');
+ findTagNamePopover().vm.$emit('hide');
- await expectValidationMessageToBe('hidden');
+ await expectValidationMessageToBeShown(false);
});
it('displays a validation error if the tag has an associated release', async () => {
- findTagNameDropdown().vm.$emit('input', 'vTest');
- findTagNameDropdown().vm.$emit('hide');
+ findTagNameSearch().vm.$emit('select', 'vTest');
+ findTagNamePopover().vm.$emit('hide');
store.state.editNew.existingRelease = {};
- await expectValidationMessageToBe('shown');
- expect(findTagNameFormGroup().text()).toContain(
- __('Selected tag is already in use. Choose another option.'),
+ await expectValidationMessageToBeShown(true);
+ expect(findTagNameFormGroup().attributes('invalidfeedback')).toBe(
+ i18n.tagIsAlredyInUseMessage,
);
});
});
describe('when the user has interacted with the component and the value is empty', () => {
it('displays a validation error', async () => {
- findTagNameDropdown().vm.$emit('input', '');
- findTagNameDropdown().vm.$emit('hide');
+ findTagNameSearch().vm.$emit('select', '');
+ findTagNamePopover().vm.$emit('hide');
- await expectValidationMessageToBe('shown');
- expect(findTagNameFormGroup().text()).toContain(__('Tag name is required.'));
+ await expectValidationMessageToBeShown(true);
+ expect(findTagNameFormGroup().attributes('invalidfeedback')).toContain(
+ i18n.tagNameIsRequiredMessage,
+ );
});
});
});
});
-
- describe('"Create from" field', () => {
- beforeEach(() => createComponent());
-
- it('renders a label', () => {
- expect(findCreateFromFormGroup().attributes().label).toBe('Create from');
- });
-
- describe('when the user selects a git ref', () => {
- it("updates the store's createFrom property", async () => {
- const updatedCreateFrom = 'update-create-from';
- findCreateFromDropdown().vm.$emit('input', updatedCreateFrom);
-
- expect(store.state.editNew.createFrom).toBe(updatedCreateFrom);
- });
- });
- });
-
- describe('"Annotated Tag" field', () => {
- beforeEach(() => {
- createComponent(mountExtended);
- });
-
- it('renders a label', () => {
- expect(wrapper.findByRole('textbox', { name: 'Set tag message' }).exists()).toBe(true);
- });
-
- it('renders a description', () => {
- expect(trimText(findAnnotatedTagMessageFormGroup().text())).toContain(
- 'Add a message to the tag. Leaving this blank creates a lightweight tag.',
- );
- });
-
- it('updates the store', async () => {
- await findAnnotatedTagMessageFormGroup().find('textarea').setValue(TEST_TAG_MESSAGE);
-
- expect(store.state.editNew.release.tagMessage).toBe(TEST_TAG_MESSAGE);
- });
-
- it('shows a link', () => {
- const link = wrapper.findByRole('link', {
- name: 'lightweight tag',
- });
-
- expect(link.attributes('href')).toBe('https://git-scm.com/book/en/v2/Git-Basics-Tagging/');
- });
- });
});
diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js
index 85a40f02c53..8509c347291 100644
--- a/spec/frontend/releases/components/tag_field_spec.js
+++ b/spec/frontend/releases/components/tag_field_spec.js
@@ -24,11 +24,6 @@ describe('releases/components/tag_field', () => {
const findTagFieldNew = () => wrapper.findComponent(TagFieldNew);
const findTagFieldExisting = () => wrapper.findComponent(TagFieldExisting);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when an existing release is being edited', () => {
beforeEach(() => {
createComponent({ isExistingRelease: true });
diff --git a/spec/frontend/releases/components/tag_search_spec.js b/spec/frontend/releases/components/tag_search_spec.js
new file mode 100644
index 00000000000..4144a9cc297
--- /dev/null
+++ b/spec/frontend/releases/components/tag_search_spec.js
@@ -0,0 +1,144 @@
+import { GlButton, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { DEFAULT_PER_PAGE } from '~/api';
+import { __, s__, sprintf } from '~/locale';
+import TagSearch from '~/releases/components/tag_search.vue';
+import createStore from '~/releases/stores';
+import createEditNewModule from '~/releases/stores/modules/edit_new';
+import { createRefModule } from '~/ref/stores';
+
+const TEST_TAG_NAME = 'test-tag-name';
+const TEST_PROJECT_ID = '1234';
+const TAGS = [{ name: 'v1' }, { name: 'v2' }, { name: 'v3' }];
+
+describe('releases/components/tag_search', () => {
+ let store;
+ let wrapper;
+ let mock;
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = mount(TagSearch, {
+ store,
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore({
+ modules: {
+ editNew: createEditNewModule({
+ projectId: TEST_PROJECT_ID,
+ }),
+ ref: createRefModule(),
+ },
+ });
+
+ store.state.editNew.release = {};
+
+ mock = new MockAdapter(axios);
+ gon.api_version = 'v4';
+ });
+
+ afterEach(() => mock.restore());
+
+ const findSearch = () => wrapper.findComponent(GlSearchBoxByType);
+ const findCreate = () => wrapper.findAllComponents(GlButton).at(-1);
+ const findResults = () => wrapper.findAllComponents(GlDropdownItem);
+
+ describe('init', () => {
+ beforeEach(async () => {
+ mock
+ .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`)
+ .reply(200, TAGS, { 'x-total': TAGS.length });
+
+ createWrapper();
+
+ await waitForPromises();
+ });
+
+ it('displays a set of results immediately', () => {
+ findResults().wrappers.forEach((w, i) => expect(w.text()).toBe(TAGS[i].name));
+ });
+
+ it('has a disabled button', () => {
+ const button = findCreate();
+ expect(button.text()).toBe(s__('Release|Or type a new tag name'));
+ expect(button.props('disabled')).toBe(true);
+ });
+
+ it('has an empty search input', () => {
+ expect(findSearch().props('value')).toBe('');
+ });
+
+ describe('searching', () => {
+ const query = TEST_TAG_NAME;
+
+ beforeEach(async () => {
+ mock.reset();
+ mock
+ .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`, {
+ params: { search: query, per_page: DEFAULT_PER_PAGE },
+ })
+ .reply(200, [], { 'x-total': 0 });
+
+ findSearch().vm.$emit('input', query);
+
+ await nextTick();
+ await waitForPromises();
+ });
+
+ it('shows "No results found" when there are no results', () => {
+ expect(wrapper.text()).toContain(__('No results found'));
+ });
+
+ it('searches with the given input', () => {
+ expect(mock.history.get[0].params.search).toBe(query);
+ });
+
+ it('emits the query', () => {
+ expect(wrapper.emitted('change')).toEqual([[query]]);
+ });
+ });
+ });
+
+ describe('with query', () => {
+ const query = TEST_TAG_NAME;
+
+ beforeEach(async () => {
+ mock
+ .onGet(`/api/v4/projects/${TEST_PROJECT_ID}/repository/tags`, {
+ params: { search: query, per_page: DEFAULT_PER_PAGE },
+ })
+ .reply(200, TAGS, { 'x-total': TAGS.length });
+
+ createWrapper({ query });
+
+ await waitForPromises();
+ });
+
+ it('displays a set of results immediately', () => {
+ findResults().wrappers.forEach((w, i) => expect(w.text()).toBe(TAGS[i].name));
+ });
+
+ it('has an enabled button', () => {
+ const button = findCreate();
+ expect(button.text()).toMatchInterpolatedText(
+ sprintf(s__('Release|Create tag %{tag}'), { tag: query }),
+ );
+ expect(button.props('disabled')).toBe(false);
+ });
+
+ it('emits create event when button clicked', () => {
+ findCreate().vm.$emit('click');
+ expect(wrapper.emitted('create')).toEqual([[query]]);
+ });
+
+ it('has an empty search input', () => {
+ expect(findSearch().props('value')).toBe(query);
+ });
+ });
+});
diff --git a/spec/frontend/releases/release_notification_service_spec.js b/spec/frontend/releases/release_notification_service_spec.js
index 2344d4b929a..332e0a7e6ed 100644
--- a/spec/frontend/releases/release_notification_service_spec.js
+++ b/spec/frontend/releases/release_notification_service_spec.js
@@ -1,56 +1,107 @@
import {
popCreateReleaseNotification,
putCreateReleaseNotification,
+ popDeleteReleaseNotification,
+ putDeleteReleaseNotification,
} from '~/releases/release_notification_service';
-import { createAlert, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/alert';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('~/releases/release_notification_service', () => {
const projectPath = 'test-project-path';
const releaseName = 'test-release-name';
- const storageKey = `createRelease:${projectPath}`;
+ describe('create release', () => {
+ const storageKey = `createRelease:${projectPath}`;
- describe('prepareCreateReleaseFlash', () => {
- it('should set the session storage with project path key and release name value', () => {
- putCreateReleaseNotification(projectPath, releaseName);
+ describe('prepareFlash', () => {
+ it('should set the session storage with project path key and release name value', () => {
+ putCreateReleaseNotification(projectPath, releaseName);
- const item = window.sessionStorage.getItem(storageKey);
+ const item = window.sessionStorage.getItem(storageKey);
- expect(item).toBe(releaseName);
+ expect(item).toBe(releaseName);
+ });
});
- });
- describe('showNotificationsIfPresent', () => {
- describe('if notification is prepared', () => {
- beforeEach(() => {
- window.sessionStorage.setItem(storageKey, releaseName);
- popCreateReleaseNotification(projectPath);
- });
+ describe('showNotificationsIfPresent', () => {
+ describe('if notification is prepared', () => {
+ beforeEach(() => {
+ window.sessionStorage.setItem(storageKey, releaseName);
+ popCreateReleaseNotification(projectPath);
+ });
- it('should remove storage key', () => {
- const item = window.sessionStorage.getItem(storageKey);
+ it('should remove storage key', () => {
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(null);
+ });
- expect(item).toBe(null);
+ it('should create an alert message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Release ${releaseName} has been successfully created.`,
+ variant: VARIANT_SUCCESS,
+ });
+ });
});
- it('should create a flash message', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- message: `Release ${releaseName} has been successfully created.`,
- variant: VARIANT_SUCCESS,
+ describe('if notification is not prepared', () => {
+ beforeEach(() => {
+ popCreateReleaseNotification(projectPath);
+ });
+
+ it('should not create an alert message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(0);
});
});
});
+ });
+
+ describe('delete release', () => {
+ const storageKey = `deleteRelease:${projectPath}`;
+
+ describe('prepareFlash', () => {
+ it('should set the session storage with project path key and release name value', () => {
+ putDeleteReleaseNotification(projectPath, releaseName);
+
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(releaseName);
+ });
+ });
+
+ describe('showNotificationsIfPresent', () => {
+ describe('if notification is prepared', () => {
+ beforeEach(() => {
+ window.sessionStorage.setItem(storageKey, releaseName);
+ popDeleteReleaseNotification(projectPath);
+ });
- describe('if notification is not prepared', () => {
- beforeEach(() => {
- popCreateReleaseNotification(projectPath);
+ it('should remove storage key', () => {
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(null);
+ });
+
+ it('should create an alert message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Release ${releaseName} has been successfully deleted.`,
+ variant: VARIANT_SUCCESS,
+ });
+ });
});
- it('should not create a flash message', () => {
- expect(createAlert).toHaveBeenCalledTimes(0);
+ describe('if notification is not prepared', () => {
+ beforeEach(() => {
+ popDeleteReleaseNotification(projectPath);
+ });
+
+ it('should not create an alert message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(0);
+ });
});
});
});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index ca3b2d5f734..1d164b9f5c1 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -2,8 +2,8 @@ import { cloneDeep } from 'lodash';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
import { getTag } from '~/api/tags_api';
-import { createAlert } from '~/flash';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { s__ } from '~/locale';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql';
@@ -13,17 +13,12 @@ import deleteReleaseMutation from '~/releases/graphql/mutations/delete_release.m
import * as actions from '~/releases/stores/modules/edit_new/actions';
import * as types from '~/releases/stores/modules/edit_new/mutation_types';
import createState from '~/releases/stores/modules/edit_new/state';
-import {
- gqClient,
- convertOneReleaseGraphQLResponse,
- deleteReleaseSessionKey,
-} from '~/releases/util';
+import { gqClient, convertOneReleaseGraphQLResponse } from '~/releases/util';
+import { deleteReleaseSessionKey } from '~/releases/release_notification_service';
jest.mock('~/api/tags_api');
-jest.mock('~/flash');
-
-jest.mock('~/releases/release_notification_service');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
@@ -154,7 +149,7 @@ describe('Release edit/new actions', () => {
]);
});
- it(`shows a flash message`, () => {
+ it(`shows an alert message`, () => {
return actions.fetchRelease({ commit: jest.fn(), state, rootState: state }).then(() => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
@@ -311,8 +306,8 @@ describe('Release edit/new actions', () => {
it("redirects to the release's dedicated page", () => {
const { selfUrl } = releaseResponse.data.project.release.links;
actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state }, selfUrl);
- expect(redirectTo).toHaveBeenCalledTimes(1);
- expect(redirectTo).toHaveBeenCalledWith(selfUrl);
+ expect(redirectTo).toHaveBeenCalledTimes(1); // eslint-disable-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(selfUrl); // eslint-disable-line import/no-deprecated
});
});
@@ -380,7 +375,7 @@ describe('Release edit/new actions', () => {
]);
});
- it(`shows a flash message`, () => {
+ it(`shows an alert message`, () => {
return actions
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
@@ -406,7 +401,7 @@ describe('Release edit/new actions', () => {
]);
});
- it(`shows a flash message`, () => {
+ it(`shows an alert message`, () => {
return actions
.createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
.then(() => {
@@ -538,7 +533,7 @@ describe('Release edit/new actions', () => {
expect(commit.mock.calls).toEqual([[types.RECEIVE_SAVE_RELEASE_ERROR, error]]);
});
- it('shows a flash message', async () => {
+ it('shows an alert message', async () => {
await actions.updateRelease({ commit, dispatch, state, getters });
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -558,7 +553,7 @@ describe('Release edit/new actions', () => {
]);
});
- it('shows a flash message', async () => {
+ it('shows an alert message', async () => {
await actions.updateRelease({ commit, dispatch, state, getters });
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -711,7 +706,7 @@ describe('Release edit/new actions', () => {
expect(commit.mock.calls).toContainEqual([types.RECEIVE_SAVE_RELEASE_ERROR, error]);
});
- it('shows a flash message', async () => {
+ it('shows an alert message', async () => {
await actions.deleteRelease({ commit, dispatch, state, getters });
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -747,7 +742,7 @@ describe('Release edit/new actions', () => {
]);
});
- it('shows a flash message', async () => {
+ it('shows an alert message', async () => {
await actions.deleteRelease({ commit, dispatch, state, getters });
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -778,7 +773,7 @@ describe('Release edit/new actions', () => {
expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
});
- it('creates a flash on error', async () => {
+ it('creates an alert on error', async () => {
error = new Error();
getTag.mockRejectedValue(error);
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index f8b87ec71dc..736eae13fb3 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -1,5 +1,16 @@
import { s__ } from '~/locale';
import * as getters from '~/releases/stores/modules/edit_new/getters';
+import { i18n } from '~/releases/constants';
+import { validateTag, ValidationResult } from '~/lib/utils/ref_validator';
+
+jest.mock('~/lib/utils/ref_validator', () => {
+ const original = jest.requireActual('~/lib/utils/ref_validator');
+ return {
+ __esModule: true,
+ ValidationResult: original.ValidationResult,
+ validateTag: jest.fn(() => new original.ValidationResult()),
+ };
+});
describe('Release edit/new getters', () => {
describe('releaseLinksToCreate', () => {
@@ -59,23 +70,23 @@ describe('Release edit/new getters', () => {
});
describe('validationErrors', () => {
+ const validState = {
+ release: {
+ tagName: 'test-tag-name',
+ assets: {
+ links: [
+ { id: 1, url: 'https://example.com/valid', name: 'Link 1' },
+ { id: 2, url: '', name: '' },
+ { id: 3, url: '', name: ' ' },
+ { id: 4, url: ' ', name: '' },
+ { id: 5, url: ' ', name: ' ' },
+ ],
+ },
+ },
+ };
describe('when the form is valid', () => {
+ const state = validState;
it('returns no validation errors', () => {
- const state = {
- release: {
- tagName: 'test-tag-name',
- assets: {
- links: [
- { id: 1, url: 'https://example.com/valid', name: 'Link 1' },
- { id: 2, url: '', name: '' },
- { id: 3, url: '', name: ' ' },
- { id: 4, url: ' ', name: '' },
- { id: 5, url: ' ', name: ' ' },
- ],
- },
- },
- };
-
const expectedErrors = {
assets: {
links: {
@@ -88,7 +99,27 @@ describe('Release edit/new getters', () => {
},
};
- expect(getters.validationErrors(state)).toEqual(expectedErrors);
+ expect(getters.validationErrors(state).assets).toEqual(expectedErrors.assets);
+ expect(getters.validationErrors(state).tagNameValidation.isValid).toBe(true);
+ });
+ });
+
+ describe('when validating tag', () => {
+ const state = validState;
+ it('validateTag is called with right parameters', () => {
+ getters.validationErrors(state);
+ expect(validateTag).toHaveBeenCalledWith(state.release.tagName);
+ });
+
+ it('validation error is correctly returned', () => {
+ const validationError = new ValidationResult();
+ const errorText = 'Tag format validation error';
+ validationError.addValidationError(errorText);
+ validateTag.mockReturnValue(validationError);
+
+ const result = getters.validationErrors(state);
+ expect(validateTag).toHaveBeenCalledWith(state.release.tagName);
+ expect(result.tagNameValidation.validationErrors).toContain(errorText);
});
});
@@ -140,19 +171,17 @@ describe('Release edit/new getters', () => {
});
it('returns a validation error if the tag name is empty', () => {
- const expectedErrors = {
- isTagNameEmpty: true,
- };
-
- expect(actualErrors).toMatchObject(expectedErrors);
+ expect(actualErrors.tagNameValidation.isValid).toBe(false);
+ expect(actualErrors.tagNameValidation.validationErrors).toContain(
+ i18n.tagNameIsRequiredMessage,
+ );
});
it('returns a validation error if the tag has an existing release', () => {
- const expectedErrors = {
- existingRelease: true,
- };
-
- expect(actualErrors).toMatchObject(expectedErrors);
+ expect(actualErrors.tagNameValidation.isValid).toBe(false);
+ expect(actualErrors.tagNameValidation.validationErrors).toContain(
+ i18n.tagIsAlredyInUseMessage,
+ );
});
it('returns a validation error if links share a URL', () => {
@@ -395,14 +424,27 @@ describe('Release edit/new getters', () => {
describe('formattedReleaseNotes', () => {
it.each`
- description | includeTagNotes | tagNotes | included
- ${'release notes'} | ${true} | ${'tag notes'} | ${true}
- ${'release notes'} | ${true} | ${''} | ${false}
- ${'release notes'} | ${false} | ${'tag notes'} | ${false}
+ description | includeTagNotes | tagNotes | included | showCreateFrom
+ ${'release notes'} | ${true} | ${'tag notes'} | ${true} | ${false}
+ ${'release notes'} | ${true} | ${''} | ${false} | ${false}
+ ${'release notes'} | ${false} | ${'tag notes'} | ${false} | ${false}
+ ${'release notes'} | ${true} | ${'tag notes'} | ${true} | ${true}
+ ${'release notes'} | ${true} | ${''} | ${false} | ${true}
+ ${'release notes'} | ${false} | ${'tag notes'} | ${false} | ${true}
`(
- 'should include tag notes=$included when includeTagNotes=$includeTagNotes and tagNotes=$tagNotes',
- ({ description, includeTagNotes, tagNotes, included }) => {
- const state = { release: { description }, includeTagNotes, tagNotes };
+ 'should include tag notes=$included when includeTagNotes=$includeTagNotes and tagNotes=$tagNotes and showCreateFrom=$showCreateFrom',
+ ({ description, includeTagNotes, tagNotes, included, showCreateFrom }) => {
+ let state;
+
+ if (showCreateFrom) {
+ state = {
+ release: { description, tagMessage: tagNotes },
+ includeTagNotes,
+ showCreateFrom,
+ };
+ } else {
+ state = { release: { description }, includeTagNotes, tagNotes, showCreateFrom };
+ }
const text = `### ${s__('Releases|Tag message')}\n\n${tagNotes}\n`;
if (included) {
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js
index e56975d021a..22ef552c2f9 100644
--- a/spec/frontend/repository/commits_service_spec.js
+++ b/spec/frontend/repository/commits_service_spec.js
@@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants';
import { refWithSpecialCharMock } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('commits service', () => {
let mock;
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 6fe60f3c2e6..6825d4afecf 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -8,6 +8,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
class="gl-my-2 gl-mr-4"
imgalt=""
imgcssclasses=""
+ imgcsswrapperclasses=""
imgsize="32"
imgsrc="https://test.com"
linkhref="/test"
@@ -47,6 +48,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
<timeago-tooltip-stub
cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
time="2019-01-01"
tooltipplacement="bottom"
/>
diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js
index 33a85c04fcf..2c63deb99c9 100644
--- a/spec/frontend/repository/components/blob_button_group_spec.js
+++ b/spec/frontend/repository/components/blob_button_group_spec.js
@@ -1,5 +1,6 @@
import { GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import DeleteBlobModal from '~/repository/components/delete_blob_modal.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
@@ -26,7 +27,24 @@ const DEFAULT_INJECT = {
describe('BlobButtonGroup component', () => {
let wrapper;
+ let showUploadBlobModalMock;
+ let showDeleteBlobModalMock;
+
const createComponent = (props = {}) => {
+ showUploadBlobModalMock = jest.fn();
+ showDeleteBlobModalMock = jest.fn();
+
+ const UploadBlobModalStub = stubComponent(UploadBlobModal, {
+ methods: {
+ show: showUploadBlobModalMock,
+ },
+ });
+ const DeleteBlobModalStub = stubComponent(DeleteBlobModal, {
+ methods: {
+ show: showDeleteBlobModalMock,
+ },
+ });
+
wrapper = mountExtended(BlobButtonGroup, {
propsData: {
...DEFAULT_PROPS,
@@ -35,13 +53,13 @@ describe('BlobButtonGroup component', () => {
provide: {
...DEFAULT_INJECT,
},
+ stubs: {
+ UploadBlobModal: UploadBlobModalStub,
+ DeleteBlobModal: DeleteBlobModalStub,
+ },
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDeleteBlobModal = () => wrapper.findComponent(DeleteBlobModal);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findDeleteButton = () => wrapper.findByTestId('delete');
@@ -61,8 +79,6 @@ describe('BlobButtonGroup component', () => {
describe('buttons', () => {
beforeEach(() => {
createComponent();
- jest.spyOn(findUploadBlobModal().vm, 'show');
- jest.spyOn(findDeleteBlobModal().vm, 'show');
});
it('renders both the replace and delete button', () => {
@@ -77,33 +93,31 @@ describe('BlobButtonGroup component', () => {
it('triggers the UploadBlobModal from the replace button', () => {
findReplaceButton().trigger('click');
- expect(findUploadBlobModal().vm.show).toHaveBeenCalled();
+ expect(showUploadBlobModalMock).toHaveBeenCalled();
});
it('triggers the DeleteBlobModal from the delete button', () => {
findDeleteButton().trigger('click');
- expect(findDeleteBlobModal().vm.show).toHaveBeenCalled();
+ expect(showDeleteBlobModalMock).toHaveBeenCalled();
});
describe('showForkSuggestion set to true', () => {
beforeEach(() => {
createComponent({ showForkSuggestion: true });
- jest.spyOn(findUploadBlobModal().vm, 'show');
- jest.spyOn(findDeleteBlobModal().vm, 'show');
});
it('does not trigger the UploadBlobModal from the replace button', () => {
findReplaceButton().trigger('click');
- expect(findUploadBlobModal().vm.show).not.toHaveBeenCalled();
+ expect(showUploadBlobModalMock).not.toHaveBeenCalled();
expect(wrapper.emitted().fork).toHaveLength(1);
});
it('does not trigger the DeleteBlobModal from the delete button', () => {
findDeleteButton().trigger('click');
- expect(findDeleteBlobModal().vm.show).not.toHaveBeenCalled();
+ expect(showDeleteBlobModalMock).not.toHaveBeenCalled();
expect(wrapper.emitted().fork).toHaveLength(1);
});
});
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 03a8ee6ac5d..7e14d292946 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -175,7 +175,6 @@ describe('Blob content viewer component', () => {
});
afterEach(() => {
- wrapper.destroy();
mockAxios.reset();
});
@@ -482,11 +481,6 @@ describe('Blob content viewer component', () => {
repository: { empty },
} = projectMock;
- afterEach(() => {
- delete gon.current_user_id;
- delete gon.current_username;
- });
-
it('renders component', async () => {
window.gon.current_user_id = 1;
window.gon.current_username = 'root';
@@ -557,12 +551,12 @@ describe('Blob content viewer component', () => {
it('simple edit redirects to the simple editor', () => {
findWebIdeLink().vm.$emit('edit', 'simple');
- expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.editBlobPath);
+ 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');
- expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath);
+ expect(urlUtility.redirectTo).toHaveBeenCalledWith(simpleViewerMock.ideEditPath); // eslint-disable-line import/no-deprecated
});
it.each`
diff --git a/spec/frontend/repository/components/blob_controls_spec.js b/spec/frontend/repository/components/blob_controls_spec.js
index 0d52542397f..3ced5f6c4d2 100644
--- a/spec/frontend/repository/components/blob_controls_spec.js
+++ b/spec/frontend/repository/components/blob_controls_spec.js
@@ -50,8 +50,6 @@ describe('Blob controls component', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
it('renders a find button with the correct href', () => {
expect(findFindButton().attributes('href')).toBe('find/file.js');
});
diff --git a/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js
index 599443bf862..b4f4b0058de 100644
--- a/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/lfs_viewer_spec.js
@@ -21,8 +21,6 @@ describe('LFS Viewer', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
it('renders the correct text', () => {
expect(wrapper.text()).toBe(
'This content could not be displayed because it is stored in LFS. You can download it instead.',
diff --git a/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js
index 51f3d31ec72..5d37692bf90 100644
--- a/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js
@@ -1,7 +1,6 @@
-import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NotebookViewer from '~/repository/components/blob_viewers/notebook_viewer.vue';
-import notebookLoader from '~/blob/notebook';
+import Notebook from '~/blob/notebook/notebook_viewer.vue';
jest.mock('~/blob/notebook');
@@ -17,24 +16,11 @@ describe('Notebook Viewer', () => {
});
};
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findNotebookWrapper = () => wrapper.findByTestId('notebook');
+ const findNotebook = () => wrapper.findComponent(Notebook);
beforeEach(() => createComponent());
- it('calls the notebook loader', () => {
- expect(notebookLoader).toHaveBeenCalledWith({
- el: wrapper.vm.$refs.viewer,
- relativeRawPath: ROOT_RELATIVE_PATH,
- });
- });
-
- it('renders a loading icon component', () => {
- expect(findLoadingIcon().props('size')).toBe('lg');
- });
-
- it('renders the notebook wrapper', () => {
- expect(findNotebookWrapper().exists()).toBe(true);
- expect(findNotebookWrapper().attributes('data-endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath);
+ it('renders a Notebook component', () => {
+ expect(findNotebook().props('endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath);
});
});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index c2f34f79f89..f4baa817d32 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -1,27 +1,60 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlDropdown } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
-import { nextTick } from 'vue';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import permissionsQuery from 'shared_queries/repository/permissions.query.graphql';
+import projectPathQuery from '~/repository/queries/project_path.query.graphql';
+
+import createApolloProvider from 'helpers/mock_apollo_helper';
const defaultMockRoute = {
name: 'blobPath',
};
+const TEST_PROJECT_PATH = 'test-project/path';
+
+Vue.use(VueApollo);
+
describe('Repository breadcrumbs component', () => {
let wrapper;
-
- const factory = (currentPath, extraProps = {}, mockRoute = {}) => {
- const $apollo = {
- queries: {
+ let permissionsQuerySpy;
+
+ const createPermissionsQueryResponse = ({
+ pushCode = false,
+ forkProject = false,
+ createMergeRequestIn = false,
+ } = {}) => ({
+ data: {
+ project: {
+ id: 1,
+ __typename: '__typename',
userPermissions: {
- loading: true,
+ __typename: '__typename',
+ pushCode,
+ forkProject,
+ createMergeRequestIn,
},
},
- };
+ },
+ });
+
+ const factory = (currentPath, extraProps = {}, mockRoute = {}) => {
+ const apolloProvider = createApolloProvider([[permissionsQuery, permissionsQuerySpy]]);
+
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: projectPathQuery,
+ data: {
+ projectPath: TEST_PROJECT_PATH,
+ },
+ });
wrapper = shallowMount(Breadcrumbs, {
+ apolloProvider,
propsData: {
currentPath,
...extraProps,
@@ -34,16 +67,28 @@ describe('Repository breadcrumbs component', () => {
defaultMockRoute,
...mockRoute,
},
- $apollo,
},
});
};
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal);
+ const findRouterLink = () => wrapper.findAllComponents(RouterLinkStub);
+
+ beforeEach(() => {
+ permissionsQuerySpy = jest.fn().mockResolvedValue(createPermissionsQueryResponse());
+ });
+
+ it('queries for permissions', async () => {
+ factory('/');
- afterEach(() => {
- wrapper.destroy();
+ // We need to wait for the projectPath query to resolve
+ await waitForPromises();
+
+ expect(permissionsQuerySpy).toHaveBeenCalledWith({
+ projectPath: TEST_PROJECT_PATH,
+ });
});
it.each`
@@ -55,7 +100,7 @@ describe('Repository breadcrumbs component', () => {
`('renders $linkCount links for path $path', ({ path, linkCount }) => {
factory(path);
- expect(wrapper.findAllComponents(RouterLinkStub).length).toEqual(linkCount);
+ expect(findRouterLink().length).toEqual(linkCount);
});
it.each`
@@ -68,36 +113,27 @@ describe('Repository breadcrumbs component', () => {
'links to the correct router path when routeName is $routeName',
({ routeName, path, linkTo }) => {
factory(path, {}, { name: routeName });
- expect(wrapper.findAllComponents(RouterLinkStub).at(3).props('to')).toEqual(linkTo);
+ expect(findRouterLink().at(3).props('to')).toEqual(linkTo);
},
);
it('escapes hash in directory path', () => {
factory('app/assets/javascripts#');
- expect(wrapper.findAllComponents(RouterLinkStub).at(3).props('to')).toEqual(
- '/-/tree/app/assets/javascripts%23',
- );
+ expect(findRouterLink().at(3).props('to')).toEqual('/-/tree/app/assets/javascripts%23');
});
it('renders last link as active', () => {
factory('app/assets');
- expect(wrapper.findAllComponents(RouterLinkStub).at(2).attributes('aria-current')).toEqual(
- 'page',
- );
+ expect(findRouterLink().at(2).attributes('aria-current')).toEqual('page');
});
it('does not render add to tree dropdown when permissions are false', async () => {
- factory('/', { canCollaborate: false });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
-
+ factory('/', { canCollaborate: false }, {});
await nextTick();
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
it.each`
@@ -111,20 +147,19 @@ describe('Repository breadcrumbs component', () => {
'does render add to tree dropdown $isRendered when route is $routeName',
({ routeName, isRendered }) => {
factory('app/assets/javascripts.js', { canCollaborate: true }, { name: routeName });
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(isRendered);
+ expect(findDropdown().exists()).toBe(isRendered);
},
);
it('renders add to tree dropdown when permissions are true', async () => {
- factory('/', { canCollaborate: true });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
+ permissionsQuerySpy.mockResolvedValue(
+ createPermissionsQueryResponse({ forkProject: true, createMergeRequestIn: true }),
+ );
+ factory('/', { canCollaborate: true });
await nextTick();
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
+ expect(findDropdown().exists()).toBe(true);
});
describe('renders the upload blob modal', () => {
@@ -137,10 +172,6 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
-
await nextTick();
expect(findUploadBlobModal().exists()).toBe(true);
@@ -156,10 +187,6 @@ describe('Repository breadcrumbs component', () => {
});
it('renders the modal once loaded', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } });
-
await nextTick();
expect(findNewDirectoryModal().exists()).toBe(true);
diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js
index b5996816ad8..90f2150222c 100644
--- a/spec/frontend/repository/components/delete_blob_modal_spec.js
+++ b/spec/frontend/repository/components/delete_blob_modal_spec.js
@@ -49,10 +49,6 @@ describe('DeleteBlobModal', () => {
await findCommitTextarea().vm.$emit('input', commitText);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders Modal component', () => {
createComponent();
@@ -186,13 +182,13 @@ describe('DeleteBlobModal', () => {
await fillForm({ targetText: '', commitText: '' });
});
- it('disables submit button', async () => {
- expect(findModal().props('actionPrimary').attributes[0]).toEqual(
+ it('disables submit button', () => {
+ expect(findModal().props('actionPrimary').attributes).toEqual(
expect.objectContaining({ disabled: true }),
);
});
- it('does not submit form', async () => {
+ it('does not submit form', () => {
findModal().vm.$emit('primary', { preventDefault: () => {} });
expect(submitSpy).not.toHaveBeenCalled();
});
@@ -206,13 +202,13 @@ describe('DeleteBlobModal', () => {
});
});
- it('enables submit button', async () => {
- expect(findModal().props('actionPrimary').attributes[0]).toEqual(
+ it('enables submit button', () => {
+ expect(findModal().props('actionPrimary').attributes).toEqual(
expect.objectContaining({ disabled: false }),
);
});
- it('submits form', async () => {
+ it('submits form', () => {
findModal().vm.$emit('primary', { preventDefault: () => {} });
expect(submitSpy).toHaveBeenCalled();
});
diff --git a/spec/frontend/repository/components/directory_download_links_spec.js b/spec/frontend/repository/components/directory_download_links_spec.js
index 72c4165c2e9..3739829c759 100644
--- a/spec/frontend/repository/components/directory_download_links_spec.js
+++ b/spec/frontend/repository/components/directory_download_links_spec.js
@@ -16,10 +16,6 @@ function factory(currentPath) {
}
describe('Repository directory download links component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it.each`
path
${'app'}
diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js
index f327a8cfae7..62a66e59d24 100644
--- a/spec/frontend/repository/components/fork_info_spec.js
+++ b/spec/frontend/repository/components/fork_info_spec.js
@@ -1,42 +1,82 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf, GlButton, GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/alert';
import ForkInfo, { i18n } from '~/repository/components/fork_info.vue';
+import ConflictsModal from '~/repository/components/fork_sync_conflicts_modal.vue';
import forkDetailsQuery from '~/repository/queries/fork_details.query.graphql';
+import syncForkMutation from '~/repository/mutations/sync_fork.mutation.graphql';
+import eventHub from '~/repository/event_hub';
+import {
+ POLLING_INTERVAL_DEFAULT,
+ POLLING_INTERVAL_BACKOFF,
+ FORK_UPDATED_EVENT,
+} from '~/repository/constants';
import { propsForkInfo } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('ForkInfo component', () => {
let wrapper;
- let mockResolver;
+ let mockForkDetailsQuery;
const forkInfoError = new Error('Something went wrong');
const projectId = 'gid://gitlab/Project/1';
+ const showMock = jest.fn();
+ const synchronizeFork = true;
Vue.use(VueApollo);
- const createCommitData = ({ ahead = 3, behind = 7 }) => {
+ const waitForPolling = async (interval = POLLING_INTERVAL_DEFAULT) => {
+ jest.advanceTimersByTime(interval);
+ await waitForPromises();
+ };
+
+ const mockResolvedForkDetailsQuery = (
+ forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false },
+ ) => {
+ mockForkDetailsQuery.mockResolvedValue({
+ data: {
+ project: { id: projectId, forkDetails },
+ },
+ });
+ };
+
+ const createSyncForkDetailsData = (
+ forkDetails = { ahead: 3, behind: 7, isSyncing: false, hasConflicts: false },
+ ) => {
return {
data: {
- project: { id: projectId, forkDetails: { ahead, behind, __typename: 'ForkDetails' } },
+ projectSyncFork: { details: forkDetails, errors: [] },
},
};
};
- const createComponent = (props = {}, data = {}, isRequestFailed = false) => {
- mockResolver = isRequestFailed
- ? jest.fn().mockRejectedValue(forkInfoError)
- : jest.fn().mockResolvedValue(createCommitData(data));
-
+ const createComponent = (props = {}, mutationData = {}) => {
wrapper = shallowMountExtended(ForkInfo, {
- apolloProvider: createMockApollo([[forkDetailsQuery, mockResolver]]),
+ apolloProvider: createMockApollo([
+ [forkDetailsQuery, mockForkDetailsQuery],
+ [syncForkMutation, jest.fn().mockResolvedValue(createSyncForkDetailsData(mutationData))],
+ ]),
propsData: { ...propsForkInfo, ...props },
- stubs: { GlSprintf },
+ stubs: {
+ GlSprintf,
+ GlButton,
+ ConflictsModal: stubComponent(ConflictsModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ methods: { show: showMock },
+ }),
+ },
+ provide: {
+ glFeatures: {
+ synchronizeFork,
+ },
+ },
});
return waitForPromises();
};
@@ -44,11 +84,25 @@ describe('ForkInfo component', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findIcon = () => wrapper.findComponent(GlIcon);
+ const findUpdateForkButton = () => wrapper.findByTestId('update-fork-button');
+ const findCreateMrButton = () => wrapper.findByTestId('create-mr-button');
+ const findViewMrButton = () => wrapper.findByTestId('view-mr-button');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDivergenceMessage = () => wrapper.findByTestId('divergence-message');
const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project');
const findCompareLinks = () => findDivergenceMessage().findAllComponents(GlLink);
- it('displays a skeleton while loading data', async () => {
+ const startForkUpdate = async () => {
+ findUpdateForkButton().vm.$emit('click');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ mockForkDetailsQuery = jest.fn();
+ mockResolvedForkDetailsQuery();
+ });
+
+ it('displays a skeleton while loading data', () => {
createComponent();
expect(findSkeleton().exists()).toBe(true);
});
@@ -65,12 +119,12 @@ describe('ForkInfo component', () => {
it('queries the data when sourceName is present', async () => {
await createComponent();
- expect(mockResolver).toHaveBeenCalled();
+ expect(mockForkDetailsQuery).toHaveBeenCalled();
});
it('does not query the data when sourceName is empty', async () => {
await createComponent({ sourceName: null });
- expect(mockResolver).not.toHaveBeenCalled();
+ expect(mockForkDetailsQuery).not.toHaveBeenCalled();
});
it('renders inaccessible message when fork source is not available', async () => {
@@ -87,14 +141,99 @@ describe('ForkInfo component', () => {
expect(link.attributes('href')).toBe(propsForkInfo.sourcePath);
});
- it('renders unknown divergence message when divergence is unknown', async () => {
- await createComponent({}, { ahead: null, behind: null });
- expect(findDivergenceMessage().text()).toBe(i18n.unknown);
+ it('renders Create MR Button with correct path', async () => {
+ await createComponent();
+ expect(findCreateMrButton().attributes('href')).toBe(propsForkInfo.createMrPath);
+ });
+
+ it('renders View MR Button with correct path', async () => {
+ const viewMrPath = 'path/to/view/mr';
+ await createComponent({ viewMrPath });
+ expect(findViewMrButton().attributes('href')).toBe(viewMrPath);
});
- it('renders up to date message when divergence is unknown', async () => {
- await createComponent({}, { ahead: 0, behind: 0 });
- expect(findDivergenceMessage().text()).toBe(i18n.upToDate);
+ it('does not render create MR button if create MR path is blank', async () => {
+ await createComponent({ createMrPath: '' });
+ expect(findCreateMrButton().exists()).toBe(false);
+ });
+
+ it('renders alert with error message when request fails', async () => {
+ mockForkDetailsQuery.mockRejectedValue(forkInfoError);
+ await createComponent({});
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.error,
+ captureError: true,
+ error: forkInfoError,
+ });
+ });
+
+ describe('Unknown divergence', () => {
+ it('renders unknown divergence message when divergence is unknown', async () => {
+ mockResolvedForkDetailsQuery({
+ ahead: null,
+ behind: null,
+ isSyncing: false,
+ hasConflicts: false,
+ });
+ await createComponent({});
+ expect(findDivergenceMessage().text()).toBe(i18n.unknown);
+ });
+
+ it('renders Update Fork button', async () => {
+ mockResolvedForkDetailsQuery({
+ ahead: null,
+ behind: null,
+ isSyncing: false,
+ hasConflicts: false,
+ });
+ await createComponent({});
+ expect(findUpdateForkButton().exists()).toBe(true);
+ expect(findUpdateForkButton().text()).toBe(i18n.updateFork);
+ });
+ });
+
+ describe('Up to date divergence', () => {
+ beforeEach(async () => {
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
+ await createComponent({}, { ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
+ });
+
+ it('renders up to date message when fork is up to date', () => {
+ expect(findDivergenceMessage().text()).toBe(i18n.upToDate);
+ });
+
+ it('does not render Update Fork button', () => {
+ expect(findUpdateForkButton().exists()).toBe(false);
+ });
+ });
+
+ describe('Limited visibility project', () => {
+ beforeEach(async () => {
+ mockResolvedForkDetailsQuery(null);
+ await createComponent({}, null);
+ });
+
+ it('renders limited visibility message when forkDetails are empty', () => {
+ expect(findDivergenceMessage().text()).toBe(i18n.limitedVisibility);
+ });
+
+ it('does not render Update Fork button', () => {
+ expect(findUpdateForkButton().exists()).toBe(false);
+ });
+ });
+
+ describe('User cannot sync the branch', () => {
+ beforeEach(async () => {
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 7, isSyncing: false, hasConflicts: false });
+ await createComponent(
+ { canSyncBranch: false },
+ { ahead: 0, behind: 7, isSyncing: false, hasConflicts: false },
+ );
+ });
+
+ it('does not render Update Fork button', () => {
+ expect(findUpdateForkButton().exists()).toBe(false);
+ });
});
describe.each([
@@ -104,6 +243,8 @@ describe('ForkInfo component', () => {
message: '3 commits behind, 7 commits ahead of the upstream repository.',
firstLink: propsForkInfo.behindComparePath,
secondLink: propsForkInfo.aheadComparePath,
+ hasUpdateButton: true,
+ hasCreateMrButton: true,
},
{
ahead: 7,
@@ -111,6 +252,8 @@ describe('ForkInfo component', () => {
message: '7 commits ahead of the upstream repository.',
firstLink: propsForkInfo.aheadComparePath,
secondLink: '',
+ hasUpdateButton: false,
+ hasCreateMrButton: true,
},
{
ahead: 0,
@@ -118,12 +261,15 @@ describe('ForkInfo component', () => {
message: '3 commits behind the upstream repository.',
firstLink: propsForkInfo.behindComparePath,
secondLink: '',
+ hasUpdateButton: true,
+ hasCreateMrButton: false,
},
])(
'renders correct divergence message for ahead: $ahead, behind: $behind divergence commits',
- ({ ahead, behind, message, firstLink, secondLink }) => {
+ ({ ahead, behind, message, firstLink, secondLink, hasUpdateButton, hasCreateMrButton }) => {
beforeEach(async () => {
- await createComponent({}, { ahead, behind });
+ mockResolvedForkDetailsQuery({ ahead, behind, isSyncing: false, hasConflicts: false });
+ await createComponent({});
});
it('displays correct text', () => {
@@ -138,15 +284,99 @@ describe('ForkInfo component', () => {
expect(links.at(1).attributes('href')).toBe(secondLink);
}
});
+
+ it('renders Update Fork button when fork is behind', () => {
+ expect(findUpdateForkButton().exists()).toBe(hasUpdateButton);
+ if (hasUpdateButton) {
+ expect(findUpdateForkButton().text()).toBe(i18n.updateFork);
+ }
+ });
+
+ it('renders Create Merge Request button when fork is ahead', () => {
+ expect(findCreateMrButton().exists()).toBe(hasCreateMrButton);
+ if (hasCreateMrButton) {
+ expect(findCreateMrButton().text()).toBe(i18n.createMergeRequest);
+ }
+ });
},
);
- it('renders alert with error message when request fails', async () => {
- await createComponent({}, {}, true);
- expect(createAlert).toHaveBeenCalledWith({
- message: i18n.error,
- captureError: true,
- error: forkInfoError,
+ describe('when sync is not possible due to conflicts', () => {
+ it('Opens Conflicts Modal', async () => {
+ mockResolvedForkDetailsQuery({ ahead: 7, behind: 3, isSyncing: false, hasConflicts: true });
+ await createComponent({});
+ findUpdateForkButton().vm.$emit('click');
+ expect(showMock).toHaveBeenCalled();
+ });
+ });
+
+ describe('projectSyncFork mutation', () => {
+ it('changes button to have loading state', async () => {
+ await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 3, isSyncing: false, hasConflicts: false });
+ expect(findLoadingIcon().exists()).toBe(false);
+ await startForkUpdate();
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('polling', () => {
+ beforeEach(async () => {
+ await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
+ });
+
+ it('fetches data on the initial load', () => {
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(1);
+ });
+
+ it('starts polling after sync button is clicked', async () => {
+ await startForkUpdate();
+ await waitForPolling();
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
+
+ await waitForPolling(POLLING_INTERVAL_DEFAULT * POLLING_INTERVAL_BACKOFF);
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(3);
+ });
+
+ it('stops polling once sync is finished', async () => {
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
+ await startForkUpdate();
+ await waitForPolling();
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
+ await waitForPolling(POLLING_INTERVAL_DEFAULT * POLLING_INTERVAL_BACKOFF);
+ expect(mockForkDetailsQuery).toHaveBeenCalledTimes(2);
+ await nextTick();
+ });
+ });
+
+ describe('once fork is updated', () => {
+ beforeEach(async () => {
+ await createComponent({}, { ahead: 0, behind: 3, isSyncing: true, hasConflicts: false });
+ mockResolvedForkDetailsQuery({ ahead: 0, behind: 0, isSyncing: false, hasConflicts: false });
+ });
+
+ it('shows info alert once the fork is updated', async () => {
+ await startForkUpdate();
+ await waitForPolling();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.successMessage,
+ variant: VARIANT_INFO,
+ });
+ });
+
+ it('emits fork:updated event to eventHub', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+ await startForkUpdate();
+ await waitForPolling();
+ expect(eventHub.$emit).toHaveBeenCalledWith(FORK_UPDATED_EVENT);
+ });
+
+ it('hides update fork button', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+ await startForkUpdate();
+ await waitForPolling();
+ expect(findUpdateForkButton().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/repository/components/fork_suggestion_spec.js b/spec/frontend/repository/components/fork_suggestion_spec.js
index 36a48a3fdb8..a9e5c18c0a9 100644
--- a/spec/frontend/repository/components/fork_suggestion_spec.js
+++ b/spec/frontend/repository/components/fork_suggestion_spec.js
@@ -14,8 +14,6 @@ describe('ForkSuggestion component', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
const { i18n } = ForkSuggestion;
const findMessage = () => wrapper.findByTestId('message');
const findForkButton = () => wrapper.findByTestId('fork');
diff --git a/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js b/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js
new file mode 100644
index 00000000000..3fd9284e29b
--- /dev/null
+++ b/spec/frontend/repository/components/fork_sync_conflicts_modal_spec.js
@@ -0,0 +1,46 @@
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ConflictsModal, { i18n } from '~/repository/components/fork_sync_conflicts_modal.vue';
+import { propsConflictsModal } from '../mock_data';
+
+describe('ConflictsModal', () => {
+ let wrapper;
+
+ function createComponent({ props = {} } = {}) {
+ wrapper = shallowMount(ConflictsModal, {
+ propsData: props,
+ stubs: { GlModal },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent({ props: propsConflictsModal });
+ });
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findInstructions = () => wrapper.findAll('[ data-testid="resolve-conflict-instructions"]');
+
+ it('renders a modal', () => {
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('passes title as a prop to a gl-modal component', () => {
+ expect(findModal().props().title).toBe(i18n.modalTitle);
+ });
+
+ it('renders a selection of markdown fields', () => {
+ expect(findInstructions().length).toBe(3);
+ });
+
+ it('renders a source url in a first intruction', () => {
+ expect(findInstructions().at(0).text()).toContain(propsConflictsModal.sourcePath);
+ });
+
+ it('renders default branch name in a first step intruction', () => {
+ expect(findInstructions().at(0).text()).toContain(propsConflictsModal.sourceDefaultBranch);
+ });
+
+ it('renders selected branch name in a second step intruction', () => {
+ expect(findInstructions().at(1).text()).toContain(propsConflictsModal.selectedBranch);
+ });
+});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 7226e7baa36..c207d32d61d 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -4,10 +4,12 @@ import { GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-
import LastCommit from '~/repository/components/last_commit.vue';
+import SignatureBadge from '~/commit/components/signature_badge.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import eventHub from '~/repository/event_hub';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
+import { FORK_UPDATED_EVENT } from '~/repository/constants';
import { refMock } from '../mock_data';
let wrapper;
@@ -20,7 +22,7 @@ const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink);
const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCommitRowDescription = () => wrapper.find('.commit-row-description');
-const findStatusBox = () => wrapper.find('.signature-badge');
+const findStatusBox = () => wrapper.findComponent(SignatureBadge);
const findItemTitle = () => wrapper.find('.item-title');
const defaultPipelineEdges = [
@@ -56,7 +58,7 @@ const createCommitData = ({
pipelineEdges = defaultPipelineEdges,
author = defaultAuthor,
descriptionHtml = '',
- signatureHtml = null,
+ signature = null,
message = defaultMessage,
}) => {
return {
@@ -84,7 +86,7 @@ const createCommitData = ({
authorName: 'Test',
authorGravatar: 'https://test.com',
author,
- signatureHtml,
+ signature,
pipelines: {
__typename: 'PipelineConnection',
edges: pipelineEdges,
@@ -99,7 +101,7 @@ const createCommitData = ({
};
};
-const createComponent = async (data = {}) => {
+const createComponent = (data = {}) => {
Vue.use(VueApollo);
const currentPath = 'path';
@@ -110,11 +112,13 @@ const createComponent = async (data = {}) => {
apolloProvider: createMockApollo([[pathLastCommitQuery, mockResolver]]),
propsData: { currentPath },
mixins: [{ data: () => ({ ref: refMock }) }],
+ stubs: {
+ SignatureBadge,
+ },
});
};
afterEach(() => {
- wrapper.destroy();
mockResolver = null;
});
@@ -177,6 +181,25 @@ describe('Repository last commit component', () => {
expect(findCommitRowDescription().exists()).toBe(false);
});
+ describe('created', () => {
+ it('binds `epicsListScrolled` event listener via eventHub', () => {
+ jest.spyOn(eventHub, '$on').mockImplementation(() => {});
+ createComponent();
+
+ expect(eventHub.$on).toHaveBeenCalledWith(FORK_UPDATED_EVENT, expect.any(Function));
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('unbinds `epicsListScrolled` event listener via eventHub', () => {
+ jest.spyOn(eventHub, '$off').mockImplementation(() => {});
+ createComponent();
+ wrapper.destroy();
+
+ expect(eventHub.$off).toHaveBeenCalledWith(FORK_UPDATED_EVENT, expect.any(Function));
+ });
+ });
+
describe('when the description is present', () => {
beforeEach(async () => {
createComponent({ descriptionHtml: '&#x000A;Update ADOPTERS.md' });
@@ -204,23 +227,19 @@ describe('Repository last commit component', () => {
});
it('renders the signature HTML as returned by the backend', async () => {
+ const signatureResponse = {
+ __typename: 'GpgSignature',
+ gpgKeyPrimaryKeyid: 'xxx',
+ verificationStatus: 'VERIFIED',
+ };
createComponent({
- signatureHtml: `<a
- class="btn signature-badge"
- data-content="signature-content"
- data-html="true"
- data-placement="top"
- data-title="signature-title"
- data-toggle="popover"
- role="button"
- tabindex="0"
- ><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`,
+ signature: {
+ ...signatureResponse,
+ },
});
await waitForPromises();
- expect(findStatusBox().html()).toBe(
- `<a class="btn signature-badge" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0"><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`,
- );
+ expect(findStatusBox().props()).toMatchObject({ signature: signatureResponse });
});
it('sets correct CSS class if the commit message is empty', async () => {
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
index 4e5c9a685c4..55a24089d48 100644
--- a/spec/frontend/repository/components/new_directory_modal_spec.js
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -4,12 +4,12 @@ import { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
@@ -76,10 +76,6 @@ describe('NewDirectoryModal', () => {
await waitForPromises();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders modal component', () => {
createComponent();
@@ -128,7 +124,7 @@ describe('NewDirectoryModal', () => {
});
describe('form submission', () => {
- beforeEach(async () => {
+ beforeEach(() => {
mock = new MockAdapter(axios);
});
@@ -185,10 +181,10 @@ describe('NewDirectoryModal', () => {
it('disables submit button', async () => {
await fillForm({ dirName: '', branchName: '', commitMessage: '' });
- expect(findModal().props('actionPrimary').attributes[0].disabled).toBe(true);
+ expect(findModal().props('actionPrimary').attributes.disabled).toBe(true);
});
- it('creates a flash error', async () => {
+ it('creates an alert error', async () => {
mock.onPost(initialProps.path).timeout();
await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' });
diff --git a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap b/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
deleted file mode 100644
index 48a4feca1e5..00000000000
--- a/spec/frontend/repository/components/preview/__snapshots__/index_spec.js.snap
+++ /dev/null
@@ -1,42 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Repository file preview component renders file HTML 1`] = `
-<article
- class="file-holder limited-width-container readme-holder"
->
- <div
- class="js-file-title file-title-flex-parent"
- >
- <div
- class="file-header-content"
- >
- <gl-icon-stub
- name="doc-text"
- size="16"
- />
-
- <gl-link-stub
- href="http://test.com"
- >
- <strong>
- README.md
- </strong>
- </gl-link-stub>
- </div>
- </div>
-
- <div
- class="blob-viewer"
- data-qa-selector="blob_viewer_content"
- itemprop="about"
- >
- <div>
- <div
- class="blob"
- >
- test
- </div>
- </div>
- </div>
-</article>
-`;
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index d4c746b67d6..316ddfb5731 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -1,77 +1,60 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { handleLocationHash } from '~/lib/utils/common_utils';
+import waitForPromises from 'helpers/wait_for_promises';
import Preview from '~/repository/components/preview/index.vue';
+const PROPS_DATA = {
+ blob: {
+ webPath: 'http://test.com',
+ name: 'README.md',
+ },
+};
+
+const MOCK_README_DATA = {
+ __typename: 'ReadmeFile',
+ html: '<div class="blob">test</div>',
+};
+
jest.mock('~/lib/utils/common_utils');
-let vm;
-let $apollo;
+Vue.use(VueApollo);
+
+let wrapper;
+let mockApollo;
+let mockReadmeData;
-function factory(blob, loading) {
- $apollo = {
- queries: {
- readme: {
- query: jest.fn().mockReturnValue(Promise.resolve({})),
- loading,
- },
- },
- };
+const mockResolvers = {
+ Query: {
+ readme: () => mockReadmeData(),
+ },
+};
- vm = shallowMount(Preview, {
- propsData: {
- blob,
- },
- mocks: {
- $apollo,
- },
+function createComponent() {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ return shallowMount(Preview, {
+ propsData: PROPS_DATA,
+ apolloProvider: mockApollo,
});
}
describe('Repository file preview component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
- it('renders file HTML', async () => {
- factory({
- webPath: 'http://test.com',
- name: 'README.md',
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ readme: { html: '<div class="blob">test</div>' } });
-
- await nextTick();
- expect(vm.element).toMatchSnapshot();
+ beforeEach(() => {
+ mockReadmeData = jest.fn();
+ wrapper = createComponent();
+ mockReadmeData.mockResolvedValue(MOCK_README_DATA);
});
it('handles hash after render', async () => {
- factory({
- webPath: 'http://test.com',
- name: 'README.md',
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ readme: { html: '<div class="blob">test</div>' } });
-
- await nextTick();
+ await waitForPromises();
expect(handleLocationHash).toHaveBeenCalled();
});
- it('renders loading icon', async () => {
- factory(
- {
- webPath: 'http://test.com',
- name: 'README.md',
- },
- true,
- );
-
- await nextTick();
- expect(vm.findComponent(GlLoadingIcon).exists()).toBe(true);
+ it('renders loading icon', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
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 b99d741e984..85bf683fdf6 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -56,6 +56,7 @@ exports[`Repository table row component renders a symlink table row 1`] = `
>
<timeago-tooltip-stub
cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
time="2019-01-01"
tooltipplacement="top"
/>
@@ -121,6 +122,7 @@ exports[`Repository table row component renders table row 1`] = `
>
<timeago-tooltip-stub
cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
time="2019-01-01"
tooltipplacement="top"
/>
@@ -186,6 +188,7 @@ exports[`Repository table row component renders table row for path with special
>
<timeago-tooltip-stub
cssclass=""
+ datetimeformat="DATE_WITH_TIME_FORMAT"
time="2019-01-01"
tooltipplacement="top"
/>
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index 8b987551b33..f7be367887c 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -88,10 +88,6 @@ function factory({ path, isLoading = false, hasMore = true, entries = {}, commit
const findTableRows = () => vm.findAllComponents(TableRow);
describe('Repository table component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it.each`
path | ref
${'/'} | ${'main'}
diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js
index 03fb4242e40..77822a148b7 100644
--- a/spec/frontend/repository/components/table/parent_row_spec.js
+++ b/spec/frontend/repository/components/table/parent_row_spec.js
@@ -26,10 +26,6 @@ function factory(path, loadingPath) {
}
describe('Repository parent row component', () => {
- afterEach(() => {
- vm.destroy();
- });
-
it.each`
path | to
${'app'} | ${'/-/tree/main/'}
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 5d9138ab9cd..02b505c828c 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -1,7 +1,10 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlBadge, GlLink, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import refQuery from '~/repository/queries/ref.query.graphql';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import createMockApollo from 'helpers/mock_apollo_helper';
import TableRow from '~/repository/components/table/row.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import { FILE_SYMLINK_MODE } from '~/vue_shared/constants';
@@ -9,26 +12,40 @@ import { ROW_APPEAR_DELAY } from '~/repository/constants';
const COMMIT_MOCK = { lockLabel: 'Locked by Root', committedDate: '2019-01-01' };
-let vm;
+let wrapper;
let $router;
-function factory(propsData = {}) {
+const createMockApolloProvider = (mockData) => {
+ Vue.use(VueApollo);
+ const apolloProver = createMockApollo([]);
+ apolloProver.clients.defaultClient.cache.writeQuery({ query: refQuery, data: { ...mockData } });
+
+ return apolloProver;
+};
+
+function factory({ mockData = { ref: 'main', escapedRef: 'main' }, propsData = {} } = {}) {
$router = {
push: jest.fn(),
};
- vm = shallowMount(TableRow, {
+ wrapper = shallowMount(TableRow, {
+ apolloProvider: createMockApolloProvider(mockData),
propsData: {
+ id: '1',
+ sha: '0as4k',
commitInfo: COMMIT_MOCK,
- ...propsData,
- name: propsData.path,
+ name: 'name',
+ currentPath: 'gitlab-org/gitlab-ce',
projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
totalEntries: 10,
rowNumber: 123,
+ path: 'gitlab-org/gitlab-ce',
+ type: 'tree',
+ ...propsData,
},
directives: {
- GlHoverLoad: createMockDirective(),
+ GlHoverLoad: createMockDirective('gl-hover-load'),
},
mocks: {
$router,
@@ -37,67 +54,67 @@ function factory(propsData = {}) {
RouterLink: RouterLinkStub,
},
});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ escapedRef: 'main' });
}
describe('Repository table row component', () => {
- const findRouterLink = () => vm.findComponent(RouterLinkStub);
- const findIntersectionObserver = () => vm.findComponent(GlIntersectionObserver);
-
- afterEach(() => {
- vm.destroy();
- });
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findFileIcon = () => wrapper.findComponent(FileIcon);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findRouterLink = () => wrapper.findComponent(RouterLinkStub);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
- it('renders table row', async () => {
+ it('renders table row', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'file',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'file',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.element).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('renders a symlink table row', async () => {
+ it('renders a symlink table row', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'blob',
- currentPath: '/',
- mode: FILE_SYMLINK_MODE,
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'blob',
+ currentPath: '/',
+ mode: FILE_SYMLINK_MODE,
+ },
});
- await nextTick();
- expect(vm.element).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
});
- it('renders table row for path with special character', async () => {
+ it('renders table row for path with special character', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test$/test',
- type: 'file',
- currentPath: 'test$',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test$/test',
+ type: 'file',
+ currentPath: 'test$',
+ },
});
- await nextTick();
- expect(vm.element).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
});
it('renders a gl-hover-load directive', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'blob',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'blob',
+ currentPath: '/',
+ },
});
const hoverLoadDirective = getBinding(findRouterLink().element, 'gl-hover-load');
@@ -111,150 +128,162 @@ describe('Repository table row component', () => {
${'tree'} | ${RouterLinkStub} | ${'RouterLink'}
${'blob'} | ${RouterLinkStub} | ${'RouterLink'}
${'commit'} | ${'a'} | ${'hyperlink'}
- `('renders a $componentName for type $type', async ({ type, component }) => {
+ `('renders a $componentName for type $type', ({ type, component }) => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type,
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type,
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.findComponent(component).exists()).toBe(true);
+ expect(wrapper.findComponent(component).exists()).toBe(true);
});
it.each`
path
${'test#'}
${'Änderungen'}
- `('renders link for $path', async ({ path }) => {
+ `('renders link for $path', ({ path }) => {
factory({
- id: '1',
- sha: '123',
- path,
- type: 'tree',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path,
+ type: 'tree',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.findComponent({ ref: 'link' }).props('to')).toEqual({
+ expect(wrapper.findComponent({ ref: 'link' }).props('to')).toEqual({
path: `/-/tree/main/${encodeURIComponent(path)}`,
});
});
- it('renders link for directory with hash', async () => {
+ it('renders link for directory with hash', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test#',
- type: 'tree',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test#',
+ type: 'tree',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.find('.tree-item-link').props('to')).toEqual({ path: '/-/tree/main/test%23' });
+ expect(wrapper.find('.tree-item-link').props('to')).toEqual({ path: '/-/tree/main/test%23' });
});
- it('renders commit ID for submodule', async () => {
+ it('renders commit ID for submodule', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'commit',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'commit',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.find('.commit-sha').text()).toContain('1');
+ expect(wrapper.find('.commit-sha').text()).toContain('1');
});
- it('renders link with href', async () => {
+ it('renders link with href', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'blob',
- url: 'https://test.com',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'blob',
+ url: 'https://test.com',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.find('a').attributes('href')).toEqual('https://test.com');
+ expect(wrapper.find('a').attributes('href')).toEqual('https://test.com');
});
- it('renders LFS badge', async () => {
+ it('renders LFS badge', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'commit',
- currentPath: '/',
- lfsOid: '1',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'commit',
+ currentPath: '/',
+ lfsOid: '1',
+ },
});
- await nextTick();
- expect(vm.findComponent(GlBadge).exists()).toBe(true);
+ expect(findBadge().exists()).toBe(true);
});
- it('renders commit and web links with href for submodule', async () => {
+ it('renders commit and web links with href for submodule', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'commit',
- url: 'https://test.com',
- submoduleTreeUrl: 'https://test.com/commit',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'commit',
+ url: 'https://test.com',
+ submoduleTreeUrl: 'https://test.com/commit',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.find('a').attributes('href')).toEqual('https://test.com');
- expect(vm.findComponent(GlLink).attributes('href')).toEqual('https://test.com/commit');
+ expect(wrapper.find('a').attributes('href')).toEqual('https://test.com');
+ expect(wrapper.findComponent(GlLink).attributes('href')).toEqual('https://test.com/commit');
});
- it('renders lock icon', async () => {
+ it('renders lock icon', () => {
factory({
- id: '1',
- sha: '123',
- path: 'test',
- type: 'tree',
- currentPath: '/',
+ propsData: {
+ id: '1',
+ sha: '123',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ },
});
- await nextTick();
- expect(vm.findComponent(GlIcon).exists()).toBe(true);
- expect(vm.findComponent(GlIcon).props('name')).toBe('lock');
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe('lock');
});
it('renders loading icon when path is loading', () => {
factory({
- id: '1',
- sha: '1',
- path: 'test',
- type: 'tree',
- currentPath: '/',
- loadingPath: 'test',
+ propsData: {
+ id: '1',
+ sha: '1',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ loadingPath: 'test',
+ },
});
- expect(vm.findComponent(FileIcon).props('loading')).toBe(true);
+ expect(findFileIcon().props('loading')).toBe(true);
});
describe('row visibility', () => {
beforeEach(() => {
factory({
- id: '1',
- sha: '1',
- path: 'test',
- type: 'tree',
- currentPath: '/',
- commitInfo: null,
+ propsData: {
+ id: '1',
+ sha: '1',
+ path: 'test',
+ type: 'tree',
+ currentPath: '/',
+ commitInfo: null,
+ },
});
});
afterAll(() => jest.useRealTimers());
- it('emits a `row-appear` event', async () => {
+ it('emits a `row-appear` event', () => {
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
findIntersectionObserver().vm.$emit('appear');
@@ -262,7 +291,7 @@ describe('Repository table row component', () => {
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
expect(setTimeoutSpy).toHaveBeenLastCalledWith(expect.any(Function), ROW_APPEAR_DELAY);
- expect(vm.emitted('row-appear')).toEqual([[123]]);
+ expect(wrapper.emitted('row-appear')).toEqual([[123]]);
});
});
});
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index f694c8e9166..8d45e24e9e6 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,216 +1,170 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
import TreeContent from 'jh_else_ce/repository/components/tree_content.vue';
+import { TREE_PAGE_LIMIT, i18n } from '~/repository/constants';
import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
+import createApolloProvider from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { i18n } from '~/repository/constants';
-import { graphQLErrors } from '../mock_data';
+import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
+import projectPathQuery from '~/repository/queries/project_path.query.graphql';
+
+import { createAlert } from '~/alert';
+import { graphQLErrors, paginatedTreeResponseFactory } from '../mock_data';
jest.mock('~/repository/commits_service', () => ({
loadCommits: jest.fn(() => Promise.resolve()),
isRequested: jest.fn(),
resetRequestedCommits: jest.fn(),
}));
-jest.mock('~/flash');
-
-let vm;
-let $apollo;
-const mockResponse = jest.fn().mockReturnValue(Promise.resolve({ data: {} }));
-
-function factory(path, appoloMockResponse = mockResponse) {
- $apollo = {
- query: appoloMockResponse,
- };
-
- vm = shallowMount(TreeContent, {
- propsData: {
- path,
- },
- mocks: {
- $apollo,
- },
- provide: {
- glFeatures: {
- increasePageSizeExponentially: true,
- paginatedTreeGraphqlQuery: true,
- },
- },
- });
-}
+jest.mock('~/alert');
describe('Repository table component', () => {
- const findFileTable = () => vm.findComponent(FileTable);
-
- afterEach(() => {
- vm.destroy();
- });
-
- it('renders file preview', async () => {
- factory('/');
+ Vue.use(VueApollo);
+ let wrapper;
+
+ const paginatedTreeResponseWithMoreThanLimit = jest
+ .fn()
+ .mockResolvedValue(paginatedTreeResponseFactory({ numberOfBlobs: TREE_PAGE_LIMIT + 2 }));
+ const paginatedTreeQueryResponseHandler = jest
+ .fn()
+ .mockResolvedValue(paginatedTreeResponseFactory());
+ const findFileTable = () => wrapper.findComponent(FileTable);
+
+ const createComponent = ({
+ path = '/',
+ responseHandler = paginatedTreeQueryResponseHandler,
+ } = {}) => {
+ const apolloProvider = createApolloProvider([[paginatedTreeQuery, responseHandler]]);
+
+ apolloProvider.clients.defaultClient.cache.writeQuery({
+ query: projectPathQuery,
+ data: {
+ projectPath: path,
+ },
+ });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ entries: { blobs: [{ name: 'README.md' }] } });
+ wrapper = shallowMount(TreeContent, {
+ apolloProvider,
+ propsData: {
+ path,
+ },
+ });
+ };
+ it('renders file preview when the response has README.md', async () => {
+ const paginatedTreeResponseWithReadMe = jest
+ .fn()
+ .mockResolvedValue(paginatedTreeResponseFactory({ numberOfBlobs: 1, blobHasReadme: true }));
+ createComponent({ responseHandler: paginatedTreeResponseWithReadMe });
await nextTick();
- expect(vm.findComponent(FilePreview).exists()).toBe(true);
+ await waitForPromises();
+ expect(wrapper.findComponent(FilePreview).exists()).toBe(true);
});
- it('trigger fetchFiles and resetRequestedCommits when mounted', async () => {
- factory('/');
-
- jest.spyOn(vm.vm, 'fetchFiles').mockImplementation(() => {});
+ it('calls tree response handler and resetRequestedCommits when mounted', async () => {
+ createComponent();
await nextTick();
- expect(vm.vm.fetchFiles).toHaveBeenCalled();
+ expect(paginatedTreeQueryResponseHandler).toHaveBeenCalled();
expect(resetRequestedCommits).toHaveBeenCalled();
});
describe('normalizeData', () => {
- it('normalizes edge nodes', () => {
- factory('/');
+ it('normalizes edge nodes', async () => {
+ createComponent();
- const output = vm.vm.normalizeData('blobs', { nodes: ['1', '2'] });
+ await nextTick();
+ await waitForPromises();
- expect(output).toEqual(['1', '2']);
- });
- });
+ const [
+ paginatedTreeNode,
+ ] = paginatedTreeResponseFactory().data.project.repository.paginatedTree.nodes;
- describe('hasNextPage', () => {
- it('returns undefined when hasNextPage is false', () => {
- factory('/');
+ const {
+ blobs: { nodes: blobs },
+ trees: { nodes: trees },
+ submodules: { nodes: submodules },
+ } = paginatedTreeNode;
- const output = vm.vm.hasNextPage({
- trees: { pageInfo: { hasNextPage: false } },
- submodules: { pageInfo: { hasNextPage: false } },
- blobs: { pageInfo: { hasNextPage: false } },
+ expect(findFileTable().props('entries')).toEqual({
+ blobs,
+ trees,
+ submodules,
});
-
- expect(output).toBe(undefined);
});
+ });
- it('returns pageInfo object when hasNextPage is true', () => {
- factory('/');
+ describe('when there is next page', () => {
+ it('make sure it has the correct props to filetable', async () => {
+ createComponent({ responseHandler: paginatedTreeResponseWithMoreThanLimit });
- const output = vm.vm.hasNextPage({
- trees: { pageInfo: { hasNextPage: false } },
- submodules: { pageInfo: { hasNextPage: false } },
- blobs: { pageInfo: { hasNextPage: true, nextCursor: 'test' } },
- });
+ await nextTick();
+ await waitForPromises();
- expect(output).toEqual({ hasNextPage: true, nextCursor: 'test' });
+ expect(findFileTable().props('hasMore')).toBe(true);
});
});
- describe('FileTable showMore', () => {
- describe('when is present', () => {
+ describe('FileTable', () => {
+ describe('when "showMore" event is emitted', () => {
beforeEach(async () => {
- factory('/');
- });
-
- it('is changes hasShowMore to false when "showMore" event is emitted', async () => {
- findFileTable().vm.$emit('showMore');
-
+ createComponent();
await nextTick();
-
- expect(vm.vm.hasShowMore).toBe(false);
+ await waitForPromises();
});
- it('changes clickedShowMore when "showMore" event is emitted', async () => {
+ it('changes hasShowMore to false', async () => {
findFileTable().vm.$emit('showMore');
await nextTick();
- expect(vm.vm.clickedShowMore).toBe(true);
+ expect(findFileTable().props('hasMore')).toBe(false);
});
- it('triggers fetchFiles when "showMore" event is emitted', () => {
- jest.spyOn(vm.vm, 'fetchFiles');
-
+ it('triggers the tree responseHandler', () => {
findFileTable().vm.$emit('showMore');
- expect(vm.vm.fetchFiles).toHaveBeenCalled();
+ expect(paginatedTreeQueryResponseHandler).toHaveBeenCalled();
});
});
- it('is not rendered if less than 1000 files', async () => {
- factory('/');
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ fetchCounter: 5, clickedShowMore: false });
-
- await nextTick();
-
- expect(vm.vm.hasShowMore).toBe(false);
- });
-
- it.each`
- totalBlobs | pagesLoaded | limitReached
- ${900} | ${1} | ${false}
- ${1000} | ${1} | ${true}
- ${1002} | ${1} | ${true}
- ${1002} | ${2} | ${false}
- ${1900} | ${2} | ${false}
- ${2000} | ${2} | ${true}
- `('has limit of 1000 entries per page', async ({ totalBlobs, pagesLoaded, limitReached }) => {
- factory('/');
-
- const blobs = new Array(totalBlobs).fill('fakeBlob');
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ entries: { blobs }, pagesLoaded });
-
- await nextTick();
-
- expect(findFileTable().props('hasMore')).toBe(limitReached);
- });
-
- it.each`
- fetchCounter | pageSize
- ${0} | ${10}
- ${2} | ${30}
- ${4} | ${50}
- ${6} | ${70}
- ${8} | ${90}
- ${10} | ${100}
- ${20} | ${100}
- ${100} | ${100}
- ${200} | ${100}
- `('exponentially increases page size, to a maximum of 100', ({ fetchCounter, pageSize }) => {
- factory('/');
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ fetchCounter });
-
- vm.vm.fetchFiles();
-
- expect($apollo.query).toHaveBeenCalledWith({
- query: paginatedTreeQuery,
- variables: {
- pageSize,
- nextPageCursor: '',
- path: '/',
- projectPath: '',
- ref: '',
+ describe('"hasMore" props is correctly computed with the limit to 1000 per page', () => {
+ it.each`
+ totalBlobs | limitReached
+ ${500} | ${false}
+ ${900} | ${false}
+ ${1000} | ${true}
+ ${1002} | ${true}
+ ${2000} | ${true}
+ `(
+ 'is `$limitReached` when the number of entries is `$totalBlobs`',
+ async ({ totalBlobs, limitReached }) => {
+ const paginatedTreeResponseHandler = jest
+ .fn()
+ .mockResolvedValue(paginatedTreeResponseFactory({ numberOfBlobs: totalBlobs }));
+ createComponent({ responseHandler: paginatedTreeResponseHandler });
+
+ await nextTick();
+ await waitForPromises();
+
+ expect(findFileTable().props('hasMore')).toBe(limitReached);
},
- });
+ );
});
});
describe('commit data', () => {
- const path = 'some/path';
+ const path = '';
it('loads commit data for both top and bottom batches when row-appear event is emitted', () => {
const rowNumber = 50;
- factory(path);
+ createComponent({ path });
findFileTable().vm.$emit('row-appear', rowNumber);
expect(isRequested).toHaveBeenCalledWith(rowNumber);
@@ -222,7 +176,7 @@ describe('Repository table component', () => {
});
it('loads commit data once if rowNumber is zero', () => {
- factory(path);
+ createComponent({ path });
findFileTable().vm.$emit('row-appear', 0);
expect(loadCommits.mock.calls).toEqual([['', path, '', 0]]);
@@ -235,10 +189,13 @@ describe('Repository table component', () => {
error | message
${gitalyError} | ${i18n.gitalyError}
${'Error'} | ${i18n.generalError}
- `('should show an expected error', async ({ error, message }) => {
- factory('/', jest.fn().mockRejectedValue(error));
- await waitForPromises();
- expect(createAlert).toHaveBeenCalledWith({ message, captureError: true });
- });
+ `(
+ `when the graphql error is "$error" shows the message "$message"`,
+ async ({ error, message }) => {
+ createComponent({ path: '/', responseHandler: jest.fn().mockRejectedValue(error) });
+ await waitForPromises();
+ expect(createAlert).toHaveBeenCalledWith({ message, captureError: true });
+ },
+ );
});
});
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index 9de0666f27a..319321cfcb4 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -4,13 +4,13 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
joinPaths: () => '/new_upload',
@@ -53,14 +53,9 @@ describe('UploadBlobModal', () => {
const findBranchName = () => wrapper.findComponent(GlFormInput);
const findMrToggle = () => wrapper.findComponent(GlToggle);
const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
- const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes[0].disabled;
- const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes[0].disabled;
- const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes[0].loading;
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
+ const actionButtonDisabledState = () => findModal().props('actionPrimary').attributes.disabled;
+ const cancelButtonDisabledState = () => findModal().props('actionCancel').attributes.disabled;
+ const actionButtonLoadingState = () => findModal().props('actionPrimary').attributes.loading;
describe.each`
canPushCode | displayBranchName | displayForkedBranchMessage
@@ -110,9 +105,7 @@ describe('UploadBlobModal', () => {
if (canPushCode) {
describe('when changing the branch name', () => {
it('displays the MR toggle', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ target: 'Not main' });
+ createComponent({ targetBranch: 'Not main' });
await nextTick();
@@ -123,12 +116,10 @@ describe('UploadBlobModal', () => {
describe('completed form', () => {
beforeEach(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- file: { type: 'jpg' },
- filePreviewURL: 'http://file.com?format=jpg',
- });
+ findUploadDropzone().vm.$emit(
+ 'change',
+ new File(['http://file.com?format=jpg'], 'file.jpg'),
+ );
});
it('enables the upload button when the form is completed', () => {
@@ -184,7 +175,7 @@ describe('UploadBlobModal', () => {
await waitForPromises();
});
- it('creates a flash error', () => {
+ it('creates an alert error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Error uploading file. Please try again.',
});
@@ -199,13 +190,6 @@ describe('UploadBlobModal', () => {
);
describe('blob file submission type', () => {
- const submitForm = async () => {
- wrapper.vm.uploadFile = jest.fn();
- wrapper.vm.replaceFile = jest.fn();
- wrapper.vm.submitForm();
- await nextTick();
- };
-
const submitRequest = async () => {
mock = new MockAdapter(axios);
findModal().vm.$emit('primary', mockEvent);
@@ -225,13 +209,6 @@ describe('UploadBlobModal', () => {
expect(findModal().props('actionPrimary').text).toBe('Upload file');
});
- it('calls the default uploadFile when the form submit', async () => {
- await submitForm();
-
- expect(wrapper.vm.uploadFile).toHaveBeenCalled();
- expect(wrapper.vm.replaceFile).not.toHaveBeenCalled();
- });
-
it('makes a POST request', async () => {
await submitRequest();
@@ -261,13 +238,6 @@ describe('UploadBlobModal', () => {
expect(findModal().props('actionPrimary').text).toBe(primaryBtnText);
});
- it('calls the replaceFile when the form submit', async () => {
- await submitForm();
-
- expect(wrapper.vm.replaceFile).toHaveBeenCalled();
- expect(wrapper.vm.uploadFile).not.toHaveBeenCalled();
- });
-
it('makes a PUT request', async () => {
await submitRequest();
diff --git a/spec/frontend/repository/mixins/highlight_mixin_spec.js b/spec/frontend/repository/mixins/highlight_mixin_spec.js
index 7c48fe440d2..5f872749581 100644
--- a/spec/frontend/repository/mixins/highlight_mixin_spec.js
+++ b/spec/frontend/repository/mixins/highlight_mixin_spec.js
@@ -44,8 +44,6 @@ describe('HighlightMixin', () => {
beforeEach(() => createComponent());
- afterEach(() => wrapper.destroy());
-
describe('initHighlightWorker', () => {
const firstSeventyLines = contentArray.slice(0, LINES_PER_CHUNK).join('\n');
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index 04ffe52bc3f..399341d23a0 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -123,6 +123,78 @@ export const propsForkInfo = {
selectedBranch: 'main',
sourceName: 'gitLab',
sourcePath: 'gitlab-org/gitlab',
+ canSyncBranch: true,
aheadComparePath: '/nataliia/myGitLab/-/compare/main...ref?from_project_id=1',
behindComparePath: 'gitlab-org/gitlab/-/compare/ref...main?from_project_id=2',
+ createMrPath: 'path/to/new/mr',
};
+
+export const propsConflictsModal = {
+ sourceDefaultBranch: 'branch-name',
+ sourceName: 'source-name',
+ sourcePath: 'path/to/project',
+ selectedBranch: 'my-branch',
+};
+
+export const paginatedTreeResponseFactory = ({
+ numberOfBlobs = 3,
+ numberOfTrees = 3,
+ hasNextPage = false,
+ blobHasReadme = false,
+} = {}) => ({
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/278964',
+ __typename: 'Project',
+ repository: {
+ __typename: 'Repository',
+ paginatedTree: {
+ __typename: 'TreeConnection',
+ pageInfo: {
+ __typename: 'PageInfo',
+ endCursor: hasNextPage ? 'aaa' : '',
+ startCursor: '',
+ hasNextPage,
+ },
+ nodes: [
+ {
+ __typename: 'Tree',
+ trees: {
+ __typename: 'TreeEntryConnection',
+ nodes: new Array(numberOfTrees).fill({
+ __typename: 'TreeEntry',
+ id:
+ 'gid://gitlab/Gitlab::Graphql::Representation::TreeEntry/dc36320ac91aca2f890a31458c9e9920159e68a3',
+ sha: 'dc36320ac91aca2f890a31458c9e9920159e68ae',
+ name: 'gitlab-resize-image',
+ flatPath: 'workhorse/cmd/gitlab-resize-image',
+ type: 'tree',
+ webPath: '/gitlab-org/gitlab/-/tree/master/workhorse/cmd/gitlab-resize-image',
+ }),
+ },
+ submodules: {
+ __typename: 'SubmoduleConnection',
+ nodes: [],
+ },
+ blobs: {
+ __typename: 'BlobConnection',
+ nodes: new Array(numberOfBlobs).fill({
+ __typename: 'Blob',
+ id:
+ 'gid://gitlab/Gitlab::Graphql::Representation::TreeEntry/99712dbc6b26ff92c15bf93449ea09df38adfb10',
+ sha: '99712dbc6b26ff92c15bf93449ea09df38adfb1b',
+ name: blobHasReadme ? 'README.md' : 'fakeBlob',
+ flatPath: blobHasReadme ? 'README.md' : 'fakeBlob',
+ type: 'blob',
+ mode: '100644',
+ webPath: '/gitlab-org/gitlab-build-images/-/blob/master/README.md',
+ lfsOid: null,
+ }),
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+});
diff --git a/spec/frontend/repository/pages/blob_spec.js b/spec/frontend/repository/pages/blob_spec.js
index 4fe6188370e..366523e2b8b 100644
--- a/spec/frontend/repository/pages/blob_spec.js
+++ b/spec/frontend/repository/pages/blob_spec.js
@@ -16,10 +16,6 @@ describe('Repository blob page component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a Blob Content Viewer component', () => {
expect(findBlobContentViewer().exists()).toBe(true);
expect(findBlobContentViewer().props('path')).toBe(path);
diff --git a/spec/frontend/repository/pages/index_spec.js b/spec/frontend/repository/pages/index_spec.js
index 559257d414c..e50557e7d61 100644
--- a/spec/frontend/repository/pages/index_spec.js
+++ b/spec/frontend/repository/pages/index_spec.js
@@ -13,8 +13,6 @@ describe('Repository index page component', () => {
}
afterEach(() => {
- wrapper.destroy();
-
updateElementsVisibility.mockClear();
});
diff --git a/spec/frontend/repository/pages/tree_spec.js b/spec/frontend/repository/pages/tree_spec.js
index 36662696c91..b1529d77c7d 100644
--- a/spec/frontend/repository/pages/tree_spec.js
+++ b/spec/frontend/repository/pages/tree_spec.js
@@ -12,8 +12,6 @@ describe('Repository tree page component', () => {
}
afterEach(() => {
- wrapper.destroy();
-
updateElementsVisibility.mockClear();
});
diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js
index f51d51ee182..e3a04796f7b 100644
--- a/spec/frontend/right_sidebar_spec.js
+++ b/spec/frontend/right_sidebar_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlOpenIssues from 'test_fixtures/issues/open-issue.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import Sidebar from '~/right_sidebar';
@@ -26,11 +27,10 @@ const assertSidebarState = (state) => {
describe('RightSidebar', () => {
describe('fixture tests', () => {
- const fixtureName = 'issues/open-issue.html';
let mock;
beforeEach(() => {
- loadHTMLFixture(fixtureName);
+ setHTMLFixture(htmlOpenIssues);
mock = new MockAdapter(axios);
new Sidebar(); // eslint-disable-line no-new
$aside = $('.right-sidebar');
diff --git a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
deleted file mode 100644
index 3abdfcdaf20..00000000000
--- a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
+++ /dev/null
@@ -1,21 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Saved replies list item component renders list item 1`] = `
-<li
- class="gl-mb-5"
->
- <div
- class="gl-display-flex gl-align-items-center"
- >
- <strong>
- test
- </strong>
- </div>
-
- <div
- class="gl-mt-3 gl-font-monospace"
- >
- /assign_reviewer
- </div>
-</li>
-`;
diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js
deleted file mode 100644
index cad1000473b..00000000000
--- a/spec/frontend/saved_replies/components/list_item_spec.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import ListItem from '~/saved_replies/components/list_item.vue';
-
-let wrapper;
-
-function createComponent(propsData = {}) {
- return shallowMount(ListItem, {
- propsData,
- });
-}
-
-describe('Saved replies list item component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders list item', async () => {
- wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-});
diff --git a/spec/frontend/saved_replies/components/list_spec.js b/spec/frontend/saved_replies/components/list_spec.js
deleted file mode 100644
index 66e9ddfe148..00000000000
--- a/spec/frontend/saved_replies/components/list_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import Vue from 'vue';
-import { mount } from '@vue/test-utils';
-import VueApollo from 'vue-apollo';
-import noSavedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies_empty.query.graphql.json';
-import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import List from '~/saved_replies/components/list.vue';
-import ListItem from '~/saved_replies/components/list_item.vue';
-import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql';
-
-let wrapper;
-
-function createMockApolloProvider(response) {
- Vue.use(VueApollo);
-
- const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]];
-
- return createMockApollo(requestHandlers);
-}
-
-function createComponent(options = {}) {
- const { mockApollo } = options;
-
- return mount(List, {
- apolloProvider: mockApollo,
- });
-}
-
-describe('Saved replies list component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('does not render any list items when response is empty', async () => {
- const mockApollo = createMockApolloProvider(noSavedRepliesResponse);
- wrapper = createComponent({ mockApollo });
-
- await waitForPromises();
-
- expect(wrapper.findAllComponents(ListItem).length).toBe(0);
- });
-
- it('render saved replies count', async () => {
- const mockApollo = createMockApolloProvider(savedRepliesResponse);
- wrapper = createComponent({ mockApollo });
-
- await waitForPromises();
-
- expect(wrapper.find('[data-testid="title"]').text()).toEqual('My saved replies (2)');
- });
-
- it('renders list of saved replies', async () => {
- const mockApollo = createMockApolloProvider(savedRepliesResponse);
- const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
- wrapper = createComponent({ mockApollo });
-
- await waitForPromises();
-
- expect(wrapper.findAllComponents(ListItem).length).toBe(2);
- expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual(
- expect.objectContaining(savedReplies[0]),
- );
- expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual(
- expect.objectContaining(savedReplies[1]),
- );
- });
-});
diff --git a/spec/frontend/scripts/frontend/__fixtures__/locale/de/converted.json b/spec/frontend/scripts/frontend/__fixtures__/locale/de/converted.json
new file mode 100644
index 00000000000..570980b1c27
--- /dev/null
+++ b/spec/frontend/scripts/frontend/__fixtures__/locale/de/converted.json
@@ -0,0 +1,21 @@
+{
+ "domain": "app",
+ "locale_data": {
+ "app": {
+ "": {
+ "domain": "app",
+ "lang": "de"
+ },
+ " %{start} to %{end}": [
+ " %{start} bis %{end}"
+ ],
+ "%d Alert:": [
+ "%d Warnung:",
+ "%d Warnungen:"
+ ],
+ "Example": [
+ ""
+ ]
+ }
+ }
+}
diff --git a/spec/frontend/scripts/frontend/__fixtures__/locale/de/gitlab.po b/spec/frontend/scripts/frontend/__fixtures__/locale/de/gitlab.po
new file mode 100644
index 00000000000..fe80cb72c29
--- /dev/null
+++ b/spec/frontend/scripts/frontend/__fixtures__/locale/de/gitlab.po
@@ -0,0 +1,13 @@
+# Simple translated string
+msgid " %{start} to %{end}"
+msgstr " %{start} bis %{end}"
+
+# Simple translated, pluralized string
+msgid "%d Alert:"
+msgid_plural "%d Alerts:"
+msgstr[0] "%d Warnung:"
+msgstr[1] "%d Warnungen:"
+
+# Simple string without translation
+msgid "Example"
+msgstr ""
diff --git a/spec/frontend/scripts/frontend/po_to_json_spec.js b/spec/frontend/scripts/frontend/po_to_json_spec.js
new file mode 100644
index 00000000000..858e3c9d3c7
--- /dev/null
+++ b/spec/frontend/scripts/frontend/po_to_json_spec.js
@@ -0,0 +1,244 @@
+import { join } from 'path';
+import { tmpdir } from 'os';
+import { readFile, rm, mkdtemp, stat } from 'fs/promises';
+import {
+ convertPoToJed,
+ convertPoFileForLocale,
+ main,
+} from '../../../../scripts/frontend/po_to_json';
+
+describe('PoToJson', () => {
+ const LOCALE = 'de';
+ const LOCALE_DIR = join(__dirname, '__fixtures__/locale');
+ const PO_FILE = join(LOCALE_DIR, LOCALE, 'gitlab.po');
+ const CONVERTED_FILE = join(LOCALE_DIR, LOCALE, 'converted.json');
+ let DE_CONVERTED = null;
+
+ beforeAll(async () => {
+ DE_CONVERTED = Object.freeze(JSON.parse(await readFile(CONVERTED_FILE, 'utf-8')));
+ });
+
+ describe('tests writing to the file system', () => {
+ let resultDir = null;
+
+ afterEach(async () => {
+ if (resultDir) {
+ await rm(resultDir, { recursive: true, force: true });
+ }
+ });
+
+ beforeEach(async () => {
+ resultDir = await mkdtemp(join(tmpdir(), 'locale-test'));
+ });
+
+ describe('#main', () => {
+ it('throws without arguments', () => {
+ return expect(main()).rejects.toThrow(/doesn't seem to be a folder/);
+ });
+
+ it('throws if outputDir does not exist', () => {
+ return expect(
+ main({
+ localeRoot: LOCALE_DIR,
+ outputDir: 'i-do-not-exist',
+ }),
+ ).rejects.toThrow(/doesn't seem to be a folder/);
+ });
+
+ it('throws if localeRoot does not exist', () => {
+ return expect(
+ main({
+ localeRoot: 'i-do-not-exist',
+ outputDir: resultDir,
+ }),
+ ).rejects.toThrow(/doesn't seem to be a folder/);
+ });
+
+ it('converts folder of po files to app.js files', async () => {
+ expect((await stat(resultDir)).isDirectory()).toBe(true);
+ await main({ localeRoot: LOCALE_DIR, outputDir: resultDir });
+
+ const resultFile = join(resultDir, LOCALE, 'app.js');
+ expect((await stat(resultFile)).isFile()).toBe(true);
+
+ window.translations = null;
+ await import(resultFile);
+ expect(window.translations).toEqual(DE_CONVERTED);
+ });
+ });
+
+ describe('#convertPoFileForLocale', () => {
+ it('converts simple PO to app.js, which exposes translations on the window', async () => {
+ await convertPoFileForLocale({ locale: 'de', localeFile: PO_FILE, resultDir });
+
+ const resultFile = join(resultDir, 'app.js');
+ expect((await stat(resultFile)).isFile()).toBe(true);
+
+ window.translations = null;
+ await import(resultFile);
+ expect(window.translations).toEqual(DE_CONVERTED);
+ });
+ });
+ });
+
+ describe('#convertPoToJed', () => {
+ it('converts simple PO to JED compatible JSON', async () => {
+ const poContent = await readFile(PO_FILE, 'utf-8');
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual(DE_CONVERTED);
+ });
+
+ it('returns null for empty string', () => {
+ const poContent = '';
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual(null);
+ });
+
+ describe('PO File headers', () => {
+ it('parses headers properly', () => {
+ const poContent = `
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab-ee\\n"
+"Report-Msgid-Bugs-To: \\n"
+"X-Crowdin-Project: gitlab-ee\\n"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ 'Project-Id-Version': 'gitlab-ee',
+ 'Report-Msgid-Bugs-To': '',
+ 'X-Crowdin-Project': 'gitlab-ee',
+ domain: 'app',
+ lang: LOCALE,
+ },
+ },
+ },
+ });
+ });
+
+ // JED needs that property, hopefully we could get
+ // rid of this in a future iteration
+ it("exposes 'Plural-Forms' as 'plural_forms' for `jed`", () => {
+ const poContent = `
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ 'Plural-Forms': 'nplurals=2; plural=(n != 1);',
+ plural_forms: 'nplurals=2; plural=(n != 1);',
+ domain: 'app',
+ lang: LOCALE,
+ },
+ },
+ },
+ });
+ });
+
+ it('removes POT-Creation-Date', () => {
+ const poContent = `
+msgid ""
+msgstr ""
+"Plural-Forms: nplurals=2; plural=(n != 1);\\n"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ 'Plural-Forms': 'nplurals=2; plural=(n != 1);',
+ plural_forms: 'nplurals=2; plural=(n != 1);',
+ domain: 'app',
+ lang: LOCALE,
+ },
+ },
+ },
+ });
+ });
+ });
+
+ describe('escaping', () => {
+ it('escapes quotes in msgid and translation', () => {
+ const poContent = `
+# Escaped quotes in msgid and msgstr
+msgid "Changes the title to \\"%{title_param}\\"."
+msgstr "Ändert den Titel in \\"%{title_param}\\"."
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ domain: 'app',
+ lang: LOCALE,
+ },
+ 'Changes the title to \\"%{title_param}\\".': [
+ 'Ändert den Titel in \\"%{title_param}\\".',
+ ],
+ },
+ },
+ });
+ });
+
+ it('escapes backslashes in msgid and translation', () => {
+ const poContent = `
+# Escaped backslashes in msgid and msgstr
+msgid "Example: ssh\\\\:\\\\/\\\\/"
+msgstr "Beispiel: ssh\\\\:\\\\/\\\\/"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ domain: 'app',
+ lang: LOCALE,
+ },
+ 'Example: ssh\\\\:\\\\/\\\\/': ['Beispiel: ssh\\\\:\\\\/\\\\/'],
+ },
+ },
+ });
+ });
+
+ // This is potentially faulty behavior but demands further investigation
+ // See also the escapeMsgstr method
+ it('escapes \\n and \\t in translation', () => {
+ const poContent = `
+# Escaped \\n
+msgid "Outdent line"
+msgstr "Désindenter la ligne\\n"
+
+# Escaped \\t
+msgid "Headers"
+msgstr "Cabeçalhos\\t"
+`;
+
+ expect(convertPoToJed(poContent, LOCALE).jed).toEqual({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ domain: 'app',
+ lang: LOCALE,
+ },
+ Headers: ['Cabeçalhos\\t'],
+ 'Outdent line': ['Désindenter la ligne\\n'],
+ },
+ },
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/highlight_blob_search_result_spec.js b/spec/frontend/search/highlight_blob_search_result_spec.js
index 15cff436076..91fc97c15ae 100644
--- a/spec/frontend/search/highlight_blob_search_result_spec.js
+++ b/spec/frontend/search/highlight_blob_search_result_spec.js
@@ -1,11 +1,11 @@
+import htmlPipelineSchedulesEdit from 'test_fixtures/search/blob_search_result.html';
import setHighlightClass from '~/search/highlight_blob_search_result';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-const fixture = 'search/blob_search_result.html';
const searchKeyword = 'Send'; // spec/frontend/fixtures/search.rb#79
describe('search/highlight_blob_search_result', () => {
- beforeEach(() => loadHTMLFixture(fixture));
+ beforeEach(() => setHTMLFixture(htmlPipelineSchedulesEdit));
afterEach(() => {
resetHTMLFixture();
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index fb9c0a93907..f8dd6f6df27 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -6,6 +6,7 @@ export const MOCK_QUERY = {
state: 'all',
confidential: null,
group_id: 1,
+ language: ['C', 'JavaScript'],
};
export const MOCK_GROUP = {
@@ -193,214 +194,6 @@ export const MOCK_NAVIGATION_ACTION_MUTATION = {
payload: { key: 'projects', count: '13' },
};
-export const MOCK_AGGREGATIONS = [
- {
- name: 'language',
- buckets: [
- { key: 'random-label-edumingos0', count: 1 },
- { key: 'random-label-rbourgourd1', count: 2 },
- { key: 'random-label-dfearnside2', count: 3 },
- { key: 'random-label-gewins3', count: 4 },
- { key: 'random-label-telverstone4', count: 5 },
- { key: 'random-label-ygerriets5', count: 6 },
- { key: 'random-label-lmoffet6', count: 7 },
- { key: 'random-label-ehinnerk7', count: 8 },
- { key: 'random-label-flanceley8', count: 9 },
- { key: 'random-label-adoyle9', count: 10 },
- { key: 'random-label-rmcgirla', count: 11 },
- { key: 'random-label-dwhellansb', count: 12 },
- { key: 'random-label-apitkethlyc', count: 13 },
- { key: 'random-label-senevoldsend', count: 14 },
- { key: 'random-label-tlardnare', count: 15 },
- { key: 'random-label-fcoilsf', count: 16 },
- { key: 'random-label-qgeckg', count: 17 },
- { key: 'random-label-rgrabenh', count: 18 },
- { key: 'random-label-lashardi', count: 19 },
- { key: 'random-label-sadamovitchj', count: 20 },
- { key: 'random-label-rlyddiardk', count: 21 },
- { key: 'random-label-jpoell', count: 22 },
- { key: 'random-label-kcharitym', count: 23 },
- { key: 'random-label-cbertenshawn', count: 24 },
- { key: 'random-label-jsturgeso', count: 25 },
- { key: 'random-label-ohouldcroftp', count: 26 },
- { key: 'random-label-rheijnenq', count: 27 },
- { key: 'random-label-snortheyr', count: 28 },
- { key: 'random-label-vpairpoints', count: 29 },
- { key: 'random-label-odavidovicit', count: 30 },
- { key: 'random-label-fmccartu', count: 31 },
- { key: 'random-label-cwansburyv', count: 32 },
- { key: 'random-label-bdimontw', count: 33 },
- { key: 'random-label-adocketx', count: 34 },
- { key: 'random-label-obavridgey', count: 35 },
- { key: 'random-label-jperezz', count: 36 },
- { key: 'random-label-gdeneve10', count: 37 },
- { key: 'random-label-rmckeand11', count: 38 },
- { key: 'random-label-kwestmerland12', count: 39 },
- { key: 'random-label-mpryer13', count: 40 },
- { key: 'random-label-rmcneil14', count: 41 },
- { key: 'random-label-ablondel15', count: 42 },
- { key: 'random-label-wbalducci16', count: 43 },
- { key: 'random-label-swigley17', count: 44 },
- { key: 'random-label-gferroni18', count: 45 },
- { key: 'random-label-icollings19', count: 46 },
- { key: 'random-label-wszymanski1a', count: 47 },
- { key: 'random-label-jelson1b', count: 48 },
- { key: 'random-label-fsambrook1c', count: 49 },
- { key: 'random-label-kconey1d', count: 50 },
- { key: 'random-label-agoodread1e', count: 51 },
- { key: 'random-label-nmewton1f', count: 52 },
- { key: 'random-label-gcodman1g', count: 53 },
- { key: 'random-label-rpoplee1h', count: 54 },
- { key: 'random-label-mhug1i', count: 55 },
- { key: 'random-label-ggowrie1j', count: 56 },
- { key: 'random-label-ctonepohl1k', count: 57 },
- { key: 'random-label-cstillman1l', count: 58 },
- { key: 'random-label-dcollyer1m', count: 59 },
- { key: 'random-label-idimelow1n', count: 60 },
- { key: 'random-label-djarley1o', count: 61 },
- { key: 'random-label-omclleese1p', count: 62 },
- { key: 'random-label-dstivers1q', count: 63 },
- { key: 'random-label-svose1r', count: 64 },
- { key: 'random-label-clanfare1s', count: 65 },
- { key: 'random-label-aport1t', count: 66 },
- { key: 'random-label-hcarlett1u', count: 67 },
- { key: 'random-label-dstillmann1v', count: 68 },
- { key: 'random-label-ncorpe1w', count: 69 },
- { key: 'random-label-mjacobsohn1x', count: 70 },
- { key: 'random-label-ycleiment1y', count: 71 },
- { key: 'random-label-owherton1z', count: 72 },
- { key: 'random-label-anowaczyk20', count: 73 },
- { key: 'random-label-rmckennan21', count: 74 },
- { key: 'random-label-cmoulding22', count: 75 },
- { key: 'random-label-sswate23', count: 76 },
- { key: 'random-label-cbarge24', count: 77 },
- { key: 'random-label-agrainger25', count: 78 },
- { key: 'random-label-ncosin26', count: 79 },
- { key: 'random-label-pkears27', count: 80 },
- { key: 'random-label-cmcarthur28', count: 81 },
- { key: 'random-label-jmantripp29', count: 82 },
- { key: 'random-label-cjekel2a', count: 83 },
- { key: 'random-label-hdilleway2b', count: 84 },
- { key: 'random-label-lbovaird2c', count: 85 },
- { key: 'random-label-mweld2d', count: 86 },
- { key: 'random-label-marnowitz2e', count: 87 },
- { key: 'random-label-nbertomieu2f', count: 88 },
- { key: 'random-label-mledward2g', count: 89 },
- { key: 'random-label-mhince2h', count: 90 },
- { key: 'random-label-baarons2i', count: 91 },
- { key: 'random-label-kfrancie2j', count: 92 },
- { key: 'random-label-ishooter2k', count: 93 },
- { key: 'random-label-glowmass2l', count: 94 },
- { key: 'random-label-rgeorgi2m', count: 95 },
- { key: 'random-label-bproby2n', count: 96 },
- { key: 'random-label-hsteffan2o', count: 97 },
- { key: 'random-label-doruane2p', count: 98 },
- { key: 'random-label-rlunny2q', count: 99 },
- { key: 'random-label-geles2r', count: 100 },
- { key: 'random-label-nmaggiore2s', count: 101 },
- { key: 'random-label-aboocock2t', count: 102 },
- { key: 'random-label-eguilbert2u', count: 103 },
- { key: 'random-label-emccutcheon2v', count: 104 },
- { key: 'random-label-hcowser2w', count: 105 },
- { key: 'random-label-dspeeding2x', count: 106 },
- { key: 'random-label-oseebright2y', count: 107 },
- { key: 'random-label-hpresdee2z', count: 108 },
- { key: 'random-label-pesseby30', count: 109 },
- { key: 'random-label-hpusey31', count: 110 },
- { key: 'random-label-dmanthorpe32', count: 111 },
- { key: 'random-label-natley33', count: 112 },
- { key: 'random-label-iferentz34', count: 113 },
- { key: 'random-label-adyble35', count: 114 },
- { key: 'random-label-dlockitt36', count: 115 },
- { key: 'random-label-acoxwell37', count: 116 },
- { key: 'random-label-amcgarvey38', count: 117 },
- { key: 'random-label-rmcgougan39', count: 118 },
- { key: 'random-label-mscole3a', count: 119 },
- { key: 'random-label-lmalim3b', count: 120 },
- { key: 'random-label-cends3c', count: 121 },
- { key: 'random-label-dmannie3d', count: 122 },
- { key: 'random-label-lgoodricke3e', count: 123 },
- { key: 'random-label-rcaghy3f', count: 124 },
- { key: 'random-label-mprozillo3g', count: 125 },
- { key: 'random-label-mcardnell3h', count: 126 },
- { key: 'random-label-gericssen3i', count: 127 },
- { key: 'random-label-fspooner3j', count: 128 },
- { key: 'random-label-achadney3k', count: 129 },
- { key: 'random-label-corchard3l', count: 130 },
- { key: 'random-label-lyerill3m', count: 131 },
- { key: 'random-label-jrusk3n', count: 132 },
- { key: 'random-label-lbonelle3o', count: 133 },
- { key: 'random-label-eduny3p', count: 134 },
- { key: 'random-label-mhutchence3q', count: 135 },
- { key: 'random-label-rmargeram3r', count: 136 },
- { key: 'random-label-smaudlin3s', count: 137 },
- { key: 'random-label-sfarrance3t', count: 138 },
- { key: 'random-label-eclendennen3u', count: 139 },
- { key: 'random-label-cyabsley3v', count: 140 },
- { key: 'random-label-ahensmans3w', count: 141 },
- { key: 'random-label-tsenchenko3x', count: 142 },
- { key: 'random-label-ryurchishin3y', count: 143 },
- { key: 'random-label-teby3z', count: 144 },
- { key: 'random-label-dvaillant40', count: 145 },
- { key: 'random-label-kpetyakov41', count: 146 },
- { key: 'random-label-cmorrison42', count: 147 },
- { key: 'random-label-ltwiddy43', count: 148 },
- { key: 'random-label-ineame44', count: 149 },
- { key: 'random-label-blucock45', count: 150 },
- { key: 'random-label-kdunsford46', count: 151 },
- { key: 'random-label-dducham47', count: 152 },
- { key: 'random-label-javramovitz48', count: 153 },
- { key: 'random-label-mascraft49', count: 154 },
- { key: 'random-label-bloughead4a', count: 155 },
- { key: 'random-label-sduckit4b', count: 156 },
- { key: 'random-label-hhardman4c', count: 157 },
- { key: 'random-label-cstaniforth4d', count: 158 },
- { key: 'random-label-jedney4e', count: 159 },
- { key: 'random-label-bobbard4f', count: 160 },
- { key: 'random-label-cgiraux4g', count: 161 },
- { key: 'random-label-tkiln4h', count: 162 },
- { key: 'random-label-jwansbury4i', count: 163 },
- { key: 'random-label-dquinlan4j', count: 164 },
- { key: 'random-label-hgindghill4k', count: 165 },
- { key: 'random-label-jjowle4l', count: 166 },
- { key: 'random-label-egambrell4m', count: 167 },
- { key: 'random-label-jmcgloughlin4n', count: 168 },
- { key: 'random-label-bbabb4o', count: 169 },
- { key: 'random-label-achuck4p', count: 170 },
- { key: 'random-label-tsyers4q', count: 171 },
- { key: 'random-label-jlandon4r', count: 172 },
- { key: 'random-label-wteather4s', count: 173 },
- { key: 'random-label-dfoskin4t', count: 174 },
- { key: 'random-label-gmorlon4u', count: 175 },
- { key: 'random-label-jseely4v', count: 176 },
- { key: 'random-label-cbrass4w', count: 177 },
- { key: 'random-label-fmanilo4x', count: 178 },
- { key: 'random-label-bfrangleton4y', count: 179 },
- { key: 'random-label-vbartkiewicz4z', count: 180 },
- { key: 'random-label-tclymer50', count: 181 },
- { key: 'random-label-pqueen51', count: 182 },
- { key: 'random-label-bpol52', count: 183 },
- { key: 'random-label-jclaeskens53', count: 184 },
- { key: 'random-label-cstranieri54', count: 185 },
- { key: 'random-label-drumbelow55', count: 186 },
- { key: 'random-label-wbrumham56', count: 187 },
- { key: 'random-label-azeal57', count: 188 },
- { key: 'random-label-msnooks58', count: 189 },
- { key: 'random-label-blapre59', count: 190 },
- { key: 'random-label-cduckers5a', count: 191 },
- { key: 'random-label-mgumary5b', count: 192 },
- { key: 'random-label-rtebbs5c', count: 193 },
- { key: 'random-label-eroe5d', count: 194 },
- { key: 'random-label-rconfait5e', count: 195 },
- { key: 'random-label-fsinderland5f', count: 196 },
- { key: 'random-label-tdallywater5g', count: 197 },
- { key: 'random-label-glindenman5h', count: 198 },
- { key: 'random-label-fbauser5i', count: 199 },
- { key: 'random-label-bdownton5j', count: 200 },
- ],
- },
-];
-
export const MOCK_LANGUAGE_AGGREGATIONS_BUCKETS = [
{ key: 'random-label-edumingos0', count: 1 },
{ key: 'random-label-rbourgourd1', count: 2 },
@@ -604,13 +397,27 @@ export const MOCK_LANGUAGE_AGGREGATIONS_BUCKETS = [
{ key: 'random-label-bdownton5j', count: 200 },
];
+export const MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+ },
+];
+
+export const SORTED_MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: MOCK_LANGUAGE_AGGREGATIONS_BUCKETS.reverse(),
+ },
+];
+
export const MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION = [
{
type: types.REQUEST_AGGREGATIONS,
},
{
type: types.RECEIVE_AGGREGATIONS_SUCCESS,
- payload: MOCK_AGGREGATIONS,
+ payload: SORTED_MOCK_AGGREGATIONS,
},
];
@@ -653,3 +460,85 @@ export const TEST_FILTER_DATA = {
JSON: { label: 'JSON', value: 'JSON', count: 15 },
},
};
+
+export const SMALL_MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: TEST_RAW_BUCKETS,
+ },
+];
+
+export const MOCK_NAVIGATION_ITEMS = [
+ {
+ title: 'Projects',
+ icon: 'project',
+ link: '/search?scope=projects&search=et',
+ is_active: false,
+ pill_count: '10K+',
+ items: [],
+ },
+ {
+ title: 'Code',
+ icon: 'code',
+ link: '/search?scope=blobs&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Issues',
+ icon: 'issues',
+ link: '/search?scope=issues&search=et',
+ is_active: true,
+ pill_count: '2.4K',
+ items: [],
+ },
+ {
+ title: 'Merge requests',
+ icon: 'merge-request',
+ link: '/search?scope=merge_requests&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Wiki',
+ icon: 'overview',
+ link: '/search?scope=wiki_blobs&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Commits',
+ icon: 'commit',
+ link: '/search?scope=commits&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Comments',
+ icon: 'comments',
+ link: '/search?scope=notes&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Milestones',
+ icon: 'tag',
+ link: '/search?scope=milestones&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+ {
+ title: 'Users',
+ icon: 'users',
+ link: '/search?scope=users&search=et',
+ is_active: false,
+ pill_count: '0',
+ items: [],
+ },
+];
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 83302b90233..963b73aeae5 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -5,7 +5,7 @@ import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import ResultsFilters from '~/search/sidebar/components/results_filters.vue';
import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
-import LanguageFilter from '~/search/sidebar/components/language_filter.vue';
+import LanguageFilter from '~/search/sidebar/components/language_filter/index.vue';
Vue.use(Vuex);
@@ -17,6 +17,10 @@ describe('GlobalSearchSidebar', () => {
resetQuery: jest.fn(),
};
+ const getterSpies = {
+ currentScope: jest.fn(() => 'issues'),
+ };
+
const createComponent = (initialState, featureFlags) => {
const store = new Vuex.Store({
state: {
@@ -24,6 +28,7 @@ describe('GlobalSearchSidebar', () => {
...initialState,
},
actions: actionSpies,
+ getters: getterSpies,
});
wrapper = shallowMount(GlobalSearchSidebar, {
@@ -52,22 +57,23 @@ describe('GlobalSearchSidebar', () => {
});
describe.each`
- scope | showFilters | ShowsLanguage
+ scope | showFilters | showsLanguage
${'issues'} | ${true} | ${false}
${'merge_requests'} | ${true} | ${false}
${'projects'} | ${false} | ${false}
${'blobs'} | ${false} | ${true}
- `('sidebar scope: $scope', ({ scope, showFilters, ShowsLanguage }) => {
+ `('sidebar scope: $scope', ({ scope, showFilters, showsLanguage }) => {
beforeEach(() => {
- createComponent({ urlQuery: { scope } }, { searchBlobsLanguageAggregation: true });
+ getterSpies.currentScope = jest.fn(() => scope);
+ createComponent({ urlQuery: { scope } });
});
it(`${!showFilters ? "doesn't" : ''} shows filters`, () => {
expect(findFilters().exists()).toBe(showFilters);
});
- it(`${!ShowsLanguage ? "doesn't" : ''} shows language filters`, () => {
- expect(findLanguageAggregation().exists()).toBe(ShowsLanguage);
+ it(`${!showsLanguage ? "doesn't" : ''} shows language filters`, () => {
+ expect(findLanguageAggregation().exists()).toBe(showsLanguage);
});
});
@@ -80,22 +86,4 @@ describe('GlobalSearchSidebar', () => {
});
});
});
-
- describe('when search_blobs_language_aggregation is enabled', () => {
- beforeEach(() => {
- createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: true });
- });
- it('shows the language filter', () => {
- expect(findLanguageAggregation().exists()).toBe(true);
- });
- });
-
- describe('when search_blobs_language_aggregation is disabled', () => {
- beforeEach(() => {
- createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: false });
- });
- it('hides the language filter', () => {
- expect(findLanguageAggregation().exists()).toBe(false);
- });
- });
});
diff --git a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
index 82017754b23..3907e199cae 100644
--- a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
@@ -1,44 +1,55 @@
import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { MOCK_QUERY, MOCK_LANGUAGE_AGGREGATIONS_BUCKETS } from 'jest/search/mock_data';
-import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
+import CheckboxFilter, {
+ TRACKING_LABEL_CHECKBOX,
+ TRACKING_LABEL_SET,
+} from '~/search/sidebar/components/checkbox_filter.vue';
-import { languageFilterData } from '~/search/sidebar/constants/language_filter_data';
+import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
import { convertFiltersData } from '~/search/sidebar/utils';
Vue.use(Vuex);
describe('CheckboxFilter', () => {
let wrapper;
+ let trackingSpy;
const actionSpies = {
setQuery: jest.fn(),
};
+ const getterSpies = {
+ queryLanguageFilters: jest.fn(() => []),
+ };
+
const defaultProps = {
- filterData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ filtersData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ trackingNamespace: 'testNameSpace',
};
- const createComponent = () => {
+ const createComponent = (Props = defaultProps) => {
const store = new Vuex.Store({
state: {
query: MOCK_QUERY,
},
actions: actionSpies,
+ getters: getterSpies,
});
wrapper = shallowMountExtended(CheckboxFilter, {
store,
propsData: {
- ...defaultProps,
+ ...Props,
},
});
};
- beforeEach(() => {
- createComponent();
+ afterEach(() => {
+ unmockTracking();
});
const findFormCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
@@ -47,6 +58,11 @@ describe('CheckboxFilter', () => {
const fintAllCheckboxLabelCounts = () => wrapper.findAllByTestId('labelCount');
describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
it('renders form', () => {
expect(findFormCheckboxGroup().exists()).toBe(true);
});
@@ -71,15 +87,34 @@ describe('CheckboxFilter', () => {
});
describe('actions', () => {
- it('triggers setQuery', () => {
- const filter =
- defaultProps.filterData.filters[Object.keys(defaultProps.filterData.filters)[0]].value;
- findFormCheckboxGroup().vm.$emit('input', filter);
+ const checkedLanguageName = MOCK_LANGUAGE_AGGREGATIONS_BUCKETS[0].key;
+
+ beforeEach(() => {
+ defaultProps.filtersData = convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS.slice(0, 3));
+ CheckboxFilter.computed.selectedFilter.get = jest.fn(() => checkedLanguageName);
+ createComponent();
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ findFormCheckboxGroup().vm.$emit('input', checkedLanguageName);
+ });
+
+ it('triggers setQuery', () => {
expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
key: languageFilterData.filterParam,
- value: filter,
+ value: checkedLanguageName,
});
});
+
+ it('sends tracking information when setQuery', () => {
+ findFormCheckboxGroup().vm.$emit('input', checkedLanguageName);
+ expect(trackingSpy).toHaveBeenCalledWith(
+ defaultProps.trackingNamespace,
+ TRACKING_LABEL_CHECKBOX,
+ {
+ label: TRACKING_LABEL_SET,
+ property: checkedLanguageName,
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index 4f146757454..1f65884e959 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -1,25 +1,52 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
+Vue.use(Vuex);
+
describe('ConfidentialityFilter', () => {
let wrapper;
- const createComponent = (initProps) => {
+ const createComponent = (state) => {
+ const store = new Vuex.Store({
+ state,
+ });
+
wrapper = shallowMount(ConfidentialityFilter, {
- ...initProps,
+ store,
});
};
const findRadioFilter = () => wrapper.findComponent(RadioFilter);
+ const findHR = () => wrapper.findComponent('hr');
- describe('template', () => {
+ describe('old sidebar', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ useNewNavigation: false });
});
it('renders the component', () => {
expect(findRadioFilter().exists()).toBe(true);
});
+
+ it('renders the divider', () => {
+ expect(findHR().exists()).toBe(true);
+ });
+ });
+
+ describe('new sidebar', () => {
+ beforeEach(() => {
+ createComponent({ useNewNavigation: true });
+ });
+
+ it('renders the component', () => {
+ expect(findRadioFilter().exists()).toBe(true);
+ });
+
+ it("doesn't render the divider", () => {
+ expect(findHR().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/filters_spec.js b/spec/frontend/search/sidebar/components/filters_spec.js
index 7e564bfa005..d189c695467 100644
--- a/spec/frontend/search/sidebar/components/filters_spec.js
+++ b/spec/frontend/search/sidebar/components/filters_spec.js
@@ -17,6 +17,10 @@ describe('GlobalSearchSidebarFilters', () => {
resetQuery: jest.fn(),
};
+ const defaultGetters = {
+ currentScope: () => 'issues',
+ };
+
const createComponent = (initialState) => {
const store = new Vuex.Store({
state: {
@@ -24,6 +28,7 @@ describe('GlobalSearchSidebarFilters', () => {
...initialState,
},
actions: actionSpies,
+ getters: defaultGetters,
});
wrapper = shallowMount(ResultsFilters, {
@@ -31,10 +36,6 @@ describe('GlobalSearchSidebarFilters', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSidebarForm = () => wrapper.find('form');
const findStatusFilter = () => wrapper.findComponent(StatusFilter);
const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
@@ -65,7 +66,7 @@ describe('GlobalSearchSidebarFilters', () => {
});
it('disables the button', () => {
- expect(findApplyButton().attributes('disabled')).toBe('true');
+ expect(findApplyButton().attributes('disabled')).toBeDefined();
});
});
@@ -142,7 +143,11 @@ describe('GlobalSearchSidebarFilters', () => {
${'blobs'} | ${false}
`(`ConfidentialityFilter`, ({ scope, showFilter }) => {
beforeEach(() => {
- createComponent({ urlQuery: { scope } });
+ defaultGetters.currentScope = () => scope;
+ createComponent();
+ });
+ afterEach(() => {
+ defaultGetters.currentScope = () => 'issues';
});
it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
@@ -162,7 +167,11 @@ describe('GlobalSearchSidebarFilters', () => {
${'blobs'} | ${false}
`(`StatusFilter`, ({ scope, showFilter }) => {
beforeEach(() => {
- createComponent({ urlQuery: { scope } });
+ defaultGetters.currentScope = () => scope;
+ createComponent();
+ });
+ afterEach(() => {
+ defaultGetters.currentScope = () => 'issues';
});
it(`does${showFilter ? '' : ' not'} render when scope is ${scope}`, () => {
diff --git a/spec/frontend/search/sidebar/components/language_filters_spec.js b/spec/frontend/search/sidebar/components/language_filter_spec.js
index e297d1c33b0..9ad9d095aca 100644
--- a/spec/frontend/search/sidebar/components/language_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/language_filter_spec.js
@@ -1,20 +1,35 @@
import { GlAlert, GlFormCheckbox, GlForm } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
MOCK_QUERY,
MOCK_AGGREGATIONS,
MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
} from 'jest/search/mock_data';
-import LanguageFilter from '~/search/sidebar/components/language_filter.vue';
+import LanguageFilter from '~/search/sidebar/components/language_filter/index.vue';
import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
-import { MAX_ITEM_LENGTH } from '~/search/sidebar/constants/language_filter_data';
+
+import {
+ TRACKING_LABEL_SHOW_MORE,
+ TRACKING_CATEGORY,
+ TRACKING_PROPERTY_MAX,
+ TRACKING_LABEL_MAX,
+ TRACKING_LABEL_FILTERS,
+ TRACKING_ACTION_SHOW,
+ TRACKING_ACTION_CLICK,
+ TRACKING_LABEL_APPLY,
+ TRACKING_LABEL_ALL,
+} from '~/search/sidebar/components/language_filter/tracking';
+
+import { MAX_ITEM_LENGTH } from '~/search/sidebar/components/language_filter/data';
Vue.use(Vuex);
describe('GlobalSearchSidebarLanguageFilter', () => {
let wrapper;
+ let trackingSpy;
const actionSpies = {
fetchLanguageAggregation: jest.fn(),
@@ -22,7 +37,8 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
};
const getterSpies = {
- langugageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ languageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ queryLanguageFilters: jest.fn(() => []),
};
const createComponent = (initialState) => {
@@ -45,9 +61,14 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
});
};
+ afterEach(() => {
+ unmockTracking();
+ });
+
const findForm = () => wrapper.findComponent(GlForm);
const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter);
const findApplyButton = () => wrapper.findByTestId('apply-button');
+ const findResetButton = () => wrapper.findByTestId('reset-button');
const findShowMoreButton = () => wrapper.findByTestId('show-more-button');
const findAlert = () => wrapper.findComponent(GlAlert);
const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
@@ -56,6 +77,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
describe('Renders correctly', () => {
beforeEach(() => {
createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('renders form', () => {
@@ -84,6 +106,25 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
});
});
+ describe('resetButton', () => {
+ describe.each`
+ description | sidebarDirty | queryFilters | isDisabled
+ ${'sidebar dirty only'} | ${true} | ${[]} | ${undefined}
+ ${'query filters only'} | ${false} | ${['JSON', 'C']} | ${undefined}
+ ${'sidebar dirty and query filters'} | ${true} | ${['JSON', 'C']} | ${undefined}
+ ${'no sidebar and no query filters'} | ${false} | ${[]} | ${'true'}
+ `('$description', ({ sidebarDirty, queryFilters, isDisabled }) => {
+ beforeEach(() => {
+ getterSpies.queryLanguageFilters = jest.fn(() => queryFilters);
+ createComponent({ sidebarDirty, query: { ...MOCK_QUERY, language: queryFilters } });
+ });
+
+ it(`button is ${isDisabled ? 'enabled' : 'disabled'}`, () => {
+ expect(findResetButton().attributes('disabled')).toBe(isDisabled);
+ });
+ });
+ });
+
describe('ApplyButton', () => {
describe('when sidebarDirty is false', () => {
beforeEach(() => {
@@ -91,7 +132,7 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
});
it('disables the button', () => {
- expect(findApplyButton().attributes('disabled')).toBe('true');
+ expect(findApplyButton().attributes('disabled')).toBeDefined();
});
});
@@ -109,20 +150,40 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
describe('Show All button works', () => {
beforeEach(() => {
createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it(`renders ${MAX_ITEM_LENGTH} amount of items`, async () => {
findShowMoreButton().vm.$emit('click');
+
await nextTick();
+
expect(findAllCheckboxes()).toHaveLength(MAX_ITEM_LENGTH);
});
+ it('sends tracking information when show more clicked', () => {
+ findShowMoreButton().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_CLICK, TRACKING_LABEL_SHOW_MORE, {
+ label: TRACKING_LABEL_ALL,
+ });
+ });
+
it(`renders more then ${MAX_ITEM_LENGTH} text`, async () => {
findShowMoreButton().vm.$emit('click');
await nextTick();
expect(findHasOverMax().exists()).toBe(true);
});
+ it('sends tracking information when show more clicked and max item reached', () => {
+ findShowMoreButton().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_SHOW, TRACKING_LABEL_FILTERS, {
+ label: TRACKING_LABEL_MAX,
+ property: TRACKING_PROPERTY_MAX,
+ });
+ });
+
it(`doesn't render show more button after click`, async () => {
findShowMoreButton().vm.$emit('click');
await nextTick();
@@ -133,10 +194,11 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
describe('actions', () => {
beforeEach(() => {
createComponent({});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
- it('uses getter langugageAggregationBuckets', () => {
- expect(getterSpies.langugageAggregationBuckets).toHaveBeenCalled();
+ it('uses getter languageAggregationBuckets', () => {
+ expect(getterSpies.languageAggregationBuckets).toHaveBeenCalled();
});
it('uses action fetchLanguageAggregation', () => {
@@ -148,5 +210,13 @@ describe('GlobalSearchSidebarLanguageFilter', () => {
expect(actionSpies.applyQuery).toHaveBeenCalled();
});
+
+ it('sends tracking information clicking ApplyButton', () => {
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_ACTION_CLICK, TRACKING_LABEL_APPLY, {
+ label: TRACKING_CATEGORY,
+ });
+ });
});
});
diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js
index 94d529348a9..47235b828c3 100644
--- a/spec/frontend/search/sidebar/components/radio_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js
@@ -16,6 +16,10 @@ describe('RadioFilter', () => {
setQuery: jest.fn(),
};
+ const defaultGetters = {
+ currentScope: jest.fn(() => 'issues'),
+ };
+
const defaultProps = {
filterData: stateFilterData,
};
@@ -27,6 +31,7 @@ describe('RadioFilter', () => {
...initialState,
},
actions: actionSpies,
+ getters: defaultGetters,
});
wrapper = shallowMount(RadioFilter, {
@@ -38,11 +43,6 @@ describe('RadioFilter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findGlRadioButtonGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findGlRadioButtons = () => findGlRadioButtonGroup().findAllComponents(GlFormRadio);
const findGlRadioButtonsText = () => findGlRadioButtons().wrappers.map((w) => w.text());
diff --git a/spec/frontend/search/sidebar/components/scope_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
index 23c158239dc..e8737384f27 100644
--- a/spec/frontend/search/sidebar/components/scope_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_navigation_spec.js
@@ -14,6 +14,10 @@ describe('ScopeNavigation', () => {
fetchSidebarCount: jest.fn(),
};
+ const getterSpies = {
+ currentScope: jest.fn(() => 'issues'),
+ };
+
const createComponent = (initialState) => {
const store = new Vuex.Store({
state: {
@@ -22,6 +26,7 @@ describe('ScopeNavigation', () => {
...initialState,
},
actions: actionSpies,
+ getters: getterSpies,
});
wrapper = shallowMount(ScopeNavigation, {
@@ -29,16 +34,12 @@ describe('ScopeNavigation', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findNavElement = () => wrapper.find('nav');
const findGlNav = () => wrapper.findComponent(GlNav);
const findGlNavItems = () => wrapper.findAllComponents(GlNavItem);
- const findGlNavItemActive = () => findGlNavItems().wrappers.filter((w) => w.attributes('active'));
- const findGlNavItemActiveLabel = () => findGlNavItemActive().at(0).findAll('span').at(0).text();
- const findGlNavItemActiveCount = () => findGlNavItemActive().at(0).findAll('span').at(1);
+ const findGlNavItemActive = () => wrapper.find('[active=true]');
+ const findGlNavItemActiveLabel = () => findGlNavItemActive().find('[data-testid="label"]');
+ const findGlNavItemActiveCount = () => findGlNavItemActive().find('[data-testid="count"]');
describe('scope navigation', () => {
beforeEach(() => {
@@ -71,8 +72,8 @@ describe('ScopeNavigation', () => {
});
it('has correct active item', () => {
- expect(findGlNavItemActive()).toHaveLength(1);
- expect(findGlNavItemActiveLabel()).toBe('Issues');
+ expect(findGlNavItemActive().exists()).toBe(true);
+ expect(findGlNavItemActiveLabel().text()).toBe('Issues');
});
it('has correct active item count', () => {
@@ -80,7 +81,7 @@ describe('ScopeNavigation', () => {
});
it('does not have plus sign after count text', () => {
- expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(false);
+ expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(false);
});
it('has count is highlighted correctly', () => {
@@ -90,14 +91,26 @@ describe('ScopeNavigation', () => {
describe('scope navigation sets proper state with NO url scope set', () => {
beforeEach(() => {
+ getterSpies.currentScope = jest.fn(() => 'projects');
createComponent({
urlQuery: {},
+ navigation: {
+ ...MOCK_NAVIGATION,
+ projects: {
+ ...MOCK_NAVIGATION.projects,
+ active: true,
+ },
+ issues: {
+ ...MOCK_NAVIGATION.issues,
+ active: false,
+ },
+ },
});
});
it('has correct active item', () => {
- expect(findGlNavItems().at(0).attributes('active')).toBe('true');
- expect(findGlNavItemActiveLabel()).toBe('Projects');
+ expect(findGlNavItemActive().exists()).toBe(true);
+ expect(findGlNavItemActiveLabel().text()).toBe('Projects');
});
it('has correct active item count', () => {
@@ -105,7 +118,25 @@ describe('ScopeNavigation', () => {
});
it('has correct active item count and over limit sign', () => {
- expect(findGlNavItemActive().at(0).findComponent(GlIcon).exists()).toBe(true);
+ expect(findGlNavItemActive().findComponent(GlIcon).exists()).toBe(true);
+ });
+ });
+
+ describe.each`
+ searchTherm | hasBeenCalled
+ ${null} | ${0}
+ ${'test'} | ${1}
+ `('fetchSidebarCount', ({ searchTherm, hasBeenCalled }) => {
+ beforeEach(() => {
+ createComponent({
+ urlQuery: {
+ search: searchTherm,
+ },
+ });
+ });
+
+ it('is only called when search term is set', () => {
+ expect(actionSpies.fetchSidebarCount).toHaveBeenCalledTimes(hasBeenCalled);
});
});
});
diff --git a/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js
new file mode 100644
index 00000000000..5207665f883
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/scope_new_navigation_spec.js
@@ -0,0 +1,83 @@
+import { mount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import ScopeNewNavigation from '~/search/sidebar/components/scope_new_navigation.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { MOCK_QUERY, MOCK_NAVIGATION, MOCK_NAVIGATION_ITEMS } from '../../mock_data';
+
+Vue.use(Vuex);
+
+describe('ScopeNewNavigation', () => {
+ let wrapper;
+
+ const actionSpies = {
+ fetchSidebarCount: jest.fn(),
+ };
+
+ const getterSpies = {
+ currentScope: jest.fn(() => 'issues'),
+ navigationItems: jest.fn(() => MOCK_NAVIGATION_ITEMS),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ urlQuery: MOCK_QUERY,
+ navigation: MOCK_NAVIGATION,
+ ...initialState,
+ },
+ actions: actionSpies,
+ getters: getterSpies,
+ });
+
+ wrapper = mount(ScopeNewNavigation, {
+ store,
+ stubs: {
+ NavItem,
+ },
+ });
+ };
+
+ const findNavElement = () => wrapper.findComponent('nav');
+ const findNavItems = () => wrapper.findAllComponents(NavItem);
+ const findNavItemActive = () => wrapper.find('[aria-current=page]');
+ const findNavItemActiveLabel = () =>
+ findNavItemActive().find('[class="gl-pr-8 gl-text-gray-900 gl-truncate-end"]');
+
+ describe('scope navigation', () => {
+ beforeEach(() => {
+ createComponent({ urlQuery: { ...MOCK_QUERY, search: 'test' } });
+ });
+
+ it('renders section', () => {
+ expect(findNavElement().exists()).toBe(true);
+ });
+
+ it('calls proper action when rendered', async () => {
+ await nextTick();
+ expect(actionSpies.fetchSidebarCount).toHaveBeenCalled();
+ });
+
+ it('renders all nav item components', () => {
+ expect(findNavItems()).toHaveLength(9);
+ });
+
+ it('has all proper links', () => {
+ const linkAtPosition = 3;
+ const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]];
+
+ expect(findNavItems().at(linkAtPosition).findComponent('a').attributes('href')).toBe(link);
+ });
+ });
+
+ describe('scope navigation sets proper state with url scope set', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('has correct active item', () => {
+ expect(findNavItemActive().exists()).toBe(true);
+ expect(findNavItemActiveLabel().text()).toBe('Issues');
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index 6704634ef36..a332a43e624 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -1,25 +1,52 @@
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
import RadioFilter from '~/search/sidebar/components/radio_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
+Vue.use(Vuex);
+
describe('StatusFilter', () => {
let wrapper;
- const createComponent = (initProps) => {
+ const createComponent = (state) => {
+ const store = new Vuex.Store({
+ state,
+ });
+
wrapper = shallowMount(StatusFilter, {
- ...initProps,
+ store,
});
};
const findRadioFilter = () => wrapper.findComponent(RadioFilter);
+ const findHR = () => wrapper.findComponent('hr');
- describe('template', () => {
+ describe('old sidebar', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ useNewNavigation: false });
});
it('renders the component', () => {
expect(findRadioFilter().exists()).toBe(true);
});
+
+ it('renders the divider', () => {
+ expect(findHR().exists()).toBe(true);
+ });
+ });
+
+ describe('new sidebar', () => {
+ beforeEach(() => {
+ createComponent({ useNewNavigation: true });
+ });
+
+ it('renders the component', () => {
+ expect(findRadioFilter().exists()).toBe(true);
+ });
+
+ it("doesn't render the divider", () => {
+ expect(findHR().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js
index a566b9b99d3..322ce1b16ef 100644
--- a/spec/frontend/search/sort/components/app_spec.js
+++ b/spec/frontend/search/sort/components/app_spec.js
@@ -38,11 +38,6 @@ describe('GlobalSearchSort', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findSortButtonGroup = () => wrapper.findComponent(GlButtonGroup);
const findSortDropdown = () => wrapper.findComponent(GlDropdown);
const findSortDirectionButton = () => wrapper.findComponent(GlButton);
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 2f87802dfe6..0884411df0c 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -33,7 +33,7 @@ import {
MOCK_AGGREGATIONS,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
setUrlParams: jest.fn(),
joinPaths: jest.fn().mockReturnValue(''),
@@ -47,7 +47,7 @@ describe('Global Search Store Actions', () => {
let mock;
let state;
- const flashCallback = (callCount) => {
+ const alertCallback = (callCount) => {
expect(createAlert).toHaveBeenCalledTimes(callCount);
createAlert.mockClear();
};
@@ -63,12 +63,12 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | flashCallCount
+ action | axiosMock | type | expectedMutations | alertCallCount
${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
- `(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
+ `(`axios calls`, ({ action, axiosMock, type, expectedMutations, alertCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
@@ -76,7 +76,7 @@ describe('Global Search Store Actions', () => {
});
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() =>
- flashCallback(flashCallCount),
+ alertCallback(alertCallCount),
);
});
});
@@ -84,12 +84,12 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | flashCallCount
+ action | axiosMock | type | expectedMutations | alertCallCount
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
- `('Promise.all calls', ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
+ `('Promise.all calls', ({ action, axiosMock, type, expectedMutations, alertCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
@@ -103,7 +103,7 @@ describe('Global Search Store Actions', () => {
it(`should dispatch the correct mutations`, () => {
return testAction({ action, state, expectedMutations }).then(() => {
- flashCallback(flashCallCount);
+ alertCallback(alertCallCount);
});
});
});
@@ -275,7 +275,7 @@ describe('Global Search Store Actions', () => {
describe.each`
action | axiosMock | type | scope | expectedMutations | errorLogs
${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0}
- ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'error'} | ${'projects'} | ${[]} | ${1}
${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${'issues'} | ${[]} | ${1}
`('fetchSidebarCount', ({ action, axiosMock, type, expectedMutations, scope, errorLogs }) => {
describe(`on ${type}`, () => {
@@ -290,9 +290,9 @@ describe('Global Search Store Actions', () => {
}
});
- it(`should ${expectedMutations.length === 0 ? 'NOT ' : ''}dispatch ${
- expectedMutations.length === 0 ? '' : 'the correct '
- }mutations for ${scope}`, () => {
+ it(`should ${expectedMutations.length === 0 ? 'NOT' : ''} dispatch ${
+ expectedMutations.length === 0 ? '' : 'the correct'
+ } mutations for ${scope}`, () => {
return testAction({ action, state, expectedMutations }).then(() => {
expect(logger.logError).toHaveBeenCalledTimes(errorLogs);
});
@@ -325,4 +325,26 @@ describe('Global Search Store Actions', () => {
});
});
});
+
+ describe('resetLanguageQueryWithRedirect', () => {
+ it('calls visitUrl and setParams with the state.query', () => {
+ return testAction(actions.resetLanguageQueryWithRedirect, null, state, [], [], () => {
+ expect(urlUtils.setUrlParams).toHaveBeenCalledWith({ ...state.query, page: null });
+ expect(urlUtils.visitUrl).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('resetLanguageQuery', () => {
+ it('calls commit SET_QUERY with value []', () => {
+ state = { ...state, query: { ...state.query, language: ['YAML', 'Text', 'Markdown'] } };
+ return testAction(
+ actions.resetLanguageQuery,
+ null,
+ state,
+ [{ type: types.SET_QUERY, payload: { key: 'language', value: [] } }],
+ [],
+ );
+ });
+ });
});
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
index 818902ee720..e3b8e7575a4 100644
--- a/spec/frontend/search/store/getters_spec.js
+++ b/spec/frontend/search/store/getters_spec.js
@@ -1,12 +1,16 @@
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import * as getters from '~/search/store/getters';
import createState from '~/search/store/state';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import {
MOCK_QUERY,
MOCK_GROUPS,
MOCK_PROJECTS,
MOCK_AGGREGATIONS,
MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+ TEST_FILTER_DATA,
+ MOCK_NAVIGATION,
+ MOCK_NAVIGATION_ITEMS,
} from '../mock_data';
describe('Global Search Store Getters', () => {
@@ -14,37 +18,62 @@ describe('Global Search Store Getters', () => {
beforeEach(() => {
state = createState({ query: MOCK_QUERY });
+ useMockLocationHelper();
});
describe('frequentGroups', () => {
- beforeEach(() => {
- state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS;
- });
-
it('returns the correct data', () => {
+ state.frequentItems[GROUPS_LOCAL_STORAGE_KEY] = MOCK_GROUPS;
expect(getters.frequentGroups(state)).toStrictEqual(MOCK_GROUPS);
});
});
describe('frequentProjects', () => {
- beforeEach(() => {
- state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS;
- });
-
it('returns the correct data', () => {
+ state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY] = MOCK_PROJECTS;
expect(getters.frequentProjects(state)).toStrictEqual(MOCK_PROJECTS);
});
});
- describe('langugageAggregationBuckets', () => {
- beforeEach(() => {
+ describe('languageAggregationBuckets', () => {
+ it('returns the correct data', () => {
state.aggregations.data = MOCK_AGGREGATIONS;
+ expect(getters.languageAggregationBuckets(state)).toStrictEqual(
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+ );
});
+ });
+ describe('queryLanguageFilters', () => {
it('returns the correct data', () => {
- expect(getters.langugageAggregationBuckets(state)).toStrictEqual(
- MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
- );
+ state.query.language = Object.keys(TEST_FILTER_DATA.filters);
+ expect(getters.queryLanguageFilters(state)).toStrictEqual(state.query.language);
+ });
+ });
+
+ describe('currentScope', () => {
+ it('returns the correct scope name', () => {
+ state.navigation = MOCK_NAVIGATION;
+ expect(getters.currentScope(state)).toBe('issues');
+ });
+ });
+
+ describe('currentUrlQueryHasLanguageFilters', () => {
+ it.each`
+ description | lang | result
+ ${'has valid language'} | ${{ language: ['a', 'b'] }} | ${true}
+ ${'has empty lang'} | ${{ language: [] }} | ${false}
+ ${'has no lang'} | ${{}} | ${false}
+ `('$description', ({ lang, result }) => {
+ state.urlQuery = lang;
+ expect(getters.currentUrlQueryHasLanguageFilters(state)).toBe(result);
+ });
+ });
+
+ describe('navigationItems', () => {
+ it('returns the re-mapped navigation data', () => {
+ state.navigation = MOCK_NAVIGATION;
+ expect(getters.navigationItems(state)).toStrictEqual(MOCK_NAVIGATION_ITEMS);
});
});
});
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index 487ed7bfe03..802c5219799 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -7,6 +7,8 @@ import {
isSidebarDirty,
formatSearchResultCount,
getAggregationsUrl,
+ prepareSearchAggregations,
+ addCountOverLimit,
} from '~/search/store/utils';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import {
@@ -15,6 +17,9 @@ import {
MOCK_INFLATED_DATA,
FRESH_STORED_DATA,
STALE_STORED_DATA,
+ MOCK_AGGREGATIONS,
+ SMALL_MOCK_AGGREGATIONS,
+ TEST_RAW_BUCKETS,
} from '../mock_data';
const PREV_TIME = new Date().getTime() - 1;
@@ -226,11 +231,14 @@ describe('Global Search Store Utils', () => {
});
describe.each`
- description | currentQuery | urlQuery | isDirty
- ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${false}
- ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${true}
- ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${false}
- ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${true}
+ description | currentQuery | urlQuery | isDirty
+ ${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${false}
+ ${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default', [SIDEBAR_PARAMS[2]]: ['a', 'c'] }} | ${true}
+ ${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null, [SIDEBAR_PARAMS[2]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined, [SIDEBAR_PARAMS[2]]: undefined }} | ${false}
+ ${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new', [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined, [SIDEBAR_PARAMS[2]]: [] }} | ${true}
+ ${'language only no url params'} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[2]]: undefined }} | ${true}
+ ${'language only url params symetric'} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${false}
+ ${'language only url params asymetric'} | ${{ [SIDEBAR_PARAMS[2]]: ['a'] }} | ${{ [SIDEBAR_PARAMS[2]]: ['a', 'b'] }} | ${true}
`('isSidebarDirty', ({ description, currentQuery, urlQuery, isDirty }) => {
describe(`with ${description} sidebar query data`, () => {
let res;
@@ -263,4 +271,36 @@ describe('Global Search Store Utils', () => {
expect(getAggregationsUrl()).toStrictEqual(`${testURL}search/aggregations`);
});
});
+
+ const TEST_LANGUAGE_QUERY = ['Markdown', 'JSON'];
+ const TEST_EXPECTED_ORDERED_BUCKETS = [
+ TEST_RAW_BUCKETS.find((x) => x.key === 'Markdown'),
+ TEST_RAW_BUCKETS.find((x) => x.key === 'JSON'),
+ ...TEST_RAW_BUCKETS.filter((x) => !TEST_LANGUAGE_QUERY.includes(x.key)),
+ ];
+
+ describe('prepareSearchAggregations', () => {
+ it.each`
+ description | query | data | result
+ ${'has no query'} | ${undefined} | ${MOCK_AGGREGATIONS} | ${MOCK_AGGREGATIONS}
+ ${'has query'} | ${{ language: TEST_LANGUAGE_QUERY }} | ${SMALL_MOCK_AGGREGATIONS} | ${[{ ...SMALL_MOCK_AGGREGATIONS[0], buckets: TEST_EXPECTED_ORDERED_BUCKETS }]}
+ ${'has bad query'} | ${{ language: ['sdf', 'wrt'] }} | ${SMALL_MOCK_AGGREGATIONS} | ${SMALL_MOCK_AGGREGATIONS}
+ `('$description', ({ query, data, result }) => {
+ expect(prepareSearchAggregations({ query }, data)).toStrictEqual(result);
+ });
+ });
+
+ describe('addCountOverLimit', () => {
+ it("should return '+' if count includes '+'", () => {
+ expect(addCountOverLimit('10+')).toEqual('+');
+ });
+
+ it("should return empty string if count does not include '+'", () => {
+ expect(addCountOverLimit('10')).toEqual('');
+ });
+
+ it('should return empty string if count is not provided', () => {
+ expect(addCountOverLimit()).toEqual('');
+ });
+ });
});
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
index 3975887cfff..423ec6ff63b 100644
--- a/spec/frontend/search/topbar/components/app_spec.js
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -36,10 +36,6 @@ describe('GlobalSearchTopbar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
const findGroupFilter = () => wrapper.findComponent(GroupFilter);
const findProjectFilter = () => wrapper.findComponent(ProjectFilter);
diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js
index b2d0297fdc2..78d9efbd686 100644
--- a/spec/frontend/search/topbar/components/group_filter_spec.js
+++ b/spec/frontend/search/topbar/components/group_filter_spec.js
@@ -49,10 +49,6 @@ describe('GroupFilter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSearchableDropdown = () => wrapper.findComponent(SearchableDropdown);
describe('template', () => {
diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js
index 297a536e075..9eda34b1633 100644
--- a/spec/frontend/search/topbar/components/project_filter_spec.js
+++ b/spec/frontend/search/topbar/components/project_filter_spec.js
@@ -49,10 +49,6 @@ describe('ProjectFilter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSearchableDropdown = () => wrapper.findComponent(SearchableDropdown);
describe('template', () => {
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js
index e51fe9a4cf9..c911fe53d40 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_item_spec.js
@@ -24,10 +24,6 @@ describe('Global Search Searchable Dropdown Item', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findGlAvatar = () => wrapper.findComponent(GlAvatar);
const findDropdownTitle = () => wrapper.findByTestId('item-title');
diff --git a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
index de1cefa9e9d..f7d847674eb 100644
--- a/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
+++ b/spec/frontend/search/topbar/components/searchable_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_GROUPS, MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
@@ -40,10 +40,6 @@ describe('Global Search Searchable Dropdown', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findGlDropdownSearch = () => findGlDropdown().findComponent(GlSearchBoxByType);
const findDropdownText = () => findGlDropdown().find('.dropdown-toggle-text');
@@ -133,9 +129,7 @@ describe('Global Search Searchable Dropdown', () => {
describe(`when search is ${searchText} and frequentItems length is ${frequentItems.length}`, () => {
beforeEach(() => {
createComponent({}, { frequentItems });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ searchText });
+ findGlDropdownSearch().vm.$emit('input', searchText);
});
it(`should${length ? '' : ' not'} render frequent dropdown items`, () => {
@@ -191,28 +185,33 @@ describe('Global Search Searchable Dropdown', () => {
});
describe('opening the dropdown', () => {
- describe('for the first time', () => {
- beforeEach(() => {
- findGlDropdown().vm.$emit('show');
- });
+ beforeEach(() => {
+ findGlDropdown().vm.$emit('show');
+ });
- it('$emits @search and @first-open', () => {
- expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]);
- expect(wrapper.emitted('first-open')[0]).toStrictEqual([]);
- });
+ it('$emits @search and @first-open on the first open', () => {
+ expect(wrapper.emitted('search')[0]).toStrictEqual(['']);
+ expect(wrapper.emitted('first-open')[0]).toStrictEqual([]);
});
- describe('not for the first time', () => {
- beforeEach(() => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ hasBeenOpened: true });
- findGlDropdown().vm.$emit('show');
+ describe('when the dropdown has been opened', () => {
+ it('$emits @search with the searchText', async () => {
+ const searchText = 'foo';
+
+ findGlDropdownSearch().vm.$emit('input', searchText);
+ await nextTick();
+
+ expect(wrapper.emitted('search')[1]).toStrictEqual([searchText]);
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
});
- it('$emits @search and not @first-open', () => {
- expect(wrapper.emitted('search')[0]).toStrictEqual([wrapper.vm.searchText]);
- expect(wrapper.emitted('first-open')).toBeUndefined();
+ it('does not emit @first-open again', async () => {
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
+
+ findGlDropdownSearch().vm.$emit('input');
+ await nextTick();
+
+ expect(wrapper.emitted('first-open')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
deleted file mode 100644
index a3098fb81ea..00000000000
--- a/spec/frontend/search_autocomplete_spec.js
+++ /dev/null
@@ -1,293 +0,0 @@
-import AxiosMockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import axios from '~/lib/utils/axios_utils';
-import initSearchAutocomplete from '~/search_autocomplete';
-import '~/lib/utils/common_utils';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-
-describe('Search autocomplete dropdown', () => {
- let widget = null;
-
- const userName = 'root';
- const userId = 1;
- const dashboardIssuesPath = '/dashboard/issues';
- const dashboardMRsPath = '/dashboard/merge_requests';
- const projectIssuesPath = '/gitlab-org/gitlab-foss/issues';
- const projectMRsPath = '/gitlab-org/gitlab-foss/-/merge_requests';
- const groupIssuesPath = '/groups/gitlab-org/-/issues';
- const groupMRsPath = '/groups/gitlab-org/-/merge_requests';
- const autocompletePath = '/search/autocomplete';
- const projectName = 'GitLab Community Edition';
- const groupName = 'Gitlab Org';
-
- const removeBodyAttributes = () => {
- const { body } = document;
-
- delete body.dataset.page;
- delete body.dataset.project;
- delete body.dataset.group;
- };
-
- // Add required attributes to body before starting the test.
- // section would be dashboard|group|project
- const addBodyAttributes = (section = 'dashboard') => {
- removeBodyAttributes();
-
- const { body } = document;
- switch (section) {
- case 'dashboard':
- body.dataset.page = 'root:index';
- break;
- case 'group':
- body.dataset.page = 'groups:show';
- body.dataset.group = 'gitlab-org';
- break;
- case 'project':
- body.dataset.page = 'projects:show';
- body.dataset.project = 'gitlab-ce';
- break;
- default:
- break;
- }
- };
-
- const disableProjectIssues = () => {
- document.querySelector('.js-search-project-options').dataset.issuesDisabled = true;
- };
-
- // Mock `gl` object in window for dashboard specific page. App code will need it.
- const mockDashboardOptions = () => {
- window.gl.dashboardOptions = {
- issuesPath: dashboardIssuesPath,
- mrPath: dashboardMRsPath,
- };
- };
-
- // Mock `gl` object in window for project specific page. App code will need it.
- const mockProjectOptions = () => {
- window.gl.projectOptions = {
- 'gitlab-ce': {
- issuesPath: projectIssuesPath,
- mrPath: projectMRsPath,
- projectName,
- },
- };
- };
-
- const mockGroupOptions = () => {
- window.gl.groupOptions = {
- 'gitlab-org': {
- issuesPath: groupIssuesPath,
- mrPath: groupMRsPath,
- projectName: groupName,
- },
- };
- };
-
- const assertLinks = (list, issuesPath, mrsPath) => {
- if (issuesPath) {
- const issuesAssignedToMeLink = `a[href="${issuesPath}/?assignee_username=${userName}"]`;
- const issuesIHaveCreatedLink = `a[href="${issuesPath}/?author_username=${userName}"]`;
-
- expect(list.find(issuesAssignedToMeLink).length).toBe(1);
- expect(list.find(issuesAssignedToMeLink).text()).toBe('Issues assigned to me');
- expect(list.find(issuesIHaveCreatedLink).length).toBe(1);
- expect(list.find(issuesIHaveCreatedLink).text()).toBe("Issues I've created");
- }
- const mrsAssignedToMeLink = `a[href="${mrsPath}/?assignee_username=${userName}"]`;
- const mrsIHaveCreatedLink = `a[href="${mrsPath}/?author_username=${userName}"]`;
-
- expect(list.find(mrsAssignedToMeLink).length).toBe(1);
- expect(list.find(mrsAssignedToMeLink).text()).toBe('Merge requests assigned to me');
- expect(list.find(mrsIHaveCreatedLink).length).toBe(1);
- expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
- };
-
- beforeEach(() => {
- loadHTMLFixture('static/search_autocomplete.html');
-
- window.gon = {};
- window.gon.current_user_id = userId;
- window.gon.current_username = userName;
- window.gl = window.gl || (window.gl = {});
-
- widget = initSearchAutocomplete({ autocompletePath });
- });
-
- afterEach(() => {
- // Undo what we did to the shared <body>
- removeBodyAttributes();
- window.gon = {};
-
- resetHTMLFixture();
- });
-
- it('should show Dashboard specific dropdown menu', () => {
- addBodyAttributes();
- mockDashboardOptions();
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
- });
-
- it('should show Group specific dropdown menu', () => {
- addBodyAttributes('group');
- mockGroupOptions();
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- return assertLinks(list, groupIssuesPath, groupMRsPath);
- });
-
- it('should show Project specific dropdown menu', () => {
- addBodyAttributes('project');
- mockProjectOptions();
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- return assertLinks(list, projectIssuesPath, projectMRsPath);
- });
-
- it('should show only Project mergeRequest dropdown menu items when project issues are disabled', () => {
- addBodyAttributes('project');
- disableProjectIssues();
- mockProjectOptions();
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- assertLinks(list, null, projectMRsPath);
- });
-
- it('should not show category related menu if there is text in the input', () => {
- addBodyAttributes('project');
- mockProjectOptions();
- widget.searchInput.val('help');
- widget.searchInput.triggerHandler('focus');
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- const link = `a[href='${projectIssuesPath}/?assignee_username=${userName}']`;
-
- expect(list.find(link).length).toBe(0);
- });
-
- it('should not submit the search form when selecting an autocomplete row with the keyboard', () => {
- const ENTER = 13;
- const DOWN = 40;
- addBodyAttributes();
- mockDashboardOptions(true);
- const submitSpy = jest.spyOn(document.querySelector('form'), 'submit');
- widget.searchInput.triggerHandler('focus');
- widget.wrap.trigger($.Event('keydown', { which: DOWN }));
- const enterKeyEvent = $.Event('keydown', { which: ENTER });
- widget.searchInput.trigger(enterKeyEvent);
-
- // This does not currently catch failing behavior. For security reasons,
- // browsers will not trigger default behavior (form submit, in this
- // example) on JavaScript-created keypresses.
- expect(submitSpy).not.toHaveBeenCalled();
- });
-
- describe('show autocomplete results', () => {
- beforeEach(() => {
- widget.enableAutocomplete();
-
- const axiosMock = new AxiosMockAdapter(axios);
- const autocompleteUrl = new RegExp(autocompletePath);
-
- axiosMock.onGet(autocompleteUrl).reply(HTTP_STATUS_OK, [
- {
- category: 'Projects',
- id: 1,
- value: 'Gitlab Test',
- label: 'Gitlab Org / Gitlab Test',
- url: '/gitlab-org/gitlab-test',
- avatar_url: '',
- },
- {
- category: 'Groups',
- id: 1,
- value: 'Gitlab Org',
- label: 'Gitlab Org',
- url: '/gitlab-org',
- avatar_url: '',
- },
- ]);
- });
-
- function triggerAutocomplete() {
- return new Promise((resolve) => {
- const dropdown = widget.searchInput.data('deprecatedJQueryDropdown');
- const filterCallback = dropdown.filter.options.callback;
- dropdown.filter.options.callback = jest.fn((data) => {
- filterCallback(data);
-
- resolve();
- });
-
- widget.searchInput.val('Gitlab');
- widget.searchInput.triggerHandler('input');
- });
- }
-
- it('suggest Projects', async () => {
- await triggerAutocomplete();
-
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- const link = "a[href$='/gitlab-org/gitlab-test']";
-
- expect(list.find(link).length).toBe(1);
- });
-
- it('suggest Groups', async () => {
- await triggerAutocomplete();
-
- const list = widget.wrap.find('.dropdown-menu').find('ul');
- const link = "a[href$='/gitlab-org']";
-
- expect(list.find(link).length).toBe(1);
- });
- });
-
- describe('disableAutocomplete', () => {
- beforeEach(() => {
- widget.enableAutocomplete();
- });
-
- it('should close the Dropdown', () => {
- const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown');
-
- widget.dropdown.addClass('show');
- widget.disableAutocomplete();
-
- expect(toggleSpy).toHaveBeenCalledWith('toggle');
- });
- });
-
- describe('enableAutocomplete', () => {
- let toggleSpy;
- let trackingSpy;
-
- beforeEach(() => {
- toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown');
- trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
- document.body.dataset.page = 'some:page'; // default tracking for category
- });
-
- afterEach(() => {
- unmockTracking();
- });
-
- it('should open the Dropdown', () => {
- widget.enableAutocomplete();
-
- expect(toggleSpy).toHaveBeenCalledWith('toggle');
- });
-
- it('should track the opening', () => {
- widget.enableAutocomplete();
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_search_bar', {
- label: 'main_navigation',
- property: 'navigation',
- });
- });
- });
-});
diff --git a/spec/frontend/search_autocomplete_utils_spec.js b/spec/frontend/search_autocomplete_utils_spec.js
deleted file mode 100644
index 4fdec717e93..00000000000
--- a/spec/frontend/search_autocomplete_utils_spec.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import {
- isInGroupsPage,
- isInProjectPage,
- getGroupSlug,
- getProjectSlug,
-} from '~/search_autocomplete_utils';
-
-describe('search_autocomplete_utils', () => {
- let originalBody;
-
- beforeEach(() => {
- originalBody = document.body;
- document.body = document.createElement('body');
- });
-
- afterEach(() => {
- document.body = originalBody;
- });
-
- describe('isInGroupsPage', () => {
- it.each`
- page | result
- ${'groups:index'} | ${true}
- ${'groups:show'} | ${true}
- ${'projects:show'} | ${false}
- `(`returns $result in for page $page`, ({ page, result }) => {
- document.body.dataset.page = page;
-
- expect(isInGroupsPage()).toBe(result);
- });
- });
-
- describe('isInProjectPage', () => {
- it.each`
- page | result
- ${'projects:index'} | ${true}
- ${'projects:show'} | ${true}
- ${'groups:show'} | ${false}
- `(`returns $result in for page $page`, ({ page, result }) => {
- document.body.dataset.page = page;
-
- expect(isInProjectPage()).toBe(result);
- });
- });
-
- describe('getProjectSlug', () => {
- it('returns null when no project is present or on project page', () => {
- expect(getProjectSlug()).toBe(null);
- });
-
- it('returns null when not on project page', () => {
- document.body.dataset.project = 'gitlab';
-
- expect(getProjectSlug()).toBe(null);
- });
-
- it('returns null when project is missing', () => {
- document.body.dataset.page = 'projects';
-
- expect(getProjectSlug()).toBe(undefined);
- });
-
- it('returns project', () => {
- document.body.dataset.page = 'projects';
- document.body.dataset.project = 'gitlab';
-
- expect(getProjectSlug()).toBe('gitlab');
- });
-
- it('returns project in edit page', () => {
- document.body.dataset.page = 'projects:edit';
- document.body.dataset.project = 'gitlab';
-
- expect(getProjectSlug()).toBe('gitlab');
- });
- });
-
- describe('getGroupSlug', () => {
- it('returns null when no group is present or on group page', () => {
- expect(getGroupSlug()).toBe(null);
- });
-
- it('returns null when not on group page', () => {
- document.body.dataset.group = 'gitlab-org';
-
- expect(getGroupSlug()).toBe(null);
- });
-
- it('returns null when group is missing on groups page', () => {
- document.body.dataset.page = 'groups';
-
- expect(getGroupSlug()).toBe(undefined);
- });
-
- it('returns null when group is missing on project page', () => {
- document.body.dataset.page = 'project';
-
- expect(getGroupSlug()).toBe(null);
- });
-
- it.each`
- page
- ${'groups'}
- ${'groups:edit'}
- ${'projects'}
- ${'projects:edit'}
- `(`returns group in page $page`, ({ page }) => {
- document.body.dataset.page = page;
- document.body.dataset.group = 'gitlab-org';
-
- expect(getGroupSlug()).toBe('gitlab-org');
- });
- });
-});
diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js
index 3f856968db6..fe761049a70 100644
--- a/spec/frontend/search_settings/components/search_settings_spec.js
+++ b/spec/frontend/search_settings/components/search_settings_spec.js
@@ -79,10 +79,6 @@ describe('search_settings/components/search_settings.vue', () => {
buildWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('hides sections that do not match the search term', () => {
const hiddenSection = document.querySelector(`#${GENERAL_SETTINGS_ID}`);
search(SEARCH_TERM);
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index ddefda2ffc3..364fe733a41 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -8,78 +8,31 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import SecurityConfigurationApp, { i18n } from '~/security_configuration/components/app.vue';
import AutoDevopsAlert from '~/security_configuration/components/auto_dev_ops_alert.vue';
import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev_ops_enabled_alert.vue';
-import {
- SAST_NAME,
- SAST_SHORT_NAME,
- SAST_DESCRIPTION,
- SAST_HELP_PATH,
- SAST_CONFIG_HELP_PATH,
- LICENSE_COMPLIANCE_NAME,
- LICENSE_COMPLIANCE_DESCRIPTION,
- LICENSE_COMPLIANCE_HELP_PATH,
- AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
-} from '~/security_configuration/components/constants';
+import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
-import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
-import {
- REPORT_TYPE_LICENSE_COMPLIANCE,
- REPORT_TYPE_SAST,
-} from '~/vue_shared/security_reports/constants';
-
-const upgradePath = '/upgrade';
-const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
-const autoDevopsPath = '/autoDevopsPath';
+import { securityFeaturesMock, provideMock } from '../mock_data';
+
const gitlabCiHistoryPath = 'test/historyPath';
-const projectFullPath = 'namespace/project';
-const vulnerabilityTrainingDocsPath = 'user/application_security/vulnerabilities/index';
+const { vulnerabilityTrainingDocsPath, projectFullPath } = provideMock;
useLocalStorageSpy();
Vue.use(VueApollo);
-describe('App component', () => {
+describe('~/security_configuration/components/app', () => {
let wrapper;
let userCalloutDismissSpy;
- const securityFeaturesMock = [
- {
- name: SAST_NAME,
- shortName: SAST_SHORT_NAME,
- description: SAST_DESCRIPTION,
- helpPath: SAST_HELP_PATH,
- configurationHelpPath: SAST_CONFIG_HELP_PATH,
- type: REPORT_TYPE_SAST,
- available: true,
- },
- ];
-
- const complianceFeaturesMock = [
- {
- name: LICENSE_COMPLIANCE_NAME,
- description: LICENSE_COMPLIANCE_DESCRIPTION,
- helpPath: LICENSE_COMPLIANCE_HELP_PATH,
- type: REPORT_TYPE_LICENSE_COMPLIANCE,
- configurationHelpPath: LICENSE_COMPLIANCE_HELP_PATH,
- },
- ];
-
const createComponent = ({ shouldShowCallout = true, ...propsData } = {}) => {
userCalloutDismissSpy = jest.fn();
wrapper = mountExtended(SecurityConfigurationApp, {
propsData: {
augmentedSecurityFeatures: securityFeaturesMock,
- augmentedComplianceFeatures: complianceFeaturesMock,
securityTrainingEnabled: true,
...propsData,
},
- provide: {
- upgradePath,
- autoDevopsHelpPagePath,
- autoDevopsPath,
- projectFullPath,
- vulnerabilityTrainingDocsPath,
- },
+ provide: provideMock,
stubs: {
...stubChildren(SecurityConfigurationApp),
GlLink: false,
@@ -118,21 +71,11 @@ describe('App component', () => {
text: i18n.configurationHistory,
container: findByTestId('security-testing-tab'),
});
- const findComplianceViewHistoryLink = () =>
- findLink({
- href: gitlabCiHistoryPath,
- text: i18n.configurationHistory,
- container: findByTestId('compliance-testing-tab'),
- });
- const findUpgradeBanner = () => wrapper.findComponent(UpgradeBanner);
+
const findAutoDevopsAlert = () => wrapper.findComponent(AutoDevopsAlert);
const findAutoDevopsEnabledAlert = () => wrapper.findComponent(AutoDevopsEnabledAlert);
const findVulnerabilityManagementTab = () => wrapper.findByTestId('vulnerability-management-tab');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('basic structure', () => {
beforeEach(() => {
createComponent();
@@ -141,11 +84,11 @@ describe('App component', () => {
it('renders main-heading with correct text', () => {
const mainHeading = findMainHeading();
expect(mainHeading.exists()).toBe(true);
- expect(mainHeading.text()).toContain('Security Configuration');
+ expect(mainHeading.text()).toContain('Security configuration');
});
describe('tabs', () => {
- const expectedTabs = ['security-testing', 'compliance-testing', 'vulnerability-management'];
+ const expectedTabs = ['security-testing', 'vulnerability-management'];
it('renders GlTab Component', () => {
expect(findTab().exists()).toBe(true);
@@ -174,9 +117,8 @@ describe('App component', () => {
it('renders right amount of feature cards for given props with correct props', () => {
const cards = findFeatureCards();
- expect(cards).toHaveLength(2);
+ expect(cards).toHaveLength(1);
expect(cards.at(0).props()).toEqual({ feature: securityFeaturesMock[0] });
- expect(cards.at(1).props()).toEqual({ feature: complianceFeaturesMock[0] });
});
it('renders a basic description', () => {
@@ -188,7 +130,6 @@ describe('App component', () => {
});
it('should not show configuration History Link when gitlabCiPresent & gitlabCiHistoryPath are not defined', () => {
- expect(findComplianceViewHistoryLink().exists()).toBe(false);
expect(findSecurityViewHistoryLink().exists()).toBe(false);
});
});
@@ -205,17 +146,19 @@ describe('App component', () => {
});
describe('when error occurs', () => {
+ const errorMessage = 'There was a manage via MR error';
+
it('should show Alert with error Message', async () => {
expect(findManageViaMRErrorAlert().exists()).toBe(false);
- findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error');
+ findFeatureCards().at(0).vm.$emit('error', errorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
- expect(findManageViaMRErrorAlert().text()).toEqual('There was a manage via MR error');
+ expect(findManageViaMRErrorAlert().text()).toBe(errorMessage);
});
it('should hide Alert when it is dismissed', async () => {
- findFeatureCards().at(1).vm.$emit('error', 'There was a manage via MR error');
+ findFeatureCards().at(0).vm.$emit('error', errorMessage);
await nextTick();
expect(findManageViaMRErrorAlert().exists()).toBe(true);
@@ -306,7 +249,6 @@ describe('App component', () => {
createComponent({
augmentedSecurityFeatures: securityFeaturesMock,
- augmentedComplianceFeatures: complianceFeaturesMock,
autoDevopsEnabled: true,
});
@@ -328,80 +270,12 @@ describe('App component', () => {
});
});
- describe('upgrade banner', () => {
- const makeAvailable = (available) => (feature) => ({ ...feature, available });
-
- describe('given at least one unavailable feature', () => {
- beforeEach(() => {
- createComponent({
- augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(false)),
- });
- });
-
- it('renders the banner', () => {
- expect(findUpgradeBanner().exists()).toBe(true);
- });
-
- it('calls the dismiss callback when closing the banner', () => {
- expect(userCalloutDismissSpy).not.toHaveBeenCalled();
-
- findUpgradeBanner().vm.$emit('close');
-
- expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('given at least one unavailable feature, but banner is already dismissed', () => {
- beforeEach(() => {
- createComponent({
- augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(false)),
- shouldShowCallout: false,
- });
- });
-
- it('does not render the banner', () => {
- expect(findUpgradeBanner().exists()).toBe(false);
- });
- });
-
- describe('given all features are available', () => {
- beforeEach(() => {
- createComponent({
- augmentedSecurityFeatures: securityFeaturesMock.map(makeAvailable(true)),
- augmentedComplianceFeatures: complianceFeaturesMock.map(makeAvailable(true)),
- });
- });
-
- it('does not render the banner', () => {
- expect(findUpgradeBanner().exists()).toBe(false);
- });
- });
- });
-
describe('when given latestPipelinePath props', () => {
beforeEach(() => {
createComponent({
latestPipelinePath: 'test/path',
});
});
-
- it('should show latest pipeline info on the security tab with correct link when latestPipelinePath is defined', () => {
- const latestPipelineInfoSecurity = findByTestId('latest-pipeline-info-security');
-
- expect(latestPipelineInfoSecurity.text()).toMatchInterpolatedText(
- i18n.latestPipelineDescription,
- );
- expect(latestPipelineInfoSecurity.find('a').attributes('href')).toBe('test/path');
- });
-
- it('should show latest pipeline info on the compliance tab with correct link when latestPipelinePath is defined', () => {
- const latestPipelineInfoCompliance = findByTestId('latest-pipeline-info-compliance');
-
- expect(latestPipelineInfoCompliance.text()).toMatchInterpolatedText(
- i18n.latestPipelineDescription,
- );
- expect(latestPipelineInfoCompliance.find('a').attributes('href')).toBe('test/path');
- });
});
describe('given gitlabCiPresent & gitlabCiHistoryPath props', () => {
@@ -413,10 +287,8 @@ describe('App component', () => {
});
it('should show configuration History Link', () => {
- expect(findComplianceViewHistoryLink().exists()).toBe(true);
expect(findSecurityViewHistoryLink().exists()).toBe(true);
- expect(findComplianceViewHistoryLink().attributes('href')).toBe('test/historyPath');
expect(findSecurityViewHistoryLink().attributes('href')).toBe('test/historyPath');
});
});
@@ -424,7 +296,7 @@ describe('App component', () => {
describe('Vulnerability management', () => {
const props = { securityTrainingEnabled: true };
- beforeEach(async () => {
+ beforeEach(() => {
createComponent({
...props,
});
diff --git a/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js b/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js
index 467ae35408c..df1fa1a8084 100644
--- a/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js
+++ b/spec/frontend/security_configuration/components/auto_dev_ops_alert_spec.js
@@ -23,10 +23,6 @@ describe('AutoDevopsAlert component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains correct body text', () => {
expect(wrapper.text()).toContain('Quickly enable all');
});
diff --git a/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js b/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js
index 778fea2896a..22f45a92f70 100644
--- a/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js
+++ b/spec/frontend/security_configuration/components/auto_dev_ops_enabled_alert_spec.js
@@ -21,10 +21,6 @@ describe('AutoDevopsEnabledAlert component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('contains correct body text', () => {
expect(wrapper.text()).toMatchInterpolatedText(AutoDevopsEnabledAlert.i18n.body);
});
diff --git a/spec/frontend/security_configuration/components/feature_card_spec.js b/spec/frontend/security_configuration/components/feature_card_spec.js
index d10722be8ea..983a66a7fd3 100644
--- a/spec/frontend/security_configuration/components/feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/feature_card_spec.js
@@ -1,10 +1,16 @@
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { securityFeatures } from '~/security_configuration/components/constants';
import FeatureCard from '~/security_configuration/components/feature_card.vue';
import FeatureCardBadge from '~/security_configuration/components/feature_card_badge.vue';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
-import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
+import {
+ REPORT_TYPE_BREACH_AND_ATTACK_SIMULATION,
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_SAST_IAC,
+} from '~/vue_shared/security_reports/constants';
+import { manageViaMRErrorMessage } from '../constants';
import { makeFeature } from './utils';
describe('FeatureCard component', () => {
@@ -78,7 +84,6 @@ describe('FeatureCard component', () => {
};
afterEach(() => {
- wrapper.destroy();
feature = undefined;
});
@@ -107,8 +112,8 @@ describe('FeatureCard component', () => {
});
it('should catch and emit manage-via-mr-error', () => {
- findManageViaMr().vm.$emit('error', 'There was a manage via MR error');
- expect(wrapper.emitted('error')).toEqual([['There was a manage via MR error']]);
+ findManageViaMr().vm.$emit('error', manageViaMRErrorMessage);
+ expect(wrapper.emitted('error')).toEqual([[manageViaMRErrorMessage]]);
});
});
@@ -265,6 +270,56 @@ describe('FeatureCard component', () => {
expect(links.exists()).toBe(false);
});
});
+
+ describe('given an available secondary with a configuration guide', () => {
+ beforeEach(() => {
+ feature = makeFeature({
+ available: true,
+ configurationHelpPath: null,
+ secondary: {
+ name: 'secondary name',
+ description: 'secondary description',
+ configurationHelpPath: '/secondary',
+ configurationText: null,
+ },
+ });
+ createComponent({ feature });
+ });
+
+ it('shows the secondary action', () => {
+ const links = findLinks({
+ text: 'Configuration guide',
+ href: feature.secondary.configurationHelpPath,
+ });
+ expect(links.exists()).toBe(true);
+ expect(links).toHaveLength(1);
+ });
+ });
+
+ describe('given an unavailable secondary with a configuration guide', () => {
+ beforeEach(() => {
+ feature = makeFeature({
+ available: false,
+ configurationHelpPath: null,
+ secondary: {
+ name: 'secondary name',
+ description: 'secondary description',
+ configurationHelpPath: '/secondary',
+ configurationText: null,
+ },
+ });
+ createComponent({ feature });
+ });
+
+ it('does not show the secondary action', () => {
+ const links = findLinks({
+ text: 'Configuration guide',
+ href: feature.secondary.configurationHelpPath,
+ });
+ expect(links.exists()).toBe(false);
+ expect(links).toHaveLength(0);
+ });
+ });
});
describe('information badge', () => {
@@ -290,4 +345,48 @@ describe('FeatureCard component', () => {
});
});
});
+
+ describe('status and badge', () => {
+ describe.each`
+ context | available | configured | expectedStatus
+ ${'configured BAS feature'} | ${true} | ${true} | ${null}
+ ${'unavailable BAS feature'} | ${false} | ${false} | ${'Available with Ultimate'}
+ ${'unconfigured BAS feature'} | ${true} | ${false} | ${null}
+ `('given $context', ({ available, configured, expectedStatus }) => {
+ beforeEach(() => {
+ const securityFeature = securityFeatures.find(
+ ({ type }) => REPORT_TYPE_BREACH_AND_ATTACK_SIMULATION === type,
+ );
+ feature = { ...securityFeature, available, configured };
+ createComponent({ feature });
+ });
+
+ it('should show an incubating feature badge', () => {
+ expect(findBadge().exists()).toBe(true);
+ });
+
+ if (expectedStatus) {
+ it(`should show the status "${expectedStatus}"`, () => {
+ expect(wrapper.findByTestId('feature-status').text()).toBe(expectedStatus);
+ });
+ }
+ });
+
+ describe.each`
+ context | available | configured
+ ${'configured SAST IaC feature'} | ${true} | ${true}
+ ${'unavailable SAST IaC feature'} | ${false} | ${false}
+ ${'unconfigured SAST IaC feature'} | ${true} | ${false}
+ `('given $context', ({ available, configured }) => {
+ beforeEach(() => {
+ const securityFeature = securityFeatures.find(({ type }) => REPORT_TYPE_SAST_IAC === type);
+ feature = { ...securityFeature, available, configured };
+ createComponent({ feature });
+ });
+
+ it(`should not show a status`, () => {
+ expect(wrapper.findByTestId('feature-status').exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 8f2b5383191..2982cef7c74 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -106,7 +106,7 @@ describe('TrainingProviderList component', () => {
projectFullPath: testProjectPath,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
securityTrainingEnabled: true,
@@ -132,7 +132,6 @@ describe('TrainingProviderList component', () => {
const toggleFirstProvider = () => findFirstToggle().vm.$emit('change', testProviderIds[0]);
afterEach(() => {
- wrapper.destroy();
apolloProvider = null;
});
@@ -254,7 +253,7 @@ describe('TrainingProviderList component', () => {
expect(findLogos().at(provider).attributes('role')).toBe('presentation');
});
- it.each(providerIndexArray)('renders the svg content for provider %s', async (provider) => {
+ it.each(providerIndexArray)('renders the svg content for provider %s', (provider) => {
expect(findLogos().at(provider).html()).toContain(
TEMP_PROVIDER_LOGOS[testProviderName[provider]].svg,
);
@@ -402,7 +401,7 @@ describe('TrainingProviderList component', () => {
it('has disabled state for radio', () => {
findPrimaryProviderRadios().wrappers.forEach((radio) => {
- expect(radio.attributes('disabled')).toBe('true');
+ expect(radio.attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/security_configuration/components/upgrade_banner_spec.js b/spec/frontend/security_configuration/components/upgrade_banner_spec.js
deleted file mode 100644
index c34d8e47a6c..00000000000
--- a/spec/frontend/security_configuration/components/upgrade_banner_spec.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import { GlBanner } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import UpgradeBanner, {
- SECURITY_UPGRADE_BANNER,
- UPGRADE_OR_FREE_TRIAL,
-} from '~/security_configuration/components/upgrade_banner.vue';
-
-const upgradePath = '/upgrade';
-
-describe('UpgradeBanner component', () => {
- let wrapper;
- let closeSpy;
- let primarySpy;
- let trackingSpy;
-
- const createComponent = (propsData) => {
- closeSpy = jest.fn();
- primarySpy = jest.fn();
-
- wrapper = shallowMountExtended(UpgradeBanner, {
- provide: {
- upgradePath,
- },
- propsData,
- listeners: {
- close: closeSpy,
- primary: primarySpy,
- },
- });
- };
-
- const findGlBanner = () => wrapper.findComponent(GlBanner);
-
- const expectTracking = (action, label) => {
- return expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
- label,
- property: SECURITY_UPGRADE_BANNER,
- });
- };
-
- beforeEach(() => {
- trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
- });
-
- afterEach(() => {
- wrapper.destroy();
- unmockTracking();
- });
-
- describe('when the component renders', () => {
- it('tracks an event', () => {
- expect(trackingSpy).not.toHaveBeenCalled();
-
- createComponent();
-
- expectTracking('render', SECURITY_UPGRADE_BANNER);
- });
- });
-
- describe('when ready', () => {
- beforeEach(() => {
- createComponent();
- trackingSpy.mockClear();
- });
-
- it('passes the expected props to GlBanner', () => {
- expect(findGlBanner().props()).toMatchObject({
- title: UpgradeBanner.i18n.title,
- buttonText: UpgradeBanner.i18n.buttonText,
- buttonLink: upgradePath,
- });
- });
-
- it('renders the list of benefits', () => {
- const wrapperText = wrapper.text();
-
- expect(wrapperText).toContain('Immediately begin risk analysis and remediation');
- expect(wrapperText).toContain('statistics in the merge request');
- expect(wrapperText).toContain('statistics across projects');
- expect(wrapperText).toContain('Runtime security metrics');
- expect(wrapperText).toContain('More scan types, including DAST,');
- });
-
- describe('when user interacts', () => {
- it(`re-emits GlBanner's close event & tracks an event`, () => {
- expect(closeSpy).not.toHaveBeenCalled();
- expect(trackingSpy).not.toHaveBeenCalled();
-
- wrapper.findComponent(GlBanner).vm.$emit('close');
-
- expect(closeSpy).toHaveBeenCalledTimes(1);
- expectTracking('dismiss_banner', SECURITY_UPGRADE_BANNER);
- });
-
- it(`re-emits GlBanner's primary event & tracks an event`, () => {
- expect(primarySpy).not.toHaveBeenCalled();
- expect(trackingSpy).not.toHaveBeenCalled();
-
- wrapper.findComponent(GlBanner).vm.$emit('primary');
-
- expect(primarySpy).toHaveBeenCalledTimes(1);
- expectTracking('click_button', UPGRADE_OR_FREE_TRIAL);
- });
- });
- });
-});
diff --git a/spec/frontend/security_configuration/constants.js b/spec/frontend/security_configuration/constants.js
new file mode 100644
index 00000000000..d31036a2534
--- /dev/null
+++ b/spec/frontend/security_configuration/constants.js
@@ -0,0 +1 @@
+export const manageViaMRErrorMessage = 'There was a manage via MR error';
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 2fe3b59cea3..df10d33e2f0 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -1,9 +1,19 @@
+import {
+ SAST_NAME,
+ SAST_SHORT_NAME,
+ SAST_DESCRIPTION,
+ SAST_HELP_PATH,
+ SAST_CONFIG_HELP_PATH,
+} from '~/security_configuration/components/constants';
+import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
+
export const testProjectPath = 'foo/bar';
export const testProviderIds = [101, 102, 103];
-export const testProviderName = ['Kontra', 'Secure Code Warrior', 'Other Vendor'];
+export const testProviderName = ['Kontra', 'Secure Code Warrior', 'SecureFlag'];
export const testTrainingUrls = [
'https://www.vendornameone.com/url',
'https://www.vendornametwo.com/url',
+ 'https://www.vendornamethree.com/url',
];
const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [
@@ -100,3 +110,23 @@ export const updateSecurityTrainingProvidersErrorResponse = {
},
},
};
+
+export const securityFeaturesMock = [
+ {
+ name: SAST_NAME,
+ shortName: SAST_SHORT_NAME,
+ description: SAST_DESCRIPTION,
+ helpPath: SAST_HELP_PATH,
+ configurationHelpPath: SAST_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST,
+ available: true,
+ },
+];
+
+export const provideMock = {
+ upgradePath: '/upgrade',
+ autoDevopsHelpPagePath: '/autoDevopsHelpPagePath',
+ autoDevopsPath: '/autoDevopsPath',
+ projectFullPath: 'namespace/project',
+ vulnerabilityTrainingDocsPath: 'user/application_security/vulnerabilities/index',
+};
diff --git a/spec/frontend/security_configuration/utils_spec.js b/spec/frontend/security_configuration/utils_spec.js
index 241e69204d2..6e731e45da2 100644
--- a/spec/frontend/security_configuration/utils_spec.js
+++ b/spec/frontend/security_configuration/utils_spec.js
@@ -9,13 +9,6 @@ describe('augmentFeatures', () => {
},
];
- const mockComplianceFeatures = [
- {
- name: 'LICENSE_COMPLIANCE',
- type: 'LICENSE_COMPLIANCE',
- },
- ];
-
const mockFeaturesWithSecondary = [
{
name: 'DAST',
@@ -51,30 +44,25 @@ describe('augmentFeatures', () => {
const expectedOutputDefault = {
augmentedSecurityFeatures: mockSecurityFeatures,
- augmentedComplianceFeatures: mockComplianceFeatures,
};
const expectedOutputSecondary = {
augmentedSecurityFeatures: mockSecurityFeatures,
- augmentedComplianceFeatures: mockFeaturesWithSecondary,
};
const expectedOutputCustomFeature = {
augmentedSecurityFeatures: mockValidCustomFeature,
- augmentedComplianceFeatures: mockComplianceFeatures,
};
- describe('returns an object with augmentedSecurityFeatures and augmentedComplianceFeatures when', () => {
+ describe('returns an object with augmentedSecurityFeatures when', () => {
it('given an empty array', () => {
- expect(augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, [])).toEqual(
- expectedOutputDefault,
- );
+ expect(augmentFeatures(mockSecurityFeatures, [])).toEqual(expectedOutputDefault);
});
it('given an invalid populated array', () => {
- expect(
- augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockInvalidCustomFeature),
- ).toEqual(expectedOutputDefault);
+ expect(augmentFeatures(mockSecurityFeatures, mockInvalidCustomFeature)).toEqual(
+ expectedOutputDefault,
+ );
});
it('features have secondary key', () => {
@@ -84,21 +72,17 @@ describe('augmentFeatures', () => {
});
it('given a valid populated array', () => {
- expect(
- augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockValidCustomFeature),
- ).toEqual(expectedOutputCustomFeature);
+ expect(augmentFeatures(mockSecurityFeatures, mockValidCustomFeature)).toEqual(
+ expectedOutputCustomFeature,
+ );
});
});
describe('returns an object with camelcased keys', () => {
it('given a customfeature in snakecase', () => {
- expect(
- augmentFeatures(
- mockSecurityFeatures,
- mockComplianceFeatures,
- mockValidCustomFeatureSnakeCase,
- ),
- ).toEqual(expectedOutputCustomFeature);
+ expect(augmentFeatures(mockSecurityFeatures, mockValidCustomFeatureSnakeCase)).toEqual(
+ expectedOutputCustomFeature,
+ );
});
});
});
diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
deleted file mode 100644
index efe3f7e8dbf..00000000000
--- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap
+++ /dev/null
@@ -1,85 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`self-monitor component When the self-monitor project has not been created default state to match the default snapshot 1`] = `
-<section
- class="settings no-animate js-self-monitoring-settings"
->
- <div
- class="settings-header"
- >
- <h4
- class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only"
- >
-
- Self-monitoring
-
- </h4>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="js-settings-toggle"
- icon=""
- size="medium"
- variant="default"
- >
- Expand
- </gl-button-stub>
-
- <p
- class="js-section-sub-header"
- >
-
- Activate or deactivate instance self-monitoring.
-
- <gl-link-stub
- href="/help/administration/monitoring/gitlab_self_monitoring_project/index"
- >
- Learn more.
- </gl-link-stub>
- </p>
- </div>
-
- <div
- class="settings-content"
- >
- <form
- name="self-monitoring-form"
- >
- <p>
- Activate self-monitoring to create a project to use to monitor the health of your instance.
- </p>
-
- <gl-form-group-stub
- labeldescription=""
- optionaltext="(optional)"
- >
- <gl-toggle-stub
- label="Self-monitoring"
- labelposition="top"
- />
- </gl-form-group-stub>
- </form>
- </div>
-
- <gl-modal-stub
- arialabel=""
- cancel-title="Cancel"
- category="primary"
- dismisslabel="Close"
- modalclass=""
- modalid="delete-self-monitor-modal"
- ok-title="Delete self-monitoring project"
- ok-variant="danger"
- size="md"
- title="Deactivate self-monitoring?"
- titletag="h4"
- >
- <div>
-
- Deactivating self-monitoring deletes the self-monitoring project. Are you sure you want to deactivate self-monitoring and delete the project?
-
- </div>
- </gl-modal-stub>
-</section>
-`;
diff --git a/spec/frontend/self_monitor/components/self_monitor_form_spec.js b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
deleted file mode 100644
index 35f2734dde3..00000000000
--- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { GlButton, GlToggle } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
-import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue';
-import { createStore } from '~/self_monitor/store';
-
-describe('self-monitor component', () => {
- let wrapper;
- let store;
-
- describe('When the self-monitor project has not been created', () => {
- beforeEach(() => {
- store = createStore({
- projectEnabled: false,
- selfMonitoringProjectExists: false,
- createSelfMonitoringProjectPath: '/create',
- deleteSelfMonitoringProjectPath: '/delete',
- });
- });
-
- afterEach(() => {
- if (wrapper.destroy) {
- wrapper.destroy();
- }
- });
-
- describe('default state', () => {
- it('to match the default snapshot', () => {
- wrapper = shallowMount(SelfMonitor, { store });
-
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- it('renders header text', () => {
- wrapper = shallowMount(SelfMonitor, { store });
-
- expect(wrapper.find('.js-section-header').text()).toBe('Self-monitoring');
- });
-
- describe('expand/collapse button', () => {
- it('renders as an expand button by default', () => {
- wrapper = shallowMount(SelfMonitor, { store });
-
- const button = wrapper.findComponent(GlButton);
-
- expect(button.text()).toBe('Expand');
- });
- });
-
- describe('sub-header', () => {
- it('renders descriptive text', () => {
- wrapper = shallowMount(SelfMonitor, { store });
-
- expect(wrapper.find('.js-section-sub-header').text()).toContain(
- 'Activate or deactivate instance self-monitoring.',
- );
- });
- });
-
- describe('settings-content', () => {
- it('renders the form description without a link', () => {
- wrapper = shallowMount(SelfMonitor, { store });
-
- expect(wrapper.vm.selfMonitoringFormText).toContain(
- 'Activate self-monitoring to create a project to use to monitor the health of your instance.',
- );
- });
-
- it('renders the form description with a link', () => {
- store = createStore({
- projectEnabled: true,
- selfMonitoringProjectExists: true,
- createSelfMonitoringProjectPath: '/create',
- deleteSelfMonitoringProjectPath: '/delete',
- selfMonitoringProjectFullPath: 'instance-administrators-random/gitlab-self-monitoring',
- });
-
- wrapper = shallowMount(SelfMonitor, { store });
-
- expect(
- wrapper.findComponent({ ref: 'selfMonitoringFormText' }).find('a').attributes('href'),
- ).toEqual(`${TEST_HOST}/instance-administrators-random/gitlab-self-monitoring`);
- });
-
- it('renders toggle', () => {
- wrapper = shallowMount(SelfMonitor, { store });
-
- expect(wrapper.findComponent(GlToggle).props('label')).toBe(
- SelfMonitor.formLabels.createProject,
- );
- });
- });
- });
-});
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
deleted file mode 100644
index 0e28e330009..00000000000
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ /dev/null
@@ -1,254 +0,0 @@
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-import {
- HTTP_STATUS_ACCEPTED,
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
- HTTP_STATUS_OK,
-} from '~/lib/utils/http_status';
-import * as actions from '~/self_monitor/store/actions';
-import * as types from '~/self_monitor/store/mutation_types';
-import createState from '~/self_monitor/store/state';
-
-describe('self-monitor actions', () => {
- let state;
- let mock;
-
- beforeEach(() => {
- state = createState();
- mock = new MockAdapter(axios);
- });
-
- describe('setSelfMonitor', () => {
- it('commits the SET_ENABLED mutation', () => {
- return testAction(
- actions.setSelfMonitor,
- null,
- state,
- [{ type: types.SET_ENABLED, payload: null }],
- [],
- );
- });
- });
-
- describe('resetAlert', () => {
- it('commits the SET_ENABLED mutation', () => {
- return testAction(
- actions.resetAlert,
- null,
- state,
- [{ type: types.SET_SHOW_ALERT, payload: false }],
- [],
- );
- });
- });
-
- describe('requestCreateProject', () => {
- describe('success', () => {
- beforeEach(() => {
- state.createProjectEndpoint = '/create';
- state.createProjectStatusEndpoint = '/create_status';
- mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
- job_id: '123',
- });
- mock.onGet(state.createProjectStatusEndpoint).reply(HTTP_STATUS_OK, {
- project_full_path: '/self-monitor-url',
- });
- });
-
- it('dispatches status request with job data', () => {
- return testAction(
- actions.requestCreateProject,
- null,
- state,
- [
- {
- type: types.SET_LOADING,
- payload: true,
- },
- ],
- [
- {
- type: 'requestCreateProjectStatus',
- payload: '123',
- },
- ],
- );
- });
-
- it('dispatches success with project path', () => {
- return testAction(
- actions.requestCreateProjectStatus,
- null,
- state,
- [],
- [
- {
- type: 'requestCreateProjectSuccess',
- payload: { project_full_path: '/self-monitor-url' },
- },
- ],
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- state.createProjectEndpoint = '/create';
- mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- });
-
- it('dispatches error', () => {
- return testAction(
- actions.requestCreateProject,
- null,
- state,
- [
- {
- type: types.SET_LOADING,
- payload: true,
- },
- ],
- [
- {
- type: 'requestCreateProjectError',
- payload: new Error('Request failed with status code 500'),
- },
- ],
- );
- });
- });
-
- describe('requestCreateProjectSuccess', () => {
- it('should commit the received data', () => {
- return testAction(
- actions.requestCreateProjectSuccess,
- { project_full_path: '/self-monitor-url' },
- state,
- [
- { type: types.SET_LOADING, payload: false },
- { type: types.SET_PROJECT_URL, payload: '/self-monitor-url' },
- {
- type: types.SET_ALERT_CONTENT,
- payload: {
- actionName: 'viewSelfMonitorProject',
- actionText: 'View project',
- message: 'Self-monitoring project successfully created.',
- },
- },
- { type: types.SET_SHOW_ALERT, payload: true },
- { type: types.SET_PROJECT_CREATED, payload: true },
- ],
- [
- {
- payload: true,
- type: 'setSelfMonitor',
- },
- ],
- );
- });
- });
- });
-
- describe('deleteSelfMonitorProject', () => {
- describe('success', () => {
- beforeEach(() => {
- state.deleteProjectEndpoint = '/delete';
- state.deleteProjectStatusEndpoint = '/delete-status';
- mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
- job_id: '456',
- });
- mock.onGet(state.deleteProjectStatusEndpoint).reply(HTTP_STATUS_OK, {
- status: 'success',
- });
- });
-
- it('dispatches status request with job data', () => {
- return testAction(
- actions.requestDeleteProject,
- null,
- state,
- [
- {
- type: types.SET_LOADING,
- payload: true,
- },
- ],
- [
- {
- type: 'requestDeleteProjectStatus',
- payload: '456',
- },
- ],
- );
- });
-
- it('dispatches success with status', () => {
- return testAction(
- actions.requestDeleteProjectStatus,
- null,
- state,
- [],
- [
- {
- type: 'requestDeleteProjectSuccess',
- payload: { status: 'success' },
- },
- ],
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- state.deleteProjectEndpoint = '/delete';
- mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- });
-
- it('dispatches error', () => {
- return testAction(
- actions.requestDeleteProject,
- null,
- state,
- [
- {
- type: types.SET_LOADING,
- payload: true,
- },
- ],
- [
- {
- type: 'requestDeleteProjectError',
- payload: new Error('Request failed with status code 500'),
- },
- ],
- );
- });
- });
-
- describe('requestDeleteProjectSuccess', () => {
- it('should commit mutations to remove previously set data', () => {
- return testAction(
- actions.requestDeleteProjectSuccess,
- null,
- state,
- [
- { type: types.SET_PROJECT_URL, payload: '' },
- { type: types.SET_PROJECT_CREATED, payload: false },
- {
- type: types.SET_ALERT_CONTENT,
- payload: {
- actionName: 'createProject',
- actionText: 'Undo',
- message: 'Self-monitoring project successfully deleted.',
- },
- },
- { type: types.SET_SHOW_ALERT, payload: true },
- { type: types.SET_LOADING, payload: false },
- ],
- [],
- );
- });
- });
- });
-});
diff --git a/spec/frontend/self_monitor/store/mutations_spec.js b/spec/frontend/self_monitor/store/mutations_spec.js
deleted file mode 100644
index 315450f3aef..00000000000
--- a/spec/frontend/self_monitor/store/mutations_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import mutations from '~/self_monitor/store/mutations';
-import createState from '~/self_monitor/store/state';
-
-describe('self-monitoring mutations', () => {
- let localState;
-
- beforeEach(() => {
- localState = createState();
- });
-
- describe('SET_ENABLED', () => {
- it('sets selfMonitor', () => {
- mutations.SET_ENABLED(localState, true);
-
- expect(localState.projectEnabled).toBe(true);
- });
- });
-
- describe('SET_PROJECT_CREATED', () => {
- it('sets projectCreated', () => {
- mutations.SET_PROJECT_CREATED(localState, true);
-
- expect(localState.projectCreated).toBe(true);
- });
- });
-
- describe('SET_SHOW_ALERT', () => {
- it('sets showAlert', () => {
- mutations.SET_SHOW_ALERT(localState, true);
-
- expect(localState.showAlert).toBe(true);
- });
- });
-
- describe('SET_PROJECT_URL', () => {
- it('sets projectPath', () => {
- mutations.SET_PROJECT_URL(localState, '/url/');
-
- expect(localState.projectPath).toBe('/url/');
- });
- });
-
- describe('SET_LOADING', () => {
- it('sets loading', () => {
- mutations.SET_LOADING(localState, true);
-
- expect(localState.loading).toBe(true);
- });
- });
-
- describe('SET_ALERT_CONTENT', () => {
- it('set alertContent', () => {
- const alertContent = {
- message: 'success',
- actionText: 'undo',
- actionName: 'createProject',
- };
-
- mutations.SET_ALERT_CONTENT(localState, alertContent);
-
- expect(localState.alertContent).toBe(alertContent);
- });
- });
-});
diff --git a/spec/frontend/sentry/index_spec.js b/spec/frontend/sentry/index_spec.js
index 2dd528a8a1c..aa19bb03cda 100644
--- a/spec/frontend/sentry/index_spec.js
+++ b/spec/frontend/sentry/index_spec.js
@@ -4,8 +4,6 @@ import LegacySentryConfig from '~/sentry/legacy_sentry_config';
import SentryConfig from '~/sentry/sentry_config';
describe('Sentry init', () => {
- let originalGon;
-
const dsn = 'https://123@sentry.gitlab.test/123';
const environment = 'test';
const currentUserId = '1';
@@ -14,7 +12,6 @@ describe('Sentry init', () => {
const featureCategory = 'my_feature_category';
beforeEach(() => {
- originalGon = window.gon;
window.gon = {
sentry_dsn: dsn,
sentry_environment: environment,
@@ -28,10 +25,6 @@ describe('Sentry init', () => {
jest.spyOn(SentryConfig, 'init').mockImplementation();
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
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\./);
@@ -61,4 +54,49 @@ describe('Sentry init', () => {
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/legacy_index_spec.js b/spec/frontend/sentry/legacy_index_spec.js
index 5c336f8392e..493b4dfde67 100644
--- a/spec/frontend/sentry/legacy_index_spec.js
+++ b/spec/frontend/sentry/legacy_index_spec.js
@@ -4,8 +4,6 @@ import LegacySentryConfig from '~/sentry/legacy_sentry_config';
import SentryConfig from '~/sentry/sentry_config';
describe('Sentry init', () => {
- let originalGon;
-
const dsn = 'https://123@sentry.gitlab.test/123';
const environment = 'test';
const currentUserId = '1';
@@ -14,7 +12,6 @@ describe('Sentry init', () => {
const featureCategory = 'my_feature_category';
beforeEach(() => {
- originalGon = window.gon;
window.gon = {
sentry_dsn: dsn,
sentry_environment: environment,
@@ -28,10 +25,6 @@ describe('Sentry init', () => {
jest.spyOn(SentryConfig, 'init').mockImplementation();
});
- afterEach(() => {
- window.gon = originalGon;
- });
-
it('exports legacy version of Sentry in the global object', () => {
// eslint-disable-next-line no-underscore-dangle
expect(window._Sentry.SDK_VERSION).toMatch(/^5\./);
diff --git a/spec/frontend/sentry/sentry_browser_wrapper_spec.js b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
index f4d646bab78..55354eceb8d 100644
--- a/spec/frontend/sentry/sentry_browser_wrapper_spec.js
+++ b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
@@ -25,7 +25,7 @@ describe('SentryBrowserWrapper', () => {
let mockCaptureMessage;
let mockWithScope;
- beforeEach(async () => {
+ beforeEach(() => {
mockCaptureException = jest.fn();
mockCaptureMessage = jest.fn();
mockWithScope = jest.fn();
diff --git a/spec/frontend/sentry/sentry_config_spec.js b/spec/frontend/sentry/sentry_config_spec.js
index 44acbee9b38..34c5221ef0d 100644
--- a/spec/frontend/sentry/sentry_config_spec.js
+++ b/spec/frontend/sentry/sentry_config_spec.js
@@ -1,5 +1,4 @@
import * as Sentry from 'sentrybrowser7';
-import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from '~/sentry/constants';
import SentryConfig from '~/sentry/sentry_config';
@@ -62,11 +61,8 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- sampleRate: SAMPLE_RATE,
allowUrls: options.allowUrls,
environment: options.environment,
- ignoreErrors: IGNORE_ERRORS,
- denyUrls: DENY_URLS,
});
});
@@ -82,11 +78,8 @@ describe('SentryConfig', () => {
expect(Sentry.init).toHaveBeenCalledWith({
dsn: options.dsn,
release: options.release,
- sampleRate: SAMPLE_RATE,
allowUrls: options.allowUrls,
environment: 'development',
- ignoreErrors: IGNORE_ERRORS,
- denyUrls: DENY_URLS,
});
});
});
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index 85cd8d51272..60267cf31be 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -1,22 +1,27 @@
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { createWrapper } from '@vue/test-utils';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { useFakeDate } from 'helpers/fake_date';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import EmojiPicker from '~/emoji/components/picker.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import stubChildren from 'helpers/stub_children';
import SetStatusModalWrapper from '~/set_status_modal/set_status_modal_wrapper.vue';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { BV_HIDE_MODAL } from '~/lib/utils/constants';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('SetStatusModalWrapper', () => {
let wrapper;
+ const mockToastShow = jest.fn();
+
const $toast = {
- show: jest.fn(),
+ show: mockToastShow,
};
const defaultEmoji = 'speech_balloon';
@@ -58,21 +63,9 @@ describe('SetStatusModalWrapper', () => {
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
const findAvailabilityCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const getEmojiPicker = () => wrapper.findComponent(EmojiPickerStub);
-
- const initModal = async ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
- const modal = findModal();
- // mock internal emoji methods
- wrapper.vm.showEmojiMenu = jest.fn();
- wrapper.vm.hideEmojiMenu = jest.fn();
- if (mockOnUpdateSuccess) wrapper.vm.onUpdateSuccess = jest.fn();
- if (mockOnUpdateFailure) wrapper.vm.onUpdateFail = jest.fn();
-
- modal.vm.$emit('shown');
- await nextTick();
- };
+ const initModal = () => findModal().vm.$emit('shown');
afterEach(() => {
- wrapper.destroy();
clearEmojiMock();
});
@@ -149,6 +142,8 @@ describe('SetStatusModalWrapper', () => {
describe('update status', () => {
describe('succeeds', () => {
+ useMockLocationHelper();
+
beforeEach(async () => {
await initEmojiMock();
wrapper = createComponent();
@@ -195,11 +190,21 @@ describe('SetStatusModalWrapper', () => {
});
});
- it('calls the "onUpdateSuccess" handler', async () => {
+ it('displays a toast message and reloads window', async () => {
+ findModal().vm.$emit('primary');
+ await nextTick();
+
+ expect(mockToastShow).toHaveBeenCalledWith('Status updated');
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+
+ it('closes modal', async () => {
+ const rootWrapper = createWrapper(wrapper.vm.$root);
+
findModal().vm.$emit('primary');
await nextTick();
- expect(wrapper.vm.onUpdateSuccess).toHaveBeenCalled();
+ expect(rootWrapper.emitted(BV_HIDE_MODAL)).toEqual([['set-user-status-modal']]);
});
});
@@ -228,11 +233,22 @@ describe('SetStatusModalWrapper', () => {
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
});
- it('calls the "onUpdateFail" handler', async () => {
+ it('displays an error alert', async () => {
+ findModal().vm.$emit('primary');
+ await nextTick();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: "Sorry, we weren't able to set your status. Please try again later.",
+ });
+ });
+
+ it('closes modal', async () => {
+ const rootWrapper = createWrapper(wrapper.vm.$root);
+
findModal().vm.$emit('primary');
await nextTick();
- expect(wrapper.vm.onUpdateFail).toHaveBeenCalled();
+ expect(rootWrapper.emitted(BV_HIDE_MODAL)).toEqual([['set-user-status-modal']]);
});
});
@@ -244,7 +260,7 @@ describe('SetStatusModalWrapper', () => {
return initModal({ mockOnUpdateFailure: false });
});
- it('flashes an error message', async () => {
+ it('alerts an error message', async () => {
findModal().vm.$emit('primary');
await nextTick();
diff --git a/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
index a4a2a86dc73..a6ad90123b7 100644
--- a/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
@@ -37,10 +37,6 @@ describe('UserProfileSetStatusWrapper', () => {
const findInput = (name) => wrapper.find(`[name="${name}"]`);
const findSetStatusForm = () => wrapper.findComponent(SetStatusForm);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders `SetStatusForm` component and passes expected props', () => {
createComponent();
diff --git a/spec/frontend/settings_panels_spec.js b/spec/frontend/settings_panels_spec.js
index d59e1a20b27..1ef91181e1d 100644
--- a/spec/frontend/settings_panels_spec.js
+++ b/spec/frontend/settings_panels_spec.js
@@ -1,10 +1,11 @@
import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlGroupsEdit from 'test_fixtures/groups/edit.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initSettingsPanels, { isExpanded } from '~/settings_panels';
describe('Settings Panels', () => {
beforeEach(() => {
- loadHTMLFixture('groups/edit.html');
+ setHTMLFixture(htmlGroupsEdit);
});
afterEach(() => {
diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js
index e859d435f48..88ad9204d08 100644
--- a/spec/frontend/shortcuts_spec.js
+++ b/spec/frontend/shortcuts_spec.js
@@ -1,67 +1,41 @@
import $ from 'jquery';
import { flatten } from 'lodash';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import Shortcuts from '~/behaviors/shortcuts/shortcuts';
-
-const mockMousetrap = {
- bind: jest.fn(),
- unbind: jest.fn(),
-};
-
-jest.mock('mousetrap', () => {
- return jest.fn().mockImplementation(() => mockMousetrap);
-});
-
-jest.mock('mousetrap/plugins/pause/mousetrap-pause', () => {});
+import htmlSnippetsShow from 'test_fixtures/snippets/show.html';
+import { Mousetrap } from '~/lib/mousetrap';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import Shortcuts, { LOCAL_MOUSETRAP_DATA_KEY } from '~/behaviors/shortcuts/shortcuts';
+import MarkdownPreview from '~/behaviors/preview_markdown';
describe('Shortcuts', () => {
- const fixtureName = 'snippets/show.html';
const createEvent = (type, target) =>
$.Event(type, {
target,
});
+ let shortcuts;
+
+ beforeAll(() => {
+ shortcuts = new Shortcuts();
+ });
beforeEach(() => {
- loadHTMLFixture(fixtureName);
+ setHTMLFixture(htmlSnippetsShow);
+
+ new Shortcuts(); // eslint-disable-line no-new
+ new MarkdownPreview(); // eslint-disable-line no-new
- jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus');
- jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus');
jest.spyOn(document.querySelector('#search'), 'focus');
- new Shortcuts(); // eslint-disable-line no-new
+ jest.spyOn(Mousetrap.prototype, 'stopCallback');
+ jest.spyOn(Mousetrap.prototype, 'bind').mockImplementation();
+ jest.spyOn(Mousetrap.prototype, 'unbind').mockImplementation();
});
afterEach(() => {
resetHTMLFixture();
});
- describe('toggleMarkdownPreview', () => {
- it('focuses preview button in form', () => {
- Shortcuts.toggleMarkdownPreview(
- createEvent('KeyboardEvent', document.querySelector('.js-new-note-form .js-note-text')),
- );
-
- expect(
- document.querySelector('.js-new-note-form .js-md-preview-button').focus,
- ).toHaveBeenCalled();
- });
-
- it('focuses preview button inside edit comment form', () => {
- document.querySelector('.js-note-edit').click();
-
- Shortcuts.toggleMarkdownPreview(
- createEvent('KeyboardEvent', document.querySelector('.edit-note .js-note-text')),
- );
-
- expect(
- document.querySelector('.js-new-note-form .js-md-preview-button').focus,
- ).not.toHaveBeenCalled();
- expect(document.querySelector('.edit-note .js-md-preview-button').focus).toHaveBeenCalled();
- });
- });
-
describe('markdown shortcuts', () => {
- let shortcuts;
+ let shortcutElements;
beforeEach(() => {
// Get all shortcuts specified with md-shortcuts attributes in the fixture.
@@ -71,7 +45,7 @@ describe('Shortcuts', () => {
// [ 'mod+i' ],
// [ 'mod+k' ]
// ]
- shortcuts = $('.edit-note .js-md')
+ shortcutElements = $('.edit-note .js-md')
.map(function getShortcutsFromToolbarBtn() {
const mdShortcuts = $(this).data('md-shortcuts');
@@ -83,19 +57,26 @@ describe('Shortcuts', () => {
});
describe('initMarkdownEditorShortcuts', () => {
+ let $textarea;
+ let localMousetrapInstance;
+
beforeEach(() => {
- Shortcuts.initMarkdownEditorShortcuts($('.edit-note textarea'));
+ $textarea = $('.edit-note textarea');
+ Shortcuts.initMarkdownEditorShortcuts($textarea);
+ localMousetrapInstance = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY);
});
it('attaches a Mousetrap handler for every markdown shortcut specified with md-shortcuts', () => {
- const expectedCalls = shortcuts.map((s) => [s, expect.any(Function)]);
+ const expectedCalls = shortcutElements.map((s) => [s, expect.any(Function)]);
- expect(mockMousetrap.bind.mock.calls).toEqual(expectedCalls);
+ expect(Mousetrap.prototype.bind.mock.calls).toEqual(expectedCalls);
});
it('attaches a stopCallback that allows each markdown shortcut specified with md-shortcuts', () => {
- flatten(shortcuts).forEach((s) => {
- expect(mockMousetrap.stopCallback(null, null, s)).toBe(false);
+ flatten(shortcutElements).forEach((s) => {
+ expect(
+ localMousetrapInstance.stopCallback.call(localMousetrapInstance, null, null, s),
+ ).toBe(false);
});
});
});
@@ -104,25 +85,67 @@ describe('Shortcuts', () => {
it('does nothing if initMarkdownEditorShortcuts was not previous called', () => {
Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea'));
- expect(mockMousetrap.unbind.mock.calls).toEqual([]);
+ expect(Mousetrap.prototype.unbind.mock.calls).toEqual([]);
});
it('removes Mousetrap handlers for every markdown shortcut specified with md-shortcuts', () => {
Shortcuts.initMarkdownEditorShortcuts($('.edit-note textarea'));
Shortcuts.removeMarkdownEditorShortcuts($('.edit-note textarea'));
- const expectedCalls = shortcuts.map((s) => [s]);
+ const expectedCalls = shortcutElements.map((s) => [s]);
- expect(mockMousetrap.unbind.mock.calls).toEqual(expectedCalls);
+ expect(Mousetrap.prototype.unbind.mock.calls).toEqual(expectedCalls);
});
});
});
describe('focusSearch', () => {
- it('focuses the search bar', () => {
- Shortcuts.focusSearch(createEvent('KeyboardEvent'));
+ describe('when super sidebar is NOT enabled', () => {
+ let originalGon;
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = { use_new_navigation: false };
+ });
+
+ afterEach(() => {
+ window.gon = originalGon;
+ });
+
+ it('focuses the search bar', () => {
+ Shortcuts.focusSearch(createEvent('KeyboardEvent'));
+ expect(document.querySelector('#search').focus).toHaveBeenCalled();
+ });
+ });
+ });
- expect(document.querySelector('#search').focus).toHaveBeenCalled();
+ describe('bindCommand(s)', () => {
+ it('bindCommand calls Mousetrap.bind correctly', () => {
+ const mockCommand = { defaultKeys: ['m'] };
+ const mockCallback = () => {};
+
+ shortcuts.bindCommand(mockCommand, mockCallback);
+
+ expect(Mousetrap.prototype.bind).toHaveBeenCalledTimes(1);
+ const [callArguments] = Mousetrap.prototype.bind.mock.calls;
+ expect(callArguments[0]).toEqual(mockCommand.defaultKeys);
+ expect(callArguments[1]).toBe(mockCallback);
+ });
+
+ it('bindCommands calls Mousetrap.bind correctly', () => {
+ const mockCommandsAndCallbacks = [
+ [{ defaultKeys: ['1'] }, () => {}],
+ [{ defaultKeys: ['2'] }, () => {}],
+ ];
+
+ shortcuts.bindCommands(mockCommandsAndCallbacks);
+
+ expect(Mousetrap.prototype.bind).toHaveBeenCalledTimes(mockCommandsAndCallbacks.length);
+ const { calls } = Mousetrap.prototype.bind.mock;
+
+ mockCommandsAndCallbacks.forEach(([mockCommand, mockCallback], i) => {
+ expect(calls[i][0]).toEqual(mockCommand.defaultKeys);
+ expect(calls[i][1]).toBe(mockCallback);
+ });
});
});
});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 60edab8766a..81b65f4f050 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -30,10 +30,6 @@ describe('AssigneeAvatarLink component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTooltipText = () => wrapper.attributes('title');
const findUserLink = () => wrapper.findComponent(GlLink);
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
index 7df37d11987..b6b3dbd5b6b 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
@@ -7,7 +7,6 @@ const TEST_AVATAR = `${TEST_HOST}/avatar.png`;
const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`;
describe('AssigneeAvatar', () => {
- let origGon;
let wrapper;
function createComponent(props = {}) {
@@ -24,15 +23,9 @@ describe('AssigneeAvatar', () => {
}
beforeEach(() => {
- origGon = window.gon;
window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL };
});
- afterEach(() => {
- window.gon = origGon;
- wrapper.destroy();
- });
-
const findImg = () => wrapper.find('img');
it('does not show warning icon if assignee can merge', () => {
diff --git a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
index 14a6bdbf907..d561c761c99 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
@@ -17,11 +17,6 @@ describe('AssigneeTitle component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('assignee title', () => {
it('renders assignee', () => {
wrapper = createComponent({
diff --git a/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
index 080171fb2ea..0501c1bae23 100644
--- a/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_realtime_spec.js
@@ -49,7 +49,6 @@ describe('Assignees Realtime', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
SidebarMediator.singleton = null;
});
diff --git a/spec/frontend/sidebar/components/assignees/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js
index d422292ed9e..1661e28abd2 100644
--- a/spec/frontend/sidebar/components/assignees/assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js
@@ -25,10 +25,6 @@ describe('Assignee component', () => {
const findComponentTextNoUsers = () => wrapper.find('[data-testid="no-value"]');
const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No assignees/users', () => {
it('displays no assignee icon when collapsed', () => {
createWrapper();
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
index 7e7d4921cfa..40d3d090bb4 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -26,10 +26,6 @@ describe('CollapsedAssigneeList component', () => {
const findAssignees = () => wrapper.findAllComponents(CollapsedAssignee);
const getTooltipTitle = () => wrapper.attributes('title');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No assignees/users', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
index 4db95114b96..851eaedf0bd 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
@@ -21,10 +21,6 @@ describe('CollapsedAssignee assignee component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has author name', () => {
createComponent();
diff --git a/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
index 1161fefcc64..82145b82e21 100644
--- a/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/issuable_assignees_spec.js
@@ -20,11 +20,6 @@ describe('IssuableAssignees', () => {
const findUncollapsedAssigneeList = () => wrapper.findComponent(UncollapsedAssigneeList);
const findEmptyAssignee = () => wrapper.find('[data-testid="none"]');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when no assignees are present', () => {
it.each`
signedIn | editable | message
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
index 58b174059fa..a189d3656a2 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
@@ -39,9 +39,6 @@ describe('sidebar assignees', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
index 3aca346ff5f..9f7c587ca9d 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js
@@ -5,8 +5,8 @@ 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 '~/flash';
-import { IssuableType } from '~/issues/constants';
+import { createAlert } from '~/alert';
+import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
@@ -17,7 +17,7 @@ import updateIssueAssigneesMutation from '~/sidebar/queries/update_issue_assigne
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import { issuableQueryResponse, updateIssueAssigneesMutationResponse } from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const updateIssueAssigneesMutationSuccess = jest
.fn()
@@ -98,10 +98,7 @@ describe('Sidebar assignees widget', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
fakeApollo = null;
- delete gon.current_username;
});
describe('with passed initial assignees', () => {
@@ -397,7 +394,7 @@ describe('Sidebar assignees widget', () => {
});
it('does not render invite members link on non-issue sidebar', async () => {
- createComponent({ props: { issuableType: IssuableType.MergeRequest } });
+ createComponent({ props: { issuableType: TYPE_MERGE_REQUEST } });
await waitForPromises();
expect(findInviteMembersLink().exists()).toBe(false);
});
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
index 6c22d2f687d..27c31ac56c9 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js
@@ -21,11 +21,6 @@ describe('boards sidebar remove issue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('template', () => {
it('renders title', () => {
const title = 'Sidebar item title';
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 b738d931040..501048bf056 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_invite_members_spec.js
@@ -16,10 +16,6 @@ describe('Sidebar invite members component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when directly inviting members', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
index be0b14fa997..25a19b5808b 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
@@ -1,6 +1,6 @@
import { GlAvatarLabeled, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
const user = {
@@ -32,20 +32,17 @@ describe('Sidebar participant component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not show `Busy` status when user is not busy', () => {
createComponent();
expect(findAvatar().props('label')).toBe(user.name);
+ expect(wrapper.text()).not.toContain('Busy');
});
it('shows `Busy` status when user is busy', () => {
createComponent({ status: { availability: 'BUSY' } });
- expect(findAvatar().props('label')).toBe(`${user.name} (Busy)`);
+ expect(wrapper.text()).toContain('Busy');
});
it('does not render a warning icon', () => {
@@ -56,13 +53,13 @@ describe('Sidebar participant component', () => {
describe('when on merge request sidebar', () => {
it('when project member cannot merge', () => {
- createComponent({ issuableType: IssuableType.MergeRequest });
+ createComponent({ issuableType: TYPE_MERGE_REQUEST });
expect(findIcon().exists()).toBe(true);
});
it('when project member can merge', () => {
- createComponent({ issuableType: IssuableType.MergeRequest, canMerge: true });
+ createComponent({ issuableType: TYPE_MERGE_REQUEST, canMerge: true });
expect(findIcon().exists()).toBe(false);
});
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
index 03c2e1a37a9..c74a714cca4 100644
--- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -24,10 +24,6 @@ describe('UncollapsedAssigneeList component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findMoreButton = () => wrapper.find('.user-list-more button');
describe('One assignee/user', () => {
diff --git a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
index 37c16bc9235..e54ba31a30c 100644
--- a/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
+++ b/spec/frontend/sidebar/components/assignees/user_name_with_status_spec.js
@@ -18,10 +18,6 @@ describe('UserNameWithStatus', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('will render the users name', () => {
expect(wrapper.html()).toContain(name);
});
@@ -41,7 +37,7 @@ describe('UserNameWithStatus', () => {
});
it('will render "Busy"', () => {
- expect(wrapper.text()).toContain('(Busy)');
+ expect(wrapper.text()).toContain('Busy');
});
});
@@ -53,7 +49,7 @@ describe('UserNameWithStatus', () => {
});
it("renders user's name with pronouns", () => {
- expect(wrapper.text()).toMatchInterpolatedText(`${name} (${pronouns})`);
+ expect(wrapper.text()).toMatchInterpolatedText(`${name}(${pronouns})`);
});
});
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
index 81354d64a90..4a2b3b30e6d 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_content_spec.js
@@ -18,10 +18,6 @@ describe('Sidebar Confidentiality Content', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits `expandSidebar` event on collapsed icon click', () => {
createComponent();
findCollapsedIcon().trigger('click');
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 b27f7c6b4e1..1ca20dad1c6 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js
@@ -2,11 +2,11 @@ import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
import { confidentialityQueries } from '~/sidebar/constants';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Sidebar Confidentiality Form', () => {
let wrapper;
@@ -38,10 +38,6 @@ describe('Sidebar Confidentiality Form', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('emits a `closeForm` event when Cancel button is clicked', () => {
createComponent();
findCancelButton().vm.$emit('click');
@@ -58,7 +54,7 @@ describe('Sidebar Confidentiality Form', () => {
expect(findConfidentialToggle().props('loading')).toBe(true);
});
- it('creates a flash if mutation is rejected', async () => {
+ it('creates an alert if mutation is rejected', async () => {
createComponent({ mutate: jest.fn().mockRejectedValue('Error!') });
findConfidentialToggle().vm.$emit('click', new MouseEvent('click'));
await waitForPromises();
@@ -68,7 +64,7 @@ describe('Sidebar Confidentiality Form', () => {
});
});
- it('creates a flash if mutation contains errors', async () => {
+ it('creates an alert if mutation contains errors', async () => {
createComponent({
mutate: jest.fn().mockResolvedValue({
data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } },
diff --git a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
index e486a8e9ec7..39b30307dd7 100644
--- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
+++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js
@@ -4,7 +4,7 @@ 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 '~/flash';
+import { createAlert } from '~/alert';
import SidebarConfidentialityContent from '~/sidebar/components/confidential/sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from '~/sidebar/components/confidential/sidebar_confidentiality_form.vue';
import SidebarConfidentialityWidget, {
@@ -14,7 +14,7 @@ import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import { issueConfidentialityResponse } from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -48,7 +48,6 @@ describe('Sidebar Confidentiality Widget', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -120,7 +119,7 @@ describe('Sidebar Confidentiality Widget', () => {
});
});
- it('displays a flash message when query is rejected', async () => {
+ it('displays an alert message when query is rejected', async () => {
createComponent({
confidentialQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
diff --git a/spec/frontend/sidebar/components/copy/copyable_field_spec.js b/spec/frontend/sidebar/components/copy/copyable_field_spec.js
index 7790d77bc65..03e131aab35 100644
--- a/spec/frontend/sidebar/components/copy/copyable_field_spec.js
+++ b/spec/frontend/sidebar/components/copy/copyable_field_spec.js
@@ -20,10 +20,6 @@ describe('SidebarCopyableField', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
diff --git a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
index c3de076d6aa..2ae80b2c97b 100644
--- a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
+++ b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import SidebarReferenceWidget from '~/sidebar/components/copy/sidebar_reference_widget.vue';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
@@ -39,10 +39,6 @@ describe('Sidebar Reference Widget', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when reference is loading', () => {
it('sets CopyableField `is-loading` prop to `true`', () => {
createComponent({ referenceQueryHandler: jest.fn().mockReturnValue(new Promise(() => {})) });
@@ -52,7 +48,7 @@ describe('Sidebar Reference Widget', () => {
describe.each([
[TYPE_ISSUE, issueReferenceQuery],
- [IssuableType.MergeRequest, mergeRequestReferenceQuery],
+ [TYPE_MERGE_REQUEST, mergeRequestReferenceQuery],
])('when issuableType is %s', (issuableType, referenceQuery) => {
it('sets CopyableField `value` prop to reference value', async () => {
createComponent({
diff --git a/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
index ca43c219d92..546cabd07d3 100644
--- a/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
+++ b/spec/frontend/sidebar/components/crm_contacts/crm_contacts_spec.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import CrmContacts from '~/sidebar/components/crm_contacts/crm_contacts.vue';
import getIssueCrmContactsQuery from '~/sidebar/queries/get_issue_crm_contacts.query.graphql';
import issueCrmContactsSubscription from '~/sidebar/queries/issue_crm_contacts.subscription.graphql';
@@ -13,7 +13,7 @@ import {
issueCrmContactsUpdateNullResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Issue crm contacts component', () => {
Vue.use(VueApollo);
@@ -39,7 +39,6 @@ describe('Issue crm contacts component', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
index 67413cffdda..8f82a2d1258 100644
--- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
@@ -4,16 +4,21 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarFormattedDate from '~/sidebar/components/date/sidebar_formatted_date.vue';
import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.vue';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
-import { issuableDueDateResponse, issuableStartDateResponse } from '../../mock_data';
+import issueDueDateSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
+import {
+ issuableDueDateResponse,
+ issuableStartDateResponse,
+ issueDueDateSubscriptionResponse,
+} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -22,10 +27,6 @@ describe('Sidebar date Widget', () => {
let fakeApollo;
const date = '2021-04-15';
- window.gon = {
- first_day_of_week: 1,
- };
-
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]');
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
@@ -33,6 +34,7 @@ describe('Sidebar date Widget', () => {
const createComponent = ({
dueDateQueryHandler = jest.fn().mockResolvedValue(issuableDueDateResponse()),
startDateQueryHandler = jest.fn().mockResolvedValue(issuableStartDateResponse()),
+ dueDateSubscriptionHandler = jest.fn().mockResolvedValue(issueDueDateSubscriptionResponse()),
canInherit = false,
dateType = undefined,
issuableType = 'issue',
@@ -40,6 +42,7 @@ describe('Sidebar date Widget', () => {
fakeApollo = createMockApollo([
[issueDueDateQuery, dueDateQueryHandler],
[epicStartDateQuery, startDateQueryHandler],
+ [issueDueDateSubscription, dueDateSubscriptionHandler],
]);
wrapper = shallowMount(SidebarDateWidget, {
@@ -61,8 +64,11 @@ describe('Sidebar date Widget', () => {
});
};
+ beforeEach(() => {
+ window.gon.first_day_of_week = 1;
+ });
+
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -125,18 +131,30 @@ describe('Sidebar date Widget', () => {
it('uses a correct prop to set the initial date and first day of the week for GlDatePicker', () => {
expect(findDatePicker().props()).toMatchObject({
- value: null,
+ value: new Date(date),
autocomplete: 'off',
defaultDate: expect.any(Object),
firstDay: window.gon.first_day_of_week,
});
});
- it('renders GlDatePicker', async () => {
+ it('renders GlDatePicker', () => {
expect(findDatePicker().exists()).toBe(true);
});
});
+ describe('real time issue due date feature', () => {
+ it('should call the subscription', async () => {
+ const dueDateSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(issueDueDateSubscriptionResponse());
+ createComponent({ dueDateSubscriptionHandler });
+ await waitForPromises();
+
+ expect(dueDateSubscriptionHandler).toHaveBeenCalled();
+ });
+ });
+
it.each`
canInherit | component | componentName | expected
${true} | ${SidebarFormattedDate} | ${'SidebarFormattedDate'} | ${false}
@@ -153,13 +171,13 @@ describe('Sidebar date Widget', () => {
},
);
- it('does not render SidebarInheritDate when canInherit is true and date is loading', async () => {
+ it('does not render SidebarInheritDate when canInherit is true and date is loading', () => {
createComponent({ canInherit: true });
expect(wrapper.findComponent(SidebarInheritDate).exists()).toBe(false);
});
- it('displays a flash message when query is rejected', async () => {
+ it('displays an alert message when query is rejected', async () => {
createComponent({
dueDateQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
diff --git a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
index cbe01263dcd..1bb910c53ea 100644
--- a/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_formatted_date_spec.js
@@ -27,10 +27,6 @@ describe('SidebarFormattedDate', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays formatted date', () => {
expect(findFormattedDate().text()).toBe('Apr 15, 2021');
});
diff --git a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
index a7556b9110c..97debe3088d 100644
--- a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js
@@ -31,10 +31,6 @@ describe('SidebarInheritDate', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays formatted fixed and inherited dates with radio buttons', () => {
expect(wrapper.findAllComponents(SidebarFormattedDate)).toHaveLength(2);
expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(2);
diff --git a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
index 1a78ce4ddee..e356f02a36b 100644
--- a/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/escalation_status_spec.js
@@ -17,10 +17,6 @@ describe('EscalationStatus', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownComponent = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findDropdownMenu = () => findDropdownComponent().find('.dropdown-menu');
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 2dded61c073..00b57b4916e 100644
--- a/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
+++ b/spec/frontend/sidebar/components/incidents/sidebar_escalation_status_spec.js
@@ -18,11 +18,11 @@ import {
} from '~/sidebar/constants';
import waitForPromises from 'helpers/wait_for_promises';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { logError } from '~/lib/logger';
jest.mock('~/lib/logger');
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -57,7 +57,7 @@ describe('SidebarEscalationStatus', () => {
canUpdate: true,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
apolloProvider,
});
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
index 4f2a89e20db..084ca5ed3fc 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_button_spec.js
@@ -29,10 +29,6 @@ describe('DropdownButton', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownButton = () => wrapper.findComponent(GlButton);
const findDropdownText = () => wrapper.find('.dropdown-toggle-text');
const findDropdownIcon = () => wrapper.findComponent(GlIcon);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
index 59e95edfa20..7e53fcfe850 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -1,77 +1,70 @@
import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue';
import labelSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockConfig, mockSuggestedColors } from './mock_data';
Vue.use(Vuex);
-const createComponent = (initialState = mockConfig) => {
- const store = new Vuex.Store(labelSelectModule());
-
- store.dispatch('setInitialState', initialState);
-
- return shallowMount(DropdownContentsCreateView, {
- store,
- });
-};
-
describe('DropdownContentsCreateView', () => {
let wrapper;
const colors = Object.keys(mockSuggestedColors).map((color) => ({
[color]: mockSuggestedColors[color],
}));
+ const createComponent = (initialState = mockConfig) => {
+ const store = new Vuex.Store(labelSelectModule());
+
+ store.dispatch('setInitialState', initialState);
+
+ wrapper = shallowMountExtended(DropdownContentsCreateView, {
+ store,
+ });
+ };
+
+ const findColorSelectorInput = () => wrapper.findByTestId('selected-color');
+ const findLabelTitleInput = () => wrapper.findByTestId('label-title');
+ const findCreateClickButton = () => wrapper.findByTestId('create-click');
+ const findAllLinks = () => wrapper.find('.dropdown-content').findAllComponents(GlLink);
+
beforeEach(() => {
gon.suggested_label_colors = mockSuggestedColors;
- wrapper = createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
+ createComponent();
});
describe('computed', () => {
describe('disableCreate', () => {
it('returns `true` when label title and color is not defined', () => {
- expect(wrapper.vm.disableCreate).toBe(true);
+ expect(findCreateClickButton().props('disabled')).toBe(true);
});
it('returns `true` when `labelCreateInProgress` is true', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- labelTitle: 'Foo',
- selectedColor: '#ff0000',
- });
+ await findColorSelectorInput().vm.$emit('input', '#ff0000');
+ await findLabelTitleInput().vm.$emit('input', 'Foo');
wrapper.vm.$store.dispatch('requestCreateLabel');
await nextTick();
- expect(wrapper.vm.disableCreate).toBe(true);
+
+ expect(findCreateClickButton().props('disabled')).toBe(true);
});
it('returns `false` when label title and color is defined and create request is not already in progress', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- labelTitle: 'Foo',
- selectedColor: '#ff0000',
- });
+ await findColorSelectorInput().vm.$emit('input', '#ff0000');
+ await findLabelTitleInput().vm.$emit('input', 'Foo');
- await nextTick();
- expect(wrapper.vm.disableCreate).toBe(false);
+ expect(findCreateClickButton().props('disabled')).toBe(false);
});
});
describe('suggestedColors', () => {
it('returns array of color objects containing color code and name', () => {
colors.forEach((color, index) => {
- expect(wrapper.vm.suggestedColors[index]).toEqual(expect.objectContaining(color));
+ expect(findAllLinks().at(index).attributes('title')).toBe(Object.values(color)[0]);
});
});
});
@@ -86,29 +79,29 @@ describe('DropdownContentsCreateView', () => {
describe('getColorName', () => {
it('returns color name from color object', () => {
+ expect(findAllLinks().at(0).attributes('title')).toBe(Object.values(colors[0]).pop());
expect(wrapper.vm.getColorName(colors[0])).toBe(Object.values(colors[0]).pop());
});
});
describe('handleColorClick', () => {
- it('sets provided `color` param to `selectedColor` prop', () => {
- wrapper.vm.handleColorClick(colors[0]);
+ it('sets provided `color` param to `selectedColor` prop', async () => {
+ await findAllLinks()
+ .at(0)
+ .vm.$emit('click', { preventDefault: () => {} });
- expect(wrapper.vm.selectedColor).toBe(Object.keys(colors[0]).pop());
+ expect(findColorSelectorInput().attributes('value')).toBe(Object.keys(colors[0]).pop());
});
});
describe('handleCreateClick', () => {
it('calls action `createLabel` with object containing `labelTitle` & `selectedColor`', async () => {
jest.spyOn(wrapper.vm, 'createLabel').mockImplementation();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- labelTitle: 'Foo',
- selectedColor: '#ff0000',
- });
- wrapper.vm.handleCreateClick();
+ await findColorSelectorInput().vm.$emit('input', '#ff0000');
+ await findLabelTitleInput().vm.$emit('input', 'Foo');
+
+ findCreateClickButton().vm.$emit('click');
await nextTick();
expect(wrapper.vm.createLabel).toHaveBeenCalledWith(
@@ -158,27 +151,27 @@ describe('DropdownContentsCreateView', () => {
});
it('renders color block element for all suggested colors', () => {
- const colorBlocksEl = wrapper.find('.dropdown-content').findAllComponents(GlLink);
-
- colorBlocksEl.wrappers.forEach((colorBlock, index) => {
+ findAllLinks().wrappers.forEach((colorBlock, index) => {
expect(colorBlock.attributes('style')).toContain('background-color');
expect(colorBlock.attributes('title')).toBe(Object.values(colors[index]).pop());
});
});
it('renders color input element', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedColor: '#ff0000',
- });
+ await findColorSelectorInput().vm.$emit('input', '#ff0000');
await nextTick();
- const colorPreviewEl = wrapper.find('.color-input-container > .dropdown-label-color-preview');
- const colorInputEl = wrapper.find('.color-input-container').findComponent(GlFormInput);
+ const colorPreviewEl = wrapper
+ .find('.color-input-container')
+ .findAllComponents(GlFormInput)
+ .at(0);
+ const colorInputEl = wrapper
+ .find('.color-input-container')
+ .findAllComponents(GlFormInput)
+ .at(1);
expect(colorPreviewEl.exists()).toBe(true);
- expect(colorPreviewEl.attributes('style')).toContain('background-color');
+ expect(colorPreviewEl.attributes('value')).toBe('#ff0000');
expect(colorInputEl.exists()).toBe(true);
expect(colorInputEl.attributes('placeholder')).toBe('Use custom color #FF0000');
expect(colorInputEl.attributes('value')).toBe('#ff0000');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
index 865dc8fe8fb..5c6358a94ab 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -5,27 +5,31 @@ import {
GlSearchBoxByType,
GlLink,
} from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue';
import LabelItem from '~/sidebar/components/labels/labels_select_vue/label_item.vue';
-
+import { stubComponent } from 'helpers/stub_component';
import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions';
import * as getters from '~/sidebar/components/labels/labels_select_vue/store/getters';
import mutations from '~/sidebar/components/labels/labels_select_vue/store/mutations';
import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state';
-import { mockConfig, mockLabels, mockRegularLabel } from './mock_data';
+import { mockConfig, mockLabels } from './mock_data';
Vue.use(Vuex);
describe('DropdownContentsLabelsView', () => {
let wrapper;
+ let store;
+
+ const focusInputMock = jest.fn();
+ const updateSelectedLabelsMock = jest.fn();
+ const toggleDropdownContentsMock = jest.fn();
- const createComponent = (initialState = mockConfig) => {
- const store = new Vuex.Store({
+ const createComponent = (initialState = mockConfig, mountFn = shallowMountExtended) => {
+ store = new Vuex.Store({
getters,
mutations,
state: {
@@ -36,14 +40,20 @@ describe('DropdownContentsLabelsView', () => {
actions: {
...actions,
fetchLabels: jest.fn(),
+ updateSelectedLabels: updateSelectedLabelsMock,
+ toggleDropdownContents: toggleDropdownContentsMock,
},
});
store.dispatch('setInitialState', initialState);
- store.dispatch('receiveLabelsSuccess', mockLabels);
- wrapper = shallowMount(DropdownContentsLabelsView, {
+ wrapper = mountFn(DropdownContentsLabelsView, {
store,
+ stubs: {
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ methods: { focusInput: focusInputMock },
+ }),
+ },
});
};
@@ -51,48 +61,95 @@ describe('DropdownContentsLabelsView', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
+ const findDropdownContent = () => wrapper.findByTestId('dropdown-content');
+ const findDropdownTitle = () => wrapper.findByTestId('dropdown-title');
+ const findDropdownFooter = () => wrapper.findByTestId('dropdown-footer');
+ const findNoMatchingResults = () => wrapper.findByTestId('no-matching-results');
+ const findCreateLabelLink = () => wrapper.findByTestId('create-label-link');
+ const findLabelsList = () => wrapper.findByTestId('labels-list');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findLabelItems = () => wrapper.findAllComponents(LabelItem);
+
+ const setCurrentHighlightItem = (value) => {
+ let initialValue = -1;
+
+ while (initialValue < value) {
+ findLabelsList().trigger('keydown.down');
+ initialValue += 1;
+ }
+ };
+
+ describe('component', () => {
+ it('calls `focusInput` on searchInput field when the component appears', async () => {
+ findIntersectionObserver().vm.$emit('appear');
+
+ await nextTick();
+
+ expect(focusInputMock).toHaveBeenCalled();
+ });
+
+ it('removes loaded labels when the component disappears', async () => {
+ jest.spyOn(store, 'dispatch');
+
+ await findIntersectionObserver().vm.$emit('disappear');
+
+ expect(store.dispatch).toHaveBeenCalledWith(expect.anything(), []);
+ });
});
- const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]');
- const findDropdownTitle = () => wrapper.find('[data-testid="dropdown-title"]');
- const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ describe('labels', () => {
+ describe('when it is visible', () => {
+ beforeEach(() => {
+ createComponent(undefined, mountExtended);
+ store.dispatch('receiveLabelsSuccess', mockLabels);
+ });
- describe('computed', () => {
- describe('visibleLabels', () => {
- it('returns matching labels filtered with `searchKey`', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchKey: 'bug',
- });
-
- expect(wrapper.vm.visibleLabels.length).toBe(1);
- expect(wrapper.vm.visibleLabels[0].title).toBe('Bug');
+ it('returns matching labels filtered with `searchKey`', async () => {
+ await findSearchBoxByType().vm.$emit('input', 'bug');
+
+ const labelItems = findLabelItems();
+ expect(labelItems).toHaveLength(1);
+ expect(labelItems.at(0).text()).toBe('Bug');
+ });
+
+ it('returns matching labels with fuzzy filtering', async () => {
+ await findSearchBoxByType().vm.$emit('input', 'bg');
+
+ const labelItems = findLabelItems();
+ expect(labelItems).toHaveLength(2);
+ expect(labelItems.at(0).text()).toBe('Bug');
+ expect(labelItems.at(1).text()).toBe('Boog');
+ });
+
+ it('returns all labels when `searchKey` is empty', async () => {
+ await findSearchBoxByType().vm.$emit('input', '');
+
+ expect(findLabelItems()).toHaveLength(mockLabels.length);
+ });
+ });
+
+ describe('when it is clicked', () => {
+ beforeEach(() => {
+ createComponent(undefined, mountExtended);
+ store.dispatch('receiveLabelsSuccess', mockLabels);
});
- it('returns matching labels with fuzzy filtering', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchKey: 'bg',
- });
+ it('calls action `updateSelectedLabels` with provided `label` param', () => {
+ findLabelItems().at(0).findComponent(GlLink).vm.$emit('click');
- expect(wrapper.vm.visibleLabels.length).toBe(2);
- expect(wrapper.vm.visibleLabels[0].title).toBe('Bug');
- expect(wrapper.vm.visibleLabels[1].title).toBe('Boog');
+ expect(updateSelectedLabelsMock).toHaveBeenCalledWith(expect.anything(), [
+ { ...mockLabels[0], indeterminate: expect.anything(), set: expect.anything() },
+ ]);
});
- it('returns all labels when `searchKey` is empty', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchKey: '',
- });
+ it('calls action `toggleDropdownContents` when `state.allowMultiselect` is false', () => {
+ store.state.allowMultiselect = false;
+
+ findLabelItems().at(0).findComponent(GlLink).vm.$emit('click');
- expect(wrapper.vm.visibleLabels.length).toBe(mockLabels.length);
+ expect(toggleDropdownContentsMock).toHaveBeenCalled();
});
});
@@ -106,190 +163,110 @@ describe('DropdownContentsLabelsView', () => {
`(
'returns $returnValue when searchKey is "$searchKey" and visibleLabels is $labelsDescription',
async ({ searchKey, labels, returnValue }) => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchKey,
- });
+ store.dispatch('receiveLabelsSuccess', labels);
- wrapper.vm.$store.dispatch('receiveLabelsSuccess', labels);
+ await findSearchBoxByType().vm.$emit('input', searchKey);
- await nextTick();
-
- expect(wrapper.vm.showNoMatchingResultsMessage).toBe(returnValue);
+ expect(findNoMatchingResults().isVisible()).toBe(returnValue);
},
);
});
});
- describe('methods', () => {
- const fakePreventDefault = jest.fn();
+ describe('create label link', () => {
+ it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', async () => {
+ jest.spyOn(store, 'dispatch');
- describe('isLabelSelected', () => {
- it('returns true when provided `label` param is one of the selected labels', () => {
- expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true);
- });
+ await findCreateLabelLink().vm.$emit('click');
- it('returns false when provided `label` param is not one of the selected labels', () => {
- expect(wrapper.vm.isLabelSelected(mockLabels[1])).toBe(false);
- });
+ expect(store.dispatch).toHaveBeenCalledWith('receiveLabelsSuccess', []);
+ expect(store.dispatch).toHaveBeenCalledWith('toggleDropdownContentsCreateView');
});
+ });
- describe('handleComponentAppear', () => {
- it('calls `focusInput` on searchInput field', async () => {
- wrapper.vm.$refs.searchInput.focusInput = jest.fn();
-
- await wrapper.vm.handleComponentAppear();
-
- expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
- });
- });
-
- describe('handleComponentDisappear', () => {
- it('calls action `receiveLabelsSuccess` with empty array', () => {
- jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
-
- wrapper.vm.handleComponentDisappear();
+ describe('keyboard navigation', () => {
+ const fakePreventDefault = jest.fn();
- expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]);
- });
+ beforeEach(() => {
+ createComponent(undefined, mountExtended);
+ store.dispatch('receiveLabelsSuccess', mockLabels);
});
- describe('handleCreateLabelClick', () => {
- it('calls actions `receiveLabelsSuccess` with empty array and `toggleDropdownContentsCreateView`', () => {
- jest.spyOn(wrapper.vm, 'receiveLabelsSuccess');
- jest.spyOn(wrapper.vm, 'toggleDropdownContentsCreateView');
+ describe('when the "down" key is pressed', () => {
+ it('highlights the item', async () => {
+ expect(findLabelItems().at(0).classes()).not.toContain('is-focused');
- wrapper.vm.handleCreateLabelClick();
+ await findLabelsList().trigger('keydown.down');
- expect(wrapper.vm.receiveLabelsSuccess).toHaveBeenCalledWith([]);
- expect(wrapper.vm.toggleDropdownContentsCreateView).toHaveBeenCalled();
+ expect(findLabelItems().at(0).classes()).toContain('is-focused');
});
});
- describe('handleKeyDown', () => {
- it('decreases `currentHighlightItem` value by 1 when Up arrow key is pressed', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- currentHighlightItem: 1,
- });
+ describe('when the "up" arrow key is pressed', () => {
+ it('un-highlights the item', async () => {
+ await setCurrentHighlightItem(1);
- wrapper.vm.handleKeyDown({
- keyCode: UP_KEY_CODE,
- });
+ expect(findLabelItems().at(1).classes()).toContain('is-focused');
- expect(wrapper.vm.currentHighlightItem).toBe(0);
- });
-
- it('increases `currentHighlightItem` value by 1 when Down arrow key is pressed', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- currentHighlightItem: 1,
- });
-
- wrapper.vm.handleKeyDown({
- keyCode: DOWN_KEY_CODE,
- });
+ await findLabelsList().trigger('keydown.up');
- expect(wrapper.vm.currentHighlightItem).toBe(2);
+ expect(findLabelItems().at(1).classes()).not.toContain('is-focused');
});
+ });
- it('resets the search text when the Enter key is pressed', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- currentHighlightItem: 1,
- searchKey: 'bug',
- });
-
- wrapper.vm.handleKeyDown({
- keyCode: ENTER_KEY_CODE,
- preventDefault: fakePreventDefault,
- });
+ describe('when the "enter" key is pressed', () => {
+ it('resets the search text', async () => {
+ await setCurrentHighlightItem(1);
+ await findSearchBoxByType().vm.$emit('input', 'bug');
+ await findLabelsList().trigger('keydown.enter', { preventDefault: fakePreventDefault });
- expect(wrapper.vm.searchKey).toBe('');
+ expect(findSearchBoxByType().props('value')).toBe('');
expect(fakePreventDefault).toHaveBeenCalled();
});
- it('calls action `updateSelectedLabels` with currently highlighted label when Enter key is pressed', () => {
- jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- currentHighlightItem: 2,
- });
-
- wrapper.vm.handleKeyDown({
- keyCode: ENTER_KEY_CODE,
- preventDefault: fakePreventDefault,
- });
-
- expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockLabels[2]]);
- });
-
- it('calls action `toggleDropdownContents` when Esc key is pressed', () => {
- jest.spyOn(wrapper.vm, 'toggleDropdownContents').mockImplementation();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- currentHighlightItem: 1,
- });
+ it('calls action `updateSelectedLabels` with currently highlighted label', async () => {
+ await setCurrentHighlightItem(2);
+ await findLabelsList().trigger('keydown.enter', { preventDefault: fakePreventDefault });
- wrapper.vm.handleKeyDown({
- keyCode: ESC_KEY_CODE,
- });
-
- expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
- });
-
- it('calls action `scrollIntoViewIfNeeded` in next tick when any key is pressed', async () => {
- jest.spyOn(wrapper.vm, 'scrollIntoViewIfNeeded').mockImplementation();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- currentHighlightItem: 1,
- });
-
- wrapper.vm.handleKeyDown({
- keyCode: DOWN_KEY_CODE,
- });
-
- await nextTick();
- expect(wrapper.vm.scrollIntoViewIfNeeded).toHaveBeenCalled();
+ expect(updateSelectedLabelsMock).toHaveBeenCalledWith(expect.anything(), [mockLabels[2]]);
});
});
- describe('handleLabelClick', () => {
- beforeEach(() => {
- jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
- });
-
- it('calls action `updateSelectedLabels` with provided `label` param', () => {
- wrapper.vm.handleLabelClick(mockRegularLabel);
+ describe('when the "esc" key is pressed', () => {
+ it('calls action `toggleDropdownContents`', async () => {
+ await setCurrentHighlightItem(1);
+ await findLabelsList().trigger('keydown.esc');
- expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockRegularLabel]);
+ expect(toggleDropdownContentsMock).toHaveBeenCalled();
});
- it('calls action `toggleDropdownContents` when `state.allowMultiselect` is false', () => {
- jest.spyOn(wrapper.vm, 'toggleDropdownContents');
- wrapper.vm.$store.state.allowMultiselect = false;
+ it('scrolls dropdown content into view', async () => {
+ const containerTop = 500;
+ const labelTop = 0;
+
+ jest
+ .spyOn(findDropdownContent().element, 'getBoundingClientRect')
+ .mockReturnValueOnce({ top: containerTop });
- wrapper.vm.handleLabelClick(mockRegularLabel);
+ await setCurrentHighlightItem(1);
+ await findLabelsList().trigger('keydown.esc');
- expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
+ expect(findDropdownContent().element.scrollTop).toBe(labelTop - containerTop);
});
});
});
describe('template', () => {
+ beforeEach(() => {
+ store.dispatch('receiveLabelsSuccess', mockLabels);
+ });
+
it('renders gl-intersection-observer as component root', () => {
expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true);
});
it('renders gl-loading-icon component when `labelsFetchInProgress` prop is true', async () => {
- wrapper.vm.$store.dispatch('requestLabels');
+ store.dispatch('requestLabels');
await nextTick();
const loadingIconEl = findLoadingIcon();
@@ -329,30 +306,19 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders label elements for all labels', () => {
- expect(wrapper.findAllComponents(LabelItem)).toHaveLength(mockLabels.length);
+ expect(findLabelItems()).toHaveLength(mockLabels.length);
});
it('renders label element with `highlight` set to true when value of `currentHighlightItem` is more than -1', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- currentHighlightItem: 0,
- });
+ await setCurrentHighlightItem(0);
- await nextTick();
const labelItemEl = findDropdownContent().findComponent(LabelItem);
expect(labelItemEl.attributes('highlight')).toBe('true');
});
it('renders element containing "No matching results" when `searchKey` does not match with any label', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchKey: 'abc',
- });
-
- await nextTick();
+ await findSearchBoxByType().vm.$emit('input', 'abc');
const noMatchEl = findDropdownContent().find('li');
expect(noMatchEl.isVisible()).toBe(true);
@@ -360,7 +326,7 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders empty content while loading', async () => {
- wrapper.vm.$store.state.labelsFetchInProgress = true;
+ store.state.labelsFetchInProgress = true;
await nextTick();
const dropdownContent = findDropdownContent();
@@ -384,7 +350,7 @@ describe('DropdownContentsLabelsView', () => {
});
it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', async () => {
- wrapper.vm.$store.state.allowLabelCreate = false;
+ store.state.allowLabelCreate = false;
await nextTick();
const createLabelLink = findDropdownFooter().findAllComponents(GlLink).at(0);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
index e9ffda7c251..d74cea2827c 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js
@@ -2,9 +2,13 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants';
import DropdownContents from '~/sidebar/components/labels/labels_select_vue/dropdown_contents.vue';
import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
+import {
+ VARIANT_EMBEDDED,
+ VARIANT_SIDEBAR,
+ VARIANT_STANDALONE,
+} from '~/sidebar/components/labels/labels_select_widget/constants';
import { mockConfig } from './mock_data';
@@ -28,10 +32,6 @@ describe('DropdownContent', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('dropdownContentsView', () => {
it('returns string "dropdown-contents-create-view" when `showDropdownContentsCreateView` prop is `true`', () => {
@@ -54,10 +54,10 @@ describe('DropdownContent', () => {
describe('when `renderOnTop` is true', () => {
it.each`
- variant | expected
- ${DropdownVariant.Sidebar} | ${'bottom: 3rem'}
- ${DropdownVariant.Standalone} | ${'bottom: 2rem'}
- ${DropdownVariant.Embedded} | ${'bottom: 2rem'}
+ variant | expected
+ ${VARIANT_SIDEBAR} | ${'bottom: 3rem'}
+ ${VARIANT_STANDALONE} | ${'bottom: 2rem'}
+ ${VARIANT_EMBEDDED} | ${'bottom: 2rem'}
`('renders upward for $variant variant', ({ variant, expected }) => {
wrapper = createComponent({ ...mockConfig, variant }, { renderOnTop: true });
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
index 6c3fda421ff..367f6007194 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_title_spec.js
@@ -31,10 +31,6 @@ describe('DropdownTitle', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('renders component container element with string "Labels"', () => {
expect(wrapper.text()).toContain('Labels');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
index 56f25a1c6a4..6684cf0c5f4 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed_spec.js
@@ -19,15 +19,11 @@ describe('DropdownValueCollapsedComponent', () => {
wrapper = shallowMount(DropdownValueCollapsedComponent, {
propsData: { ...defaultProps, ...props },
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlIcon = () => wrapper.findComponent(GlIcon);
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
index a1ccc9d2ab1..70aafceb00c 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_value_spec.js
@@ -28,10 +28,6 @@ describe('DropdownValue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('labelFilterUrl', () => {
it('returns a label filter URL based on provided label param', () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
index e14c0e308ce..468dd14c9ee 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/label_item_spec.js
@@ -26,10 +26,6 @@ describe('LabelItem', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('renders gl-link component', () => {
expect(wrapper.findComponent(GlLink).exists()).toBe(true);
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
index a3b10c18374..3add96f2c03 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js
@@ -3,15 +3,18 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
-import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants';
import DropdownButton from '~/sidebar/components/labels/labels_select_vue/dropdown_button.vue';
import DropdownContents from '~/sidebar/components/labels/labels_select_vue/dropdown_contents.vue';
import DropdownTitle from '~/sidebar/components/labels/labels_select_vue/dropdown_title.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_vue/dropdown_value.vue';
import DropdownValueCollapsed from '~/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue';
import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
-
import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store';
+import {
+ VARIANT_EMBEDDED,
+ VARIANT_SIDEBAR,
+ VARIANT_STANDALONE,
+} from '~/sidebar/components/labels/labels_select_widget/constants';
import { mockConfig } from './mock_data';
@@ -40,10 +43,6 @@ describe('LabelsSelectRoot', () => {
store = new Vuex.Store(labelsSelectModule());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('handleVuexActionDispatch', () => {
const touchedLabels = [
@@ -173,7 +172,7 @@ describe('LabelsSelectRoot', () => {
});
describe('sets content direction based on viewport', () => {
- describe.each(Object.values(DropdownVariant))(
+ describe.each(Object.values([VARIANT_EMBEDDED, VARIANT_SIDEBAR, VARIANT_STANDALONE]))(
'when labels variant is "%s"',
({ variant }) => {
beforeEach(() => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
index 55651bccaa8..c27afb75375 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
@@ -1,14 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+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 * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions';
import * as types from '~/sidebar/components/labels/labels_select_vue/store/mutation_types';
import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('LabelsSelect Actions', () => {
let state;
@@ -100,7 +100,7 @@ describe('LabelsSelect Actions', () => {
);
});
- it('shows flash error', () => {
+ it('shows alert error', () => {
actions.receiveLabelsFailure({ commit: () => {} });
expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
@@ -184,7 +184,7 @@ describe('LabelsSelect Actions', () => {
);
});
- it('shows flash error', () => {
+ it('shows alert error', () => {
actions.receiveCreateLabelFailure({ commit: () => {} });
expect(createAlert).toHaveBeenCalledWith({ message: 'Error creating label.' });
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 79b164b0ea7..9c8d9656955 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
@@ -4,18 +4,20 @@ 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 '~/flash';
+import { createAlert } from '~/alert';
import { workspaceLabelsQueries } from '~/sidebar/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 {
mockRegularLabel,
mockSuggestedColors,
createLabelSuccessfulResponse,
workspaceLabelsQueryResponse,
+ workspaceLabelsQueryEmptyResponse,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const colors = Object.keys(mockSuggestedColors);
@@ -61,14 +63,16 @@ describe('DropdownContentsCreateView', () => {
mutationHandler = createLabelSuccessHandler,
labelCreateType = 'project',
workspaceType = 'project',
+ labelsResponse = workspaceLabelsQueryResponse,
+ searchTerm = '',
} = {}) => {
const mockApollo = createMockApollo([[createLabelMutation, mutationHandler]]);
mockApollo.clients.defaultClient.cache.writeQuery({
query: workspaceLabelsQueries[workspaceType].query,
- data: workspaceLabelsQueryResponse.data,
+ data: labelsResponse.data,
variables: {
fullPath: '',
- searchTerm: '',
+ searchTerm,
},
});
@@ -87,10 +91,6 @@ describe('DropdownContentsCreateView', () => {
gon.suggested_label_colors = mockSuggestedColors;
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a palette of 21 colors', () => {
createComponent();
expect(findAllColors()).toHaveLength(21);
@@ -98,17 +98,17 @@ describe('DropdownContentsCreateView', () => {
it('selects a color after clicking on colored block', async () => {
createComponent();
- expect(findSelectedColor().attributes('style')).toBeUndefined();
+ expect(findSelectedColorText().attributes('value')).toBe(DEFAULT_LABEL_COLOR);
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
await nextTick();
- expect(findSelectedColor().attributes('style')).toBe('background-color: rgb(0, 153, 102);');
+ expect(findSelectedColor().attributes('value')).toBe('#009966');
});
it('shows correct color hex code after selecting a color', async () => {
createComponent();
- expect(findSelectedColorText().attributes('value')).toBe('');
+ expect(findSelectedColorText().attributes('value')).toBe(DEFAULT_LABEL_COLOR);
findAllColors().at(0).vm.$emit('click', new Event('mouseclick'));
await nextTick();
@@ -127,6 +127,7 @@ describe('DropdownContentsCreateView', () => {
it('disables a Create button if color is not set', async () => {
createComponent();
findLabelTitleInput().vm.$emit('input', 'Test title');
+ findSelectedColorText().vm.$emit('input', '');
await nextTick();
expect(findCreateButton().props('disabled')).toBe(true);
@@ -236,4 +237,21 @@ describe('DropdownContentsCreateView', () => {
titleTakenError.data.labelCreate.errors[0],
);
});
+
+ describe('when empty labels response', () => {
+ it('is able to create label with searched text when empty response', async () => {
+ createComponent({ searchTerm: '', labelsResponse: workspaceLabelsQueryEmptyResponse });
+
+ findLabelTitleInput().vm.$emit('input', 'random');
+
+ findCreateButton().vm.$emit('click');
+ await waitForPromises();
+
+ expect(createLabelSuccessHandler).toHaveBeenCalledWith({
+ color: DEFAULT_LABEL_COLOR,
+ projectPath: '',
+ title: 'random',
+ });
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
index 913badccbe4..c939856331d 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js
@@ -9,15 +9,15 @@ 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 '~/flash';
+import { createAlert } from '~/alert';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import { VARIANT_SIDEBAR } from '~/sidebar/components/labels/labels_select_widget/constants';
import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { mockConfig, workspaceLabelsQueryResponse } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -48,7 +48,7 @@ describe('DropdownContentsLabelsView', () => {
wrapper = shallowMount(DropdownContentsLabelsView, {
apolloProvider: mockApollo,
provide: {
- variant: DropdownVariant.Sidebar,
+ variant: VARIANT_SIDEBAR,
...injected,
},
propsData: {
@@ -64,10 +64,6 @@ describe('DropdownContentsLabelsView', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findLabels = () => wrapper.findAllComponents(LabelItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findObserver = () => wrapper.findComponent(GlIntersectionObserver);
@@ -100,11 +96,11 @@ describe('DropdownContentsLabelsView', () => {
await waitForPromises();
});
- it('does not render loading icon', async () => {
+ it('does not render loading icon', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('renders labels list', async () => {
+ it('renders labels list', () => {
expect(findLabelsList().exists()).toBe(true);
expect(findLabels()).toHaveLength(2);
});
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
index 9bbb1413ee9..3abd87a69d6 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js
@@ -1,6 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants';
+import {
+ VARIANT_EMBEDDED,
+ VARIANT_STANDALONE,
+} from '~/sidebar/components/labels/labels_select_widget/constants';
import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue';
@@ -66,10 +69,6 @@ describe('DropdownContent', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findCreateView = () => wrapper.findComponent(DropdownContentsCreateView);
const findLabelsView = () => wrapper.findComponent(DropdownContentsLabelsView);
const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub);
@@ -93,7 +92,7 @@ describe('DropdownContent', () => {
});
it('emits `setLabels` event on dropdown hide if labels changed on non-sidebar widget', async () => {
- createComponent({ props: { variant: DropdownVariant.Standalone } });
+ createComponent({ props: { variant: VARIANT_STANDALONE } });
const updatedLabel = {
id: 28,
title: 'Bug',
@@ -109,7 +108,7 @@ describe('DropdownContent', () => {
});
it('emits `setLabels` event on visibility change if labels changed on sidebar widget', async () => {
- createComponent({ props: { variant: DropdownVariant.Standalone, isVisible: true } });
+ createComponent({ props: { variant: VARIANT_STANDALONE, isVisible: true } });
const updatedLabel = {
id: 28,
title: 'Bug',
@@ -177,6 +176,21 @@ describe('DropdownContent', () => {
expect(findCreateView().exists()).toBe(false);
expect(findLabelsView().exists()).toBe(true);
});
+
+ it('selects created labels', async () => {
+ const createdLabel = {
+ id: 29,
+ title: 'new label',
+ description: null,
+ color: '#FF0000',
+ textColor: '#FFFFFF',
+ };
+
+ findCreateView().vm.$emit('labelCreated', createdLabel);
+ await nextTick();
+
+ expect(findLabelsView().props('localSelectedLabels')).toContain(createdLabel);
+ });
});
describe('Labels view', () => {
@@ -193,13 +207,13 @@ describe('DropdownContent', () => {
});
it('does not render footer on standalone dropdown', () => {
- createComponent({ props: { variant: DropdownVariant.Standalone } });
+ createComponent({ props: { variant: VARIANT_STANDALONE } });
expect(findDropdownFooter().exists()).toBe(false);
});
it('renders footer on embedded dropdown', () => {
- createComponent({ props: { variant: DropdownVariant.Embedded } });
+ createComponent({ props: { variant: VARIANT_EMBEDDED } });
expect(findDropdownFooter().exists()).toBe(true);
});
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 9a6e0ca3ccd..ad1edaa6671 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
@@ -20,10 +20,6 @@ describe('DropdownFooter', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findCreateLabelButton = () => wrapper.find('[data-testid="create-label-button"]');
describe('Labels view', () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
index d9001dface4..4861d2ca55e 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_header_spec.js
@@ -28,10 +28,6 @@ describe('DropdownHeader', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findGoBackButton = () => wrapper.findByTestId('go-back-button');
const findDropdownTitle = () => wrapper.findByTestId('dropdown-header-title');
@@ -49,7 +45,7 @@ describe('DropdownHeader', () => {
expect(findGoBackButton().exists()).toBe(true);
});
- it('does not render search input field', async () => {
+ it('does not render search input field', () => {
expect(findSearchInput().exists()).toBe(false);
});
});
@@ -85,7 +81,7 @@ describe('DropdownHeader', () => {
expect(findSearchInput().exists()).toBe(true);
});
- it('does not render title', async () => {
+ it('does not render title', () => {
expect(findDropdownTitle().exists()).toBe(false);
});
});
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
index 585048983c9..d70b989b493 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_value_spec.js
@@ -30,10 +30,6 @@ describe('DropdownValue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there are no labels', () => {
beforeEach(() => {
createComponent(
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
index 4fa65c752f9..715dd4e034e 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/embedded_labels_list_spec.js
@@ -30,10 +30,6 @@ describe('EmbeddedLabelsList', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there are no labels', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
index 74188a77994..377d1894411 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/label_item_spec.js
@@ -19,10 +19,6 @@ describe('LabelItem', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('renders label color element', () => {
const colorEl = wrapper.find('[data-testid="label-color-box"]');
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
index fd8e72bac49..b0a080ba1ef 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
@@ -3,8 +3,8 @@ 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 '~/flash';
-import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
+import { createAlert } from '~/alert';
+import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST, TYPE_TEST_CASE } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
@@ -25,7 +25,7 @@ import {
mockRegularLabel,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -36,9 +36,9 @@ const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a proble
const updateLabelsMutation = {
[TYPE_ISSUE]: updateIssueLabelsMutation,
- [IssuableType.MergeRequest]: updateMergeRequestLabelsMutation,
+ [TYPE_MERGE_REQUEST]: updateMergeRequestLabelsMutation,
[TYPE_EPIC]: updateEpicLabelsMutation,
- [IssuableType.TestCase]: updateTestCaseLabelsMutation,
+ [TYPE_TEST_CASE]: updateTestCaseLabelsMutation,
};
describe('LabelsSelectRoot', () => {
@@ -83,10 +83,6 @@ describe('LabelsSelectRoot', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders component with classes `labels-select-wrapper gl-relative`', () => {
createComponent();
expect(wrapper.classes()).toEqual(['labels-select-wrapper', 'gl-relative']);
@@ -150,7 +146,7 @@ describe('LabelsSelectRoot', () => {
});
});
- it('creates flash with error message when query is rejected', async () => {
+ it('creates alert with error message when query is rejected', async () => {
createComponent({ queryHandler: errorQueryHandler });
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message: 'Error fetching labels.' });
@@ -203,7 +199,7 @@ describe('LabelsSelectRoot', () => {
});
});
- it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', async () => {
+ it('emits `updateSelectedLabels` event on dropdown contents `setLabels` event if iid is not set', () => {
const label = { id: 'gid://gitlab/ProjectLabel/1' };
createComponent({ config: { ...mockConfig, iid: undefined } });
@@ -214,9 +210,9 @@ describe('LabelsSelectRoot', () => {
describe.each`
issuableType
${TYPE_ISSUE}
- ${IssuableType.MergeRequest}
+ ${TYPE_MERGE_REQUEST}
${TYPE_EPIC}
- ${IssuableType.TestCase}
+ ${TYPE_TEST_CASE}
`('when updating labels for $issuableType', ({ issuableType }) => {
const label = { id: 'gid://gitlab/ProjectLabel/2' };
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
index 5d5a7e9a200..b0b473625bb 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
@@ -117,6 +117,17 @@ export const workspaceLabelsQueryResponse = {
},
};
+export const workspaceLabelsQueryEmptyResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/126',
+ labels: {
+ nodes: [],
+ },
+ },
+ },
+};
+
export const issuableLabelsQueryResponse = {
data: {
workspace: {
diff --git a/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
index 2abb0c24d7d..2c256a67bb0 100644
--- a/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/components/lock/edit_form_buttons_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { createStore as createMrStore } from '~/mr_notes/stores';
import createStore from '~/notes/stores';
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
@@ -8,7 +8,7 @@ import eventHub from '~/sidebar/event_hub';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('EditFormButtons', () => {
let wrapper;
@@ -51,11 +51,6 @@ describe('EditFormButtons', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
@@ -74,7 +69,7 @@ describe('EditFormButtons', () => {
});
it('disables the toggle button', () => {
- expect(findLockToggle().attributes('disabled')).toBe('disabled');
+ expect(findLockToggle().attributes('disabled')).toBeDefined();
});
it('sets loading on the toggle button', () => {
@@ -128,7 +123,7 @@ describe('EditFormButtons', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
- it('does not flash an error message', () => {
+ it('does not alert an error message', () => {
expect(createAlert).not.toHaveBeenCalled();
});
});
@@ -161,7 +156,7 @@ describe('EditFormButtons', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
- it('calls flash with the correct message', () => {
+ it('calls alert with the correct message', () => {
expect(createAlert).toHaveBeenCalledWith({
message: `Something went wrong trying to change the locked state of this ${issuableDisplayName}`,
});
diff --git a/spec/frontend/sidebar/components/lock/edit_form_spec.js b/spec/frontend/sidebar/components/lock/edit_form_spec.js
index 4ae9025ee39..06cce7bd7ca 100644
--- a/spec/frontend/sidebar/components/lock/edit_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/edit_form_spec.js
@@ -24,11 +24,6 @@ describe('Edit Form Dropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index 8f825847cfc..5e766e9a41c 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -29,6 +29,7 @@ describe('IssuableLockForm', () => {
const findEditForm = () => wrapper.findComponent(EditForm);
const findSidebarLockStatusTooltip = () =>
getBinding(findSidebarCollapseIcon().element, 'gl-tooltip');
+ const findIssuableLockClickable = () => wrapper.find('[data-testid="issuable-lock"]');
const initStore = (isLocked) => {
if (issuableType === ISSUABLE_TYPE_ISSUE) {
@@ -48,7 +49,7 @@ describe('IssuableLockForm', () => {
store.getters.getNoteableData.discussion_locked = isLocked;
};
- const createComponent = ({ props = {} }, movedMrSidebar = false) => {
+ const createComponent = ({ props = {}, movedMrSidebar = false }) => {
wrapper = shallowMount(IssuableLockForm, {
store,
provide: {
@@ -62,16 +63,11 @@ describe('IssuableLockForm', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
@@ -174,11 +170,27 @@ describe('IssuableLockForm', () => {
`('displays $message when merge request is $locked', async ({ locked, message }) => {
initStore(locked);
- createComponent({}, true);
+ createComponent({ movedMrSidebar: true });
await wrapper.find('.dropdown-item').trigger('click');
expect(toast).toHaveBeenCalledWith(message);
});
});
+
+ describe('moved_mr_sidebar flag', () => {
+ describe('when the flag is off', () => {
+ it('does not show the non editable lock status', () => {
+ createComponent({ movedMrSidebar: false });
+ expect(findIssuableLockClickable().exists()).toBe(false);
+ });
+ });
+
+ describe('when the flag is on', () => {
+ it('does not show the non editable lock status', () => {
+ createComponent({ movedMrSidebar: true });
+ expect(findIssuableLockClickable().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
index b492753867b..8a0db1715f3 100644
--- a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_ISSUE, WorkspaceType } from '~/issues/constants';
+import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
import { __ } from '~/locale';
import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
@@ -12,7 +12,7 @@ describe('MilestoneDropdown component', () => {
const propsData = {
attrWorkspacePath: 'full/path',
issuableType: TYPE_ISSUE,
- workspaceType: WorkspaceType.project,
+ workspaceType: WORKSPACE_PROJECT,
};
const findHiddenInput = () => wrapper.find('input');
diff --git a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
index 72279f44e80..56c915c4cae 100644
--- a/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/move/issuable_move_dropdown_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import {
GlIcon,
GlLoadingIcon,
@@ -7,12 +8,13 @@ import {
GlSearchBoxByType,
GlButton,
} from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-
-import { nextTick } from 'vue';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
const mockProjects = [
{
@@ -45,60 +47,82 @@ const mockEvent = {
preventDefault: jest.fn(),
};
+const focusInputMock = jest.fn();
+const hideMock = jest.fn();
+
describe('IssuableMoveDropdown', () => {
let mock;
let wrapper;
const createComponent = (propsData = mockProps) => {
- wrapper = shallowMount(IssuableMoveDropdown, {
+ wrapper = shallowMountExtended(IssuableMoveDropdown, {
propsData,
+ stubs: {
+ GlDropdown: stubComponent(GlDropdown, {
+ methods: {
+ hide: hideMock,
+ },
+ }),
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ methods: {
+ focusInput: focusInputMock,
+ },
+ }),
+ },
});
- wrapper.vm.$refs.dropdown.hide = jest.fn();
- wrapper.vm.$refs.searchInput.focusInput = jest.fn();
};
beforeEach(() => {
mock = new MockAdapter(axios);
+ mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, mockProjects);
+
createComponent();
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
+ const findCollapsedEl = () => wrapper.findByTestId('move-collapsed');
+ const findFooter = () => wrapper.findByTestId('footer');
+ const findHeader = () => wrapper.findByTestId('header');
+ const findFailedLoadResults = () => wrapper.findByTestId('failed-load-results');
+ const findDropdownContent = () => wrapper.findByTestId('content');
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findDropdownEl = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+
describe('watch', () => {
describe('searchKey', () => {
it('calls `fetchProjects` with value of the prop', async () => {
- jest.spyOn(wrapper.vm, 'fetchProjects');
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchKey: 'foo',
- });
+ jest.spyOn(axios, 'get');
+ findSearchBox().vm.$emit('input', 'foo');
- await nextTick();
+ await waitForPromises();
- expect(wrapper.vm.fetchProjects).toHaveBeenCalledWith('foo');
+ expect(axios.get).toHaveBeenCalledWith('/-/autocomplete/projects?project_id=1', {
+ params: { search: 'foo' },
+ });
});
});
});
describe('methods', () => {
describe('fetchProjects', () => {
- it('sets projectsListLoading to true and projectsListLoadFailed to false', () => {
- wrapper.vm.fetchProjects();
+ it('sets projectsListLoading to true and projectsListLoadFailed to false', async () => {
+ findDropdownEl().vm.$emit('shown');
+ await nextTick();
- expect(wrapper.vm.projectsListLoading).toBe(true);
- expect(wrapper.vm.projectsListLoadFailed).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findFailedLoadResults().exists()).toBe(false);
});
- it('calls `axios.get` with `projectsFetchPath` and query param `search`', () => {
- jest.spyOn(axios, 'get').mockResolvedValue({
- data: mockProjects,
- });
+ it('calls `axios.get` with `projectsFetchPath` and query param `search`', async () => {
+ jest.spyOn(axios, 'get');
- wrapper.vm.fetchProjects('foo');
+ findSearchBox().vm.$emit('input', 'foo');
+ await waitForPromises();
expect(axios.get).toHaveBeenCalledWith(
mockProps.projectsFetchPath,
@@ -111,74 +135,65 @@ describe('IssuableMoveDropdown', () => {
});
it('sets response to `projects` and focuses on searchInput when request is successful', async () => {
- jest.spyOn(axios, 'get').mockResolvedValue({
- data: mockProjects,
- });
+ jest.spyOn(axios, 'get');
- await wrapper.vm.fetchProjects('foo');
+ findSearchBox().vm.$emit('input', 'foo');
+ await waitForPromises();
- expect(wrapper.vm.projects).toBe(mockProjects);
- expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
+ expect(findAllDropdownItems()).toHaveLength(mockProjects.length);
+ expect(focusInputMock).toHaveBeenCalled();
});
it('sets projectsListLoadFailed to true when request fails', async () => {
jest.spyOn(axios, 'get').mockRejectedValue({});
- await wrapper.vm.fetchProjects('foo');
+ findSearchBox().vm.$emit('input', 'foo');
+ await waitForPromises();
- expect(wrapper.vm.projectsListLoadFailed).toBe(true);
+ expect(findFailedLoadResults().exists()).toBe(true);
});
it('sets projectsListLoading to false when request completes', async () => {
- jest.spyOn(axios, 'get').mockResolvedValue({
- data: mockProjects,
- });
+ jest.spyOn(axios, 'get');
- await wrapper.vm.fetchProjects('foo');
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
- expect(wrapper.vm.projectsListLoading).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('isSelectedProject', () => {
it.each`
- project | selectedProject | title | returnValue
- ${mockProjects[0]} | ${mockProjects[0]} | ${'are same projects'} | ${true}
- ${mockProjects[0]} | ${mockProjects[1]} | ${'are different projects'} | ${false}
+ projectIndex | selectedProjectIndex | title | returnValue
+ ${0} | ${0} | ${'are same projects'} | ${true}
+ ${0} | ${1} | ${'are different projects'} | ${false}
`(
'returns $returnValue when selectedProject and provided project param $title',
- async ({ project, selectedProject, returnValue }) => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedProject,
- });
+ async ({ projectIndex, selectedProjectIndex, returnValue }) => {
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
+
+ findAllDropdownItems().at(selectedProjectIndex).vm.$emit('click', mockEvent);
await nextTick();
- expect(wrapper.vm.isSelectedProject(project)).toBe(returnValue);
+ expect(findAllDropdownItems().at(projectIndex).props('isChecked')).toBe(returnValue);
},
);
it('returns false when selectedProject is null', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedProject: null,
- });
-
- await nextTick();
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
- expect(wrapper.vm.isSelectedProject(mockProjects[0])).toBe(false);
+ expect(findAllDropdownItems().at(0).props('isChecked')).toBe(false);
});
});
});
describe('template', () => {
- const findDropdownEl = () => wrapper.findComponent(GlDropdown);
-
it('renders collapsed state element with icon', () => {
- const collapsedEl = wrapper.find('[data-testid="move-collapsed"]');
+ const collapsedEl = findCollapsedEl();
expect(collapsedEl.exists()).toBe(true);
expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle);
@@ -198,12 +213,11 @@ describe('IssuableMoveDropdown', () => {
it('renders disabled dropdown when `disabled` is true', () => {
createComponent({ ...mockProps, disabled: true });
-
- expect(findDropdownEl().attributes('disabled')).toBe('true');
+ expect(findDropdownEl().props('disabled')).toBe(true);
});
it('renders header element', () => {
- const headerEl = findDropdownEl().find('[data-testid="header"]');
+ const headerEl = findHeader();
expect(headerEl.exists()).toBe(true);
expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle);
@@ -221,108 +235,71 @@ describe('IssuableMoveDropdown', () => {
});
it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- projectsListLoading: true,
- });
-
+ findDropdownEl().vm.$emit('shown');
await nextTick();
- expect(findDropdownEl().findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
it('renders gl-dropdown-item components for available projects', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- projects: mockProjects,
- selectedProject: mockProjects[0],
- });
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
+ findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
await nextTick();
- const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
-
- expect(dropdownItems).toHaveLength(mockProjects.length);
- expect(dropdownItems.at(0).props()).toMatchObject({
+ expect(findAllDropdownItems()).toHaveLength(mockProjects.length);
+ expect(findAllDropdownItems().at(0).props()).toMatchObject({
isCheckItem: true,
isChecked: true,
});
- expect(dropdownItems.at(0).text()).toBe(mockProjects[0].name_with_namespace);
+ expect(findAllDropdownItems().at(0).text()).toBe(mockProjects[0].name_with_namespace);
});
it('renders string "No matching results" when search does not yield any matches', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchKey: 'foo',
- });
-
- // Wait for `searchKey` watcher to run.
- await nextTick();
+ mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- projects: [],
- projectsListLoading: false,
- });
-
- await nextTick();
+ findSearchBox().vm.$emit('input', 'foo');
+ await waitForPromises();
- const dropdownContentEl = wrapper.find('[data-testid="content"]');
-
- expect(dropdownContentEl.text()).toContain('No matching results');
+ expect(findDropdownContent().text()).toContain('No matching results');
});
it('renders string "Failed to load projects" when loading projects list fails', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- projects: [],
- projectsListLoading: false,
- projectsListLoadFailed: true,
- });
-
- await nextTick();
+ mock.onGet(mockProps.projectsFetchPath).reply(HTTP_STATUS_OK, []);
+ jest.spyOn(axios, 'get').mockRejectedValue({});
- const dropdownContentEl = wrapper.find('[data-testid="content"]');
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
- expect(dropdownContentEl.text()).toContain('Failed to load projects');
+ expect(findDropdownContent().text()).toContain('Failed to load projects');
});
it('renders gl-button within footer', async () => {
- const moveButtonEl = wrapper.find('[data-testid="footer"]').findComponent(GlButton);
+ const moveButtonEl = findFooter().findComponent(GlButton);
expect(moveButtonEl.text()).toBe('Move');
- expect(moveButtonEl.attributes('disabled')).toBe('true');
+ expect(moveButtonEl.attributes('disabled')).toBeDefined();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedProject: mockProjects[0],
- });
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
+ findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
await nextTick();
- expect(
- wrapper.find('[data-testid="footer"]').findComponent(GlButton).attributes('disabled'),
- ).not.toBeDefined();
+ expect(findFooter().findComponent(GlButton).attributes('disabled')).not.toBeDefined();
});
});
describe('events', () => {
it('collapsed state element emits `toggle-collapse` event on component when clicked', () => {
- wrapper.find('[data-testid="move-collapsed"]').trigger('click');
+ findCollapsedEl().trigger('click');
expect(wrapper.emitted('toggle-collapse')).toHaveLength(1);
});
it('gl-dropdown component calls `fetchProjects` on `shown` event', () => {
- jest.spyOn(axios, 'get').mockResolvedValue({
- data: mockProjects,
- });
+ jest.spyOn(axios, 'get');
findDropdownEl().vm.$emit('shown');
@@ -330,56 +307,50 @@ describe('IssuableMoveDropdown', () => {
});
it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- projectItemClick: true,
- });
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
+
+ findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
+ await nextTick();
findDropdownEl().vm.$emit('hide', mockEvent);
expect(mockEvent.preventDefault).toHaveBeenCalled();
- expect(wrapper.vm.projectItemClick).toBe(false);
});
- it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', async () => {
+ it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', () => {
findDropdownEl().vm.$emit('hide');
expect(wrapper.emitted('dropdown-close')).toHaveLength(1);
});
- it('close icon in dropdown header closes the dropdown when clicked', () => {
- wrapper.find('[data-testid="header"]').findComponent(GlButton).vm.$emit('click', mockEvent);
+ it('close icon in dropdown header closes the dropdown when clicked', async () => {
+ findHeader().findComponent(GlButton).vm.$emit('click', mockEvent);
- expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
+ await nextTick();
+ expect(hideMock).toHaveBeenCalled();
});
it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- projects: mockProjects,
- });
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
+ findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
await nextTick();
- wrapper.findAllComponents(GlDropdownItem).at(0).vm.$emit('click', mockEvent);
-
- expect(wrapper.vm.selectedProject).toBe(mockProjects[0]);
+ expect(findAllDropdownItems().at(0).props('isChecked')).toBe(true);
});
it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedProject: mockProjects[0],
- });
+ findDropdownEl().vm.$emit('shown');
+ await waitForPromises();
+ findAllDropdownItems().at(0).vm.$emit('click', mockEvent);
await nextTick();
- wrapper.find('[data-testid="footer"]').findComponent(GlButton).vm.$emit('click');
+ findFooter().findComponent(GlButton).vm.$emit('click');
- expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
+ expect(hideMock).toHaveBeenCalled();
expect(wrapper.emitted('move-issuable')).toHaveLength(1);
expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]);
});
diff --git a/spec/frontend/sidebar/components/move/move_issue_button_spec.js b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
index acd6b23c1f5..e2f5414056a 100644
--- a/spec/frontend/sidebar/components/move/move_issue_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
@@ -4,14 +4,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { visitUrl } from '~/lib/utils/url_utility';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import ProjectSelect from '~/sidebar/components/move/issuable_move_dropdown.vue';
import MoveIssueButton from '~/sidebar/components/move/move_issue_button.vue';
import moveIssueMutation from '~/sidebar/queries/move_issue.mutation.graphql';
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
}));
@@ -71,10 +71,6 @@ describe('MoveIssueButton', () => {
});
};
- afterEach(() => {
- fakeApollo = null;
- });
-
it('renders the project select dropdown', () => {
createComponent();
@@ -118,7 +114,7 @@ describe('MoveIssueButton', () => {
expect(findProjectSelect().props('moveInProgress')).toBe(false);
});
- it('creates a flash and logs errors when a mutation returns errors', async () => {
+ it('creates an alert and logs errors when a mutation returns errors', async () => {
createComponent(resolvedMutationWithErrorsMock);
emitProjectSelectEvent();
diff --git a/spec/frontend/sidebar/components/move/move_issues_button_spec.js b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
index c65bad642a0..83b32d04fcf 100644
--- a/spec/frontend/sidebar/components/move/move_issues_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
@@ -6,7 +6,7 @@ import { GlAlert } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { logError } from '~/lib/logger';
import IssuableMoveDropdown from '~/sidebar/components/move/issuable_move_dropdown.vue';
import issuableEventHub from '~/issues/list/eventhub';
@@ -22,7 +22,7 @@ import {
WORK_ITEM_TYPE_ENUM_TEST_CASE,
} from '~/work_items/constants';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/logger');
useMockLocationHelper();
@@ -159,7 +159,6 @@ describe('MoveIssuesButton', () => {
});
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -167,7 +166,7 @@ describe('MoveIssuesButton', () => {
it('renders disabled by default', () => {
createComponent();
expect(findDropdown().exists()).toBe(true);
- expect(findDropdown().attributes('disabled')).toBe('true');
+ expect(findDropdown().attributes('disabled')).toBeDefined();
});
it.each`
@@ -186,7 +185,7 @@ describe('MoveIssuesButton', () => {
await nextTick();
if (disabled) {
- expect(findDropdown().attributes('disabled')).toBe('true');
+ expect(findDropdown().attributes('disabled')).toBeDefined();
} else {
expect(findDropdown().attributes('disabled')).toBeUndefined();
}
@@ -347,7 +346,7 @@ describe('MoveIssuesButton', () => {
expect(issuableEventHub.$emit).not.toHaveBeenCalled();
});
- it('emits `issuables:bulkMoveStarted` when issues are moving', async () => {
+ it('emits `issuables:bulkMoveStarted` when issues are moving', () => {
createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
emitMoveIssuablesEvent();
@@ -389,7 +388,7 @@ describe('MoveIssuesButton', () => {
});
describe('shows errors', () => {
- it('does not create flashes or logs errors when no issue is selected', async () => {
+ it('does not create alerts or logs errors when no issue is selected', async () => {
createComponent();
emitMoveIssuablesEvent();
@@ -399,7 +398,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('does not create flashes or logs errors when only tasks are selected', async () => {
+ it('does not create alerts or logs errors when only tasks are selected', async () => {
createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly });
emitMoveIssuablesEvent();
@@ -409,7 +408,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('does not create flashes or logs errors when only test cases are selected', async () => {
+ it('does not create alerts or logs errors when only test cases are selected', async () => {
createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly });
emitMoveIssuablesEvent();
@@ -419,7 +418,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('does not create flashes or logs errors when only tasks and test cases are selected', async () => {
+ it('does not create alerts or logs errors when only tasks and test cases are selected', async () => {
createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases });
emitMoveIssuablesEvent();
@@ -429,7 +428,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('does not create flashes or logs errors when issues are moved without errors', async () => {
+ it('does not create alerts or logs errors when issues are moved without errors', async () => {
createComponent(
{ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
resolvedMutationWithoutErrorsMock,
@@ -442,7 +441,7 @@ describe('MoveIssuesButton', () => {
expect(createAlert).not.toHaveBeenCalled();
});
- it('creates a flash and logs errors when a mutation returns errors', async () => {
+ it('creates an alert and logs errors when a mutation returns errors', async () => {
createComponent(
{ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases },
resolvedMutationWithErrorsMock,
@@ -462,14 +461,14 @@ describe('MoveIssuesButton', () => {
`Error moving issue. Error message: ${mockMutationErrorMessage}`,
);
- // Only one flash is created even if multiple errors are reported
+ // Only one alert is created even if multiple errors are reported
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
message: 'There was an error while moving the issues.',
});
});
- it('creates a flash but not logs errors when a mutation is rejected', async () => {
+ it('creates an alert but not logs errors when a mutation is rejected', async () => {
createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases });
emitMoveIssuablesEvent();
diff --git a/spec/frontend/sidebar/components/participants/participants_spec.js b/spec/frontend/sidebar/components/participants/participants_spec.js
index f7a626a189c..72d83ebeca4 100644
--- a/spec/frontend/sidebar/components/participants/participants_spec.js
+++ b/spec/frontend/sidebar/components/participants/participants_spec.js
@@ -1,203 +1,114 @@
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import Participants from '~/sidebar/components/participants/participants.vue';
-const PARTICIPANT = {
- id: 1,
- state: 'active',
- username: 'marcene',
- name: 'Allie Will',
- web_url: 'foo.com',
- avatar_url: 'gravatar.com/avatar/xxx',
-};
-
-const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
-
-describe('Participants', () => {
+describe('Participants component', () => {
let wrapper;
- const getMoreParticipantsButton = () => wrapper.find('[data-testid="more-participants"]');
- const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
+ const participant = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+ };
- const mountComponent = (propsData) =>
- shallowMount(Participants, {
- propsData,
- });
+ const participants = [participant, { ...participant, id: 2 }, { ...participant, id: 3 }];
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findMoreParticipantsButton = () => wrapper.findComponent(GlButton);
+ const findCollapsedIcon = () => wrapper.find('.sidebar-collapsed-icon');
+ const findParticipantsAuthor = () => wrapper.findAll('.participants-author');
+
+ const mountComponent = (propsData) => shallowMount(Participants, { propsData });
describe('collapsed sidebar state', () => {
it('shows loading spinner when loading', () => {
- wrapper = mountComponent({
- loading: true,
- });
+ wrapper = mountComponent({ loading: true });
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('does not show loading spinner not loading', () => {
- wrapper = mountComponent({
- loading: false,
- });
+ it('does not show loading spinner when not loading', () => {
+ wrapper = mountComponent({ loading: false });
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('shows participant count when given', () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- });
+ wrapper = mountComponent({ participants });
- expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ expect(findCollapsedIcon().text()).toBe(participants.length.toString());
});
it('shows full participant count when there are hidden participants', () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 1,
- });
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 1 });
- expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ expect(findCollapsedIcon().text()).toBe(participants.length.toString());
});
});
describe('expanded sidebar state', () => {
it('shows loading spinner when loading', () => {
- wrapper = mountComponent({
- loading: true,
- });
+ wrapper = mountComponent({ loading: true });
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('when only showing visible participants, shows an avatar only for each participant under the limit', async () => {
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
const numberOfLessParticipants = 2;
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isShowingMoreParticipants: false,
- });
-
- await nextTick();
- expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
+ wrapper = mountComponent({ participants, numberOfLessParticipants });
+
+ expect(findParticipantsAuthor()).toHaveLength(numberOfLessParticipants);
});
it('when only showing all participants, each has an avatar', async () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isShowingMoreParticipants: true,
- });
-
- await nextTick();
- expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
+
+ await findMoreParticipantsButton().vm.$emit('click');
+
+ expect(findParticipantsAuthor()).toHaveLength(participants.length);
});
it('does not have more participants link when they can all be shown', () => {
const numberOfLessParticipants = 100;
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
-
- expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
- expect(getMoreParticipantsButton().exists()).toBe(false);
- });
+ wrapper = mountComponent({ participants, numberOfLessParticipants });
- it('when too many participants, has more participants link to show more', async () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isShowingMoreParticipants: false,
- });
-
- await nextTick();
- expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
+ expect(participants.length).toBeLessThan(numberOfLessParticipants);
+ expect(findMoreParticipantsButton().exists()).toBe(false);
});
- it('when too many participants and already showing them, has more participants link to show less', async () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- isShowingMoreParticipants: true,
- });
-
- await nextTick();
- expect(getMoreParticipantsButton().text()).toBe('- show less');
- });
+ it('when too many participants, has more participants link to show more', () => {
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
- it('clicking more participants link emits event', () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
+ expect(findMoreParticipantsButton().text()).toBe('+ 1 more');
+ });
- expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
+ it('when too many participants and already showing them, has more participants link to show less', async () => {
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
- getMoreParticipantsButton().vm.$emit('click');
+ await findMoreParticipantsButton().vm.$emit('click');
- expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
+ expect(findMoreParticipantsButton().text()).toBe('- show less');
});
- it('clicking on participants icon emits `toggleSidebar` event', async () => {
- wrapper = mountComponent({
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
-
- const spy = jest.spyOn(wrapper.vm, '$emit');
+ it('clicking on participants icon emits `toggleSidebar` event', () => {
+ wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
- wrapper.find('.sidebar-collapsed-icon').trigger('click');
+ findCollapsedIcon().trigger('click');
- await nextTick();
- expect(spy).toHaveBeenCalledWith('toggleSidebar');
- spy.mockRestore();
+ expect(wrapper.emitted('toggleSidebar')).toEqual([[]]);
});
});
describe('when not showing participants label', () => {
beforeEach(() => {
- wrapper = mountComponent({
- participants: PARTICIPANT_LIST,
- showParticipantLabel: false,
- });
+ wrapper = mountComponent({ participants, showParticipantLabel: false });
});
it('does not show sidebar collapsed icon', () => {
- expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(false);
+ expect(findCollapsedIcon().exists()).toBe(false);
});
it('does not show participants label title', () => {
diff --git a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
index 859e63b3df6..914e848eced 100644
--- a/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
+++ b/spec/frontend/sidebar/components/participants/sidebar_participants_widget_spec.js
@@ -35,7 +35,6 @@ describe('Sidebar Participants Widget', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
diff --git a/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
index 68ecd62e4c6..0f595ab21a5 100644
--- a/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewer_title_spec.js
@@ -16,11 +16,6 @@ describe('ReviewerTitle component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('reviewer title', () => {
it('renders reviewer', () => {
wrapper = createComponent({
diff --git a/spec/frontend/sidebar/components/reviewers/reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
index 229f7ffbe04..016ec9225da 100644
--- a/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/reviewers_spec.js
@@ -35,10 +35,6 @@ describe('Reviewer component', () => {
const findCollapsedChildren = () => wrapper.findAll('.sidebar-collapsed-icon > *');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('No reviewers/users', () => {
it('displays no reviewer icon when collapsed', () => {
createWrapper();
diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
index 57ae146a27a..a221d28704b 100644
--- a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js
@@ -44,9 +44,6 @@ describe('sidebar reviewers', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
index d00c8dcb653..66bc1f393ae 100644
--- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -1,9 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
-const userDataMock = () => ({
+const userDataMock = ({ approved = false } = {}) => ({
id: 1,
name: 'Root',
state: 'active',
@@ -14,14 +15,21 @@ const userDataMock = () => ({
canMerge: true,
canUpdate: true,
reviewed: true,
- approved: false,
+ approved,
},
});
describe('UncollapsedReviewerList component', () => {
let wrapper;
- const reviewerApprovalIcons = () => wrapper.findAll('[data-testid="re-approved"]');
+ const findAllRerequestButtons = () => wrapper.findAll('[data-testid="re-request-button"]');
+ const findAllReviewerApprovalIcons = () => wrapper.findAll('[data-testid="approved"]');
+ const findAllReviewedNotApprovedIcons = () =>
+ wrapper.findAll('[data-testid="reviewed-not-approved"]');
+ const findAllReviewerAvatarLinks = () => wrapper.findAllComponents(ReviewerAvatarLink);
+
+ const hasApprovalIconAnimation = () =>
+ findAllReviewerApprovalIcons().at(0).classes('merge-request-approved-icon');
function createComponent(props = {}, glFeatures = {}) {
const propsData = {
@@ -38,10 +46,6 @@ describe('UncollapsedReviewerList component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('single reviewer', () => {
const user = userDataMock();
@@ -52,27 +56,17 @@ describe('UncollapsedReviewerList component', () => {
});
it('only has one user', () => {
- expect(wrapper.findAllComponents(ReviewerAvatarLink).length).toBe(1);
+ expect(findAllReviewerAvatarLinks()).toHaveLength(1);
});
it('shows one user with avatar, and author name', () => {
- expect(wrapper.text()).toContain(user.name);
+ expect(wrapper.text()).toBe(user.name);
});
it('renders re-request loading icon', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ loadingStates: { 1: 'loading' } });
+ await findAllRerequestButtons().at(0).vm.$emit('click');
- expect(wrapper.find('[data-testid="re-request-button"]').props('loading')).toBe(true);
- });
-
- it('renders re-request success icon', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ loadingStates: { 1: 'success' } });
-
- expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
+ expect(findAllRerequestButtons().at(0).props('loading')).toBe(true);
});
});
@@ -88,51 +82,126 @@ describe('UncollapsedReviewerList component', () => {
approved: true,
},
};
+ const user3 = {
+ ...user,
+ id: 3,
+ name: 'lizabeth-wilderman',
+ username: 'lizabeth-wilderman',
+ mergeRequestInteraction: {
+ ...user.mergeRequestInteraction,
+ approved: false,
+ reviewed: true,
+ },
+ };
beforeEach(() => {
createComponent({
- users: [user, user2],
+ users: [user, user2, user3],
});
});
- it('has both users', () => {
- expect(wrapper.findAllComponents(ReviewerAvatarLink).length).toBe(2);
+ it('has three users', () => {
+ expect(findAllReviewerAvatarLinks()).toHaveLength(3);
});
- it('shows both users with avatar, and author name', () => {
+ it('shows all users with avatar, and author name', () => {
expect(wrapper.text()).toContain(user.name);
expect(wrapper.text()).toContain(user2.name);
+ expect(wrapper.text()).toContain(user3.name);
});
it('renders approval icon', () => {
- expect(reviewerApprovalIcons().length).toBe(1);
+ expect(findAllReviewerApprovalIcons()).toHaveLength(1);
});
it('shows that hello-world approved', () => {
- const icon = reviewerApprovalIcons().at(0);
+ const icon = findAllReviewerApprovalIcons().at(0);
- expect(icon.attributes('title')).toEqual('Approved by @hello-world');
+ expect(icon.attributes('title')).toBe('Approved by @hello-world');
+ });
+
+ it('shows that lizabeth-wilderman reviewed but did not approve', () => {
+ const icon = findAllReviewedNotApprovedIcons().at(1);
+
+ expect(icon.attributes('title')).toBe('Reviewed by @lizabeth-wilderman but not yet approved');
});
it('renders re-request loading icon', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ loadingStates: { 2: 'loading' } });
+ await findAllRerequestButtons().at(1).vm.$emit('click');
- expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(2);
- expect(wrapper.findAll('[data-testid="re-request-button"]').at(1).props('loading')).toBe(
- true,
- );
+ const allRerequestButtons = findAllRerequestButtons();
+
+ expect(allRerequestButtons).toHaveLength(3);
+ expect(allRerequestButtons.at(1).props('loading')).toBe(true);
+ });
+ });
+
+ describe('when updating reviewers list', () => {
+ it('does not animate icon on initial page load', () => {
+ const user = userDataMock({ approved: true });
+ createComponent({ users: [user] });
+
+ expect(hasApprovalIconAnimation()).toBe(false);
});
- it('renders re-request success icon', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ loadingStates: { 2: 'success' } });
+ it('does not animate icon when adding a new reviewer', async () => {
+ const user = userDataMock({ approved: true });
+ const anotherUser = { ...user, id: 2 };
+ createComponent({ users: [user] });
+
+ await wrapper.setProps({ users: [user, anotherUser] });
+
+ expect(
+ findAllReviewerApprovalIcons().wrappers.every((w) =>
+ w.classes('merge-request-approved-icon'),
+ ),
+ ).toBe(false);
+ });
+
+ it('removes animation CSS class after 1500ms', async () => {
+ const previousUserState = userDataMock({ approved: false });
+ const currentUserState = userDataMock({ approved: true });
+
+ createComponent({
+ users: [previousUserState],
+ });
+
+ await wrapper.setProps({
+ users: [currentUserState],
+ });
+
+ expect(hasApprovalIconAnimation()).toBe(true);
+
+ jest.advanceTimersByTime(1500);
+ await nextTick();
- expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(1);
- expect(wrapper.findAll('[data-testid="re-request-success"]').length).toBe(1);
- expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
+ expect(findAllReviewerApprovalIcons().at(0).classes('merge-request-approved-icon')).toBe(
+ false,
+ );
+ });
+
+ describe('when reviewer was present in the list', () => {
+ it.each`
+ previousApprovalState | currentApprovalState | shouldAnimate
+ ${false} | ${true} | ${true}
+ ${true} | ${true} | ${false}
+ `(
+ 'when approval state changes from $previousApprovalState to $currentApprovalState',
+ async ({ previousApprovalState, currentApprovalState, shouldAnimate }) => {
+ const previousUserState = userDataMock({ approved: previousApprovalState });
+ const currentUserState = userDataMock({ approved: currentApprovalState });
+
+ createComponent({
+ users: [previousUserState],
+ });
+
+ await wrapper.setProps({
+ users: [currentUserState],
+ });
+
+ expect(hasApprovalIconAnimation()).toBe(shouldAnimate);
+ },
+ );
});
});
});
diff --git a/spec/frontend/sidebar/components/severity/severity_spec.js b/spec/frontend/sidebar/components/severity/severity_spec.js
index 99d33e840d5..939d86917bb 100644
--- a/spec/frontend/sidebar/components/severity/severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/severity_spec.js
@@ -14,13 +14,6 @@ describe('SeverityToken', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findIcon = () => wrapper.findComponent(GlIcon);
it('renders severity token for each severity type', () => {
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
index 71c6c259c32..bee90d2b2b6 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
@@ -1,131 +1,132 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/constants';
+import { createAlert } from '~/alert';
+import { TYPE_INCIDENT } from '~/issues/constants';
+import { INCIDENT_SEVERITY } from '~/sidebar/constants';
import updateIssuableSeverity from '~/sidebar/queries/update_issuable_severity.mutation.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severity_widget.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
-describe('SidebarSeverity', () => {
+Vue.use(VueApollo);
+
+describe('SidebarSeverityWidget', () => {
let wrapper;
- let mutate;
+ let mockApollo;
const projectPath = 'gitlab-org/gitlab-test';
const iid = '1';
const severity = 'CRITICAL';
- let canUpdate = true;
- function createComponent(props = {}) {
+ function createComponent({ props, canUpdate = true, mutationMock } = {}) {
+ mockApollo = createMockApollo([[updateIssuableSeverity, mutationMock]]);
+
const propsData = {
projectPath,
iid,
- issuableType: ISSUABLE_TYPES.INCIDENT,
+ issuableType: TYPE_INCIDENT,
initialSeverity: severity,
...props,
};
- mutate = jest.fn();
+
wrapper = mountExtended(SidebarSeverityWidget, {
propsData,
provide: {
canUpdate,
},
- mocks: {
- $apollo: {
- mutate,
- },
- },
+ apolloProvider: mockApollo,
stubs: {
GlSprintf,
},
});
}
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
- wrapper.destroy();
+ mockApollo = null;
});
const findSeverityToken = () => wrapper.findAllComponents(SeverityToken);
const findEditBtn = () => wrapper.findByTestId('edit-button');
const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem); // First dropdown item is critical severity
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findCollapsedSeverity = () => wrapper.findComponent({ ref: 'severity' });
describe('Severity widget', () => {
it('renders severity dropdown and token', () => {
+ createComponent();
+
expect(findSeverityToken().exists()).toBe(true);
expect(findDropdown().exists()).toBe(true);
});
describe('edit button', () => {
it('is rendered when `canUpdate` provided as `true`', () => {
+ createComponent();
+
expect(findEditBtn().exists()).toBe(true);
});
it('is NOT rendered when `canUpdate` provided as `false`', () => {
- canUpdate = false;
- createComponent();
+ createComponent({ canUpdate: false });
+
expect(findEditBtn().exists()).toBe(false);
});
});
});
describe('Update severity', () => {
- it('calls `$apollo.mutate` with `updateIssuableSeverity`', () => {
- jest
- .spyOn(wrapper.vm.$apollo, 'mutate')
- .mockResolvedValueOnce({ data: { issueSetSeverity: { issue: { severity } } } });
+ it('calls mutate with `updateIssuableSeverity`', () => {
+ const mutationMock = jest.fn().mockResolvedValue({
+ data: { issueSetSeverity: { issue: { severity } } },
+ });
+ createComponent({ mutationMock });
findCriticalSeverityDropdownItem().vm.$emit('click');
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: updateIssuableSeverity,
- variables: {
- iid,
- projectPath,
- severity,
- },
+
+ expect(mutationMock).toHaveBeenCalledWith({
+ iid,
+ projectPath,
+ severity,
});
});
it('shows error alert when severity update fails', async () => {
- const errorMsg = 'Something went wrong';
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValueOnce(errorMsg);
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ const mutationMock = jest.fn().mockRejectedValue('Something went wrong');
+ createComponent({ mutationMock });
+ findCriticalSeverityDropdownItem().vm.$emit('click');
await waitForPromises();
expect(createAlert).toHaveBeenCalled();
});
it('shows loading icon while updating', async () => {
- let resolvePromise;
- wrapper.vm.$apollo.mutate = jest.fn(
- () =>
- new Promise((resolve) => {
- resolvePromise = resolve;
- }),
- );
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ const mutationMock = jest.fn().mockRejectedValue({});
+ createComponent({ mutationMock });
+ findCriticalSeverityDropdownItem().vm.$emit('click');
await nextTick();
+
expect(findLoadingIcon().exists()).toBe(true);
- resolvePromise();
await waitForPromises();
+
expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('Switch between collapsed/expanded view of the sidebar', () => {
describe('collapsed', () => {
+ beforeEach(() => {
+ createComponent({ canUpdate: false });
+ });
+
it('should have collapsed icon class', () => {
expect(findCollapsedSeverity().classes('sidebar-collapsed-icon')).toBe(true);
});
@@ -139,17 +140,19 @@ describe('SidebarSeverity', () => {
describe('expanded', () => {
it('toggles dropdown with edit button', async () => {
- canUpdate = true;
createComponent();
await nextTick();
+
expect(findDropdown().isVisible()).toBe(false);
findEditBtn().vm.$emit('click');
await nextTick();
+
expect(findDropdown().isVisible()).toBe(true);
findEditBtn().vm.$emit('click');
await nextTick();
+
expect(findDropdown().isVisible()).toBe(false);
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
index 9f3d689edee..7a0044c00ac 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
@@ -10,7 +10,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
import { IssuableAttributeType } from '~/sidebar/constants';
@@ -23,7 +23,7 @@ import {
noCurrentMilestoneResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('SidebarDropdown component', () => {
let wrapper;
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index 060a2873e04..27ab347775a 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import { GlLink, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
@@ -27,7 +27,7 @@ import {
mockMilestone2,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('SidebarDropdownWidget', () => {
let wrapper;
@@ -133,43 +133,34 @@ describe('SidebarDropdownWidget', () => {
$apollo: {
mutate: mutationPromise(),
queries: {
- currentAttribute: { loading: false },
+ issuable: { loading: false },
attributesList: { loading: false },
...queries,
},
},
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
stubs: {
SidebarEditableItem,
GlSearchBoxByType,
- GlDropdown,
},
}),
);
wrapper.vm.$refs.dropdown.show = jest.fn();
-
- // We need to mock out `showDropdown` which
- // invokes `show` method of BDropdown used inside GlDropdown.
- jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when not editing', () => {
beforeEach(() => {
createComponent({
data: {
- currentAttribute: { id: 'id', title: 'title', webUrl: 'webUrl', dueDate: '2021-09-09' },
+ issuable: {
+ attribute: { id: 'id', title: 'title', webUrl: 'webUrl', dueDate: '2021-09-09' },
+ },
},
stubs: {
- GlDropdown,
SidebarEditableItem,
},
});
@@ -190,7 +181,7 @@ describe('SidebarDropdownWidget', () => {
it('shows a loading spinner while fetching the current attribute', () => {
createComponent({
queries: {
- currentAttribute: { loading: true },
+ issuable: { loading: true },
},
});
@@ -204,7 +195,7 @@ describe('SidebarDropdownWidget', () => {
selectedTitle: 'Some milestone title',
},
queries: {
- currentAttribute: { loading: false },
+ issuable: { loading: false },
},
});
@@ -229,10 +220,10 @@ describe('SidebarDropdownWidget', () => {
createComponent({
data: {
hasCurrentAttribute: true,
- currentAttribute: null,
+ issuable: {},
},
queries: {
- currentAttribute: { loading: false },
+ issuable: { loading: false },
},
});
@@ -256,7 +247,9 @@ describe('SidebarDropdownWidget', () => {
{ id: '123', title: '123' },
{ id: 'id', title: 'title' },
],
- currentAttribute: { id: '123' },
+ issuable: {
+ attribute: { id: '123' },
+ },
},
mutationPromise: mutationResp,
});
diff --git a/spec/frontend/sidebar/components/status/status_dropdown_spec.js b/spec/frontend/sidebar/components/status/status_dropdown_spec.js
index 5a75299c3a4..229b51ea568 100644
--- a/spec/frontend/sidebar/components/status/status_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/status/status_dropdown_spec.js
@@ -14,10 +14,6 @@ describe('SubscriptionsDropdown component', () => {
wrapper = shallowMount(StatusDropdown);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with no value selected', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
index c94f9918243..7275557e7f2 100644
--- a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import SidebarSubscriptionWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
@@ -15,7 +15,7 @@ import {
mergeRequestSubscriptionMutationResponse,
} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/vue_shared/plugins/global_toast');
Vue.use(VueApollo);
@@ -62,7 +62,6 @@ describe('Sidebar Subscriptions Widget', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -138,7 +137,7 @@ describe('Sidebar Subscriptions Widget', () => {
});
});
- it('displays a flash message when query is rejected', async () => {
+ it('displays an alert message when query is rejected', async () => {
createComponent({
subscriptionsQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
diff --git a/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
index 3fb8214606c..eaf7bc13d20 100644
--- a/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_dropdown_spec.js
@@ -15,10 +15,6 @@ describe('SubscriptionsDropdown component', () => {
wrapper = shallowMount(SubscriptionsDropdown);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with no value selected', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
index 1a1aa370eef..b644b7a9421 100644
--- a/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
+++ b/spec/frontend/sidebar/components/subscriptions/subscriptions_spec.js
@@ -1,28 +1,24 @@
import { GlToggle } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import eventHub from '~/sidebar/event_hub';
describe('Subscriptions', () => {
let wrapper;
+ let trackingSpy;
const findToggleButton = () => wrapper.findComponent(GlToggle);
+ const findTooltip = () => wrapper.findComponent({ ref: 'tooltip' });
- const mountComponent = (propsData) =>
- extendedWrapper(
- shallowMount(Subscriptions, {
- propsData,
- }),
- );
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
+ const mountComponent = (propsData) => {
+ wrapper = shallowMountExtended(Subscriptions, {
+ propsData,
+ });
+ };
it('shows loading spinner when loading', () => {
- wrapper = mountComponent({
+ mountComponent({
loading: true,
subscribed: undefined,
});
@@ -31,7 +27,7 @@ describe('Subscriptions', () => {
});
it('is toggled "off" when currently not subscribed', () => {
- wrapper = mountComponent({
+ mountComponent({
subscribed: false,
});
@@ -39,7 +35,7 @@ describe('Subscriptions', () => {
});
it('is toggled "on" when currently subscribed', () => {
- wrapper = mountComponent({
+ mountComponent({
subscribed: true,
});
@@ -48,44 +44,38 @@ describe('Subscriptions', () => {
it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
const id = 42;
- wrapper = mountComponent({ subscribed: true, id });
+ mountComponent({ subscribed: true, id });
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const wrapperEmitSpy = jest.spyOn(wrapper.vm, '$emit');
- wrapper.vm.toggleSubscription();
+ findToggleButton().vm.$emit('change');
expect(eventHubSpy).toHaveBeenCalledWith('toggleSubscription', id);
- expect(wrapperEmitSpy).toHaveBeenCalledWith('toggleSubscription', id);
- eventHubSpy.mockRestore();
- wrapperEmitSpy.mockRestore();
+ expect(wrapper.emitted('toggleSubscription')).toEqual([[id]]);
});
it('tracks the event when toggled', () => {
- wrapper = mountComponent({ subscribed: true });
-
- const wrapperTrackSpy = jest.spyOn(wrapper.vm, 'track');
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+ mountComponent({ subscribed: true });
- wrapper.vm.toggleSubscription();
+ findToggleButton().vm.$emit('change');
- expect(wrapperTrackSpy).toHaveBeenCalledWith('toggle_button', {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'toggle_button', {
+ category: undefined,
+ label: 'right_sidebar',
property: 'notifications',
value: 0,
});
- wrapperTrackSpy.mockRestore();
});
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
- wrapper = mountComponent({ subscribed: true });
- const spy = jest.spyOn(wrapper.vm, '$emit');
+ mountComponent({ subscribed: true });
+ findTooltip().trigger('click');
- wrapper.vm.onClickCollapsedIcon();
-
- expect(spy).toHaveBeenCalledWith('toggleSidebar');
- spy.mockRestore();
+ expect(wrapper.emitted('toggleSidebar')).toHaveLength(1);
});
it('has visually hidden label', () => {
- wrapper = mountComponent();
+ mountComponent();
expect(findToggleButton().props()).toMatchObject({
label: 'Notifications',
@@ -97,7 +87,7 @@ describe('Subscriptions', () => {
const subscribeDisabledDescription = 'Notifications have been disabled';
beforeEach(() => {
- wrapper = mountComponent({
+ mountComponent({
subscribed: false,
projectEmailsDisabled: true,
subscribeDisabledDescription,
@@ -108,9 +98,7 @@ describe('Subscriptions', () => {
expect(wrapper.findByTestId('subscription-title').text()).toContain(
subscribeDisabledDescription,
);
- expect(wrapper.findComponent({ ref: 'tooltip' }).attributes('title')).toBe(
- subscribeDisabledDescription,
- );
+ expect(findTooltip().attributes('title')).toBe(subscribeDisabledDescription);
});
it('does not render the toggle button', () => {
diff --git a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
index 715f66d305a..a7c3867c359 100644
--- a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
@@ -47,8 +47,8 @@ describe('Create Timelog Form', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findDocsLink = () => wrapper.findByTestId('timetracking-docs-link');
const findSaveButton = () => findModal().props('actionPrimary');
- const findSaveButtonLoadingState = () => findSaveButton().attributes[0].loading;
- const findSaveButtonDisabledState = () => findSaveButton().attributes[0].disabled;
+ const findSaveButtonLoadingState = () => findSaveButton().attributes.loading;
+ const findSaveButtonDisabledState = () => findSaveButton().attributes.disabled;
const submitForm = () => findForm().trigger('submit');
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 0259aee48f0..713ae83cbf1 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -6,7 +6,7 @@ import VueApollo from 'vue-apollo';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Report from '~/sidebar/components/time_tracking/report.vue';
import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.graphql';
import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
@@ -17,7 +17,7 @@ import {
timelogToRemoveId,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Issuable Time Tracking Report', () => {
Vue.use(VueApollo);
@@ -51,7 +51,6 @@ describe('Issuable Time Tracking Report', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
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 45d8b5e4647..e23d24f9629 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -32,7 +32,7 @@ describe('Issuable Time Tracker', () => {
const mountComponent = ({ props = {}, issuableType = 'issue', loading = false } = {}) => {
return mount(TimeTracker, {
propsData: { ...defaultProps, ...props },
- directives: { GlTooltip: createMockDirective() },
+ directives: { GlTooltip: createMockDirective('gl-tooltip') },
stubs: {
transition: stubTransition(),
},
@@ -53,10 +53,6 @@ describe('Issuable Time Tracker', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Initialization', () => {
beforeEach(() => {
wrapper = mountComponent();
@@ -148,7 +144,7 @@ describe('Issuable Time Tracker', () => {
});
describe('Comparison pane when limitToHours is true', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent({
props: {
limitToHours: true,
diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
index 5bfe3b59eb3..39b480b295c 100644
--- a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
@@ -4,13 +4,13 @@ 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 '~/flash';
+import { createAlert } from '~/alert';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import { todosResponse, noTodosResponse } from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -41,7 +41,6 @@ describe('Sidebar Todo Widget', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -77,7 +76,7 @@ describe('Sidebar Todo Widget', () => {
});
});
- it('displays a flash message when query is rejected', async () => {
+ it('displays an alert message when query is rejected', async () => {
createComponent({
todosQueryHandler: jest.fn().mockRejectedValue('Houston, we have a problem'),
});
diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
index fb07029a249..472a89e9b21 100644
--- a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
@@ -22,7 +22,6 @@ describe('Todo Button', () => {
});
afterEach(() => {
- wrapper.destroy();
dispatchEventSpy = null;
jest.clearAllMocks();
});
diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
index 8e6597bf80f..4da915f0dd3 100644
--- a/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_spec.js
@@ -21,10 +21,6 @@ describe('SidebarTodo', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
state | classes
${false} | ${['gl-button', 'btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']}
diff --git a/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
index cf9b2828dde..8e34a612705 100644
--- a/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
+++ b/spec/frontend/sidebar/components/toggle/toggle_sidebar_spec.js
@@ -17,10 +17,6 @@ describe('ToggleSidebar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findGlButton = () => wrapper.findComponent(GlButton);
it('should render the "chevron-double-lg-left" icon when collapsed', () => {
@@ -29,7 +25,7 @@ describe('ToggleSidebar', () => {
expect(findGlButton().props('icon')).toBe('chevron-double-lg-left');
});
- it('should render the "chevron-double-lg-right" icon when expanded', async () => {
+ it('should render the "chevron-double-lg-right" icon when expanded', () => {
createComponent({ props: { collapsed: false } });
expect(findGlButton().props('icon')).toBe('chevron-double-lg-right');
diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js
index 391cbb1e0d5..05a7f504fd4 100644
--- a/spec/frontend/sidebar/mock_data.js
+++ b/spec/frontend/sidebar/mock_data.js
@@ -243,6 +243,18 @@ export const issuableDueDateResponse = (dueDate = null) => ({
__typename: 'Issue',
id: 'gid://gitlab/Issue/4',
dueDate,
+ dueDateFixed: dueDate,
+ },
+ },
+ },
+});
+
+export const issueDueDateSubscriptionResponse = () => ({
+ data: {
+ issuableDatesUpdated: {
+ issue: {
+ id: 'gid://gitlab/Issue/4',
+ dueDate: '2022-12-31',
},
},
},
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index 77b1ccb4f9a..f2003aee96e 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -7,7 +7,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/vue_shared/plugins/global_toast');
jest.mock('~/commons/nav/user_merge_requests');
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 fec300ddd7e..c8d972b19a3 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
@@ -19,7 +19,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
<gl-form-input-stub
class="form-control"
data-qa-selector="description_placeholder"
- placeholder="Optionally add a description about what your snippet does or how to use it…"
+ placeholder="Describe what your snippet does or how to use it…"
/>
</div>
@@ -28,10 +28,13 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
data-uploads-path=""
>
<markdown-header-stub
+ data-testid="markdownHeader"
enablepreview="true"
linecontent=""
+ markdownpreviewpath="foo/"
restrictedtoolbaritems=""
suggestionstartindex="0"
+ uploadspath=""
/>
<div
@@ -87,7 +90,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
</div>
<div
- class="js-vue-md-preview md md-preview-holder"
+ class="js-vue-md-preview md md-preview-holder gl-px-5"
style="display: none;"
/>
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 f4ebc5c3e3f..ed54582ca29 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
@@ -13,7 +13,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
target="_blank"
>
<gl-icon-stub
- name="question"
+ name="question-o"
size="12"
/>
</gl-link-stub>
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index e7dab0ad79d..d17e20ac227 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -9,7 +9,7 @@ import { stubPerformanceWebAPI } from 'helpers/performance';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
@@ -25,7 +25,7 @@ import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation.
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { testEntries, createGQLSnippetsQueryResponse, createGQLSnippet } from '../test_utils';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
const TEST_API_ERROR = new Error('TEST_API_ERROR');
@@ -94,7 +94,6 @@ describe('Snippet Edit app', () => {
let mutateSpy;
const relativeUrlRoot = '/foo/';
- const originalRelativeUrlRoot = gon.relative_url_root;
beforeEach(() => {
stubPerformanceWebAPI();
@@ -108,12 +107,6 @@ describe('Snippet Edit app', () => {
jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- gon.relative_url_root = originalRelativeUrlRoot;
- });
-
const findBlobActions = () => wrapper.findComponent(SnippetBlobActionsEdit);
const findCancelButton = () => wrapper.findByTestId('snippet-cancel-btn');
const clickSubmitBtn = () => wrapper.findByTestId('snippet-edit-form').trigger('submit');
@@ -132,10 +125,6 @@ describe('Snippet Edit app', () => {
props = {},
selectedLevel = VISIBILITY_LEVEL_PRIVATE_STRING,
} = {}) => {
- if (wrapper) {
- throw new Error('wrapper already created');
- }
-
const requestHandlers = [
[GetSnippetQuery, getSpy],
// See `mutateSpy` declaration comment for why we send a key
@@ -267,7 +256,7 @@ describe('Snippet Edit app', () => {
VISIBILITY_LEVEL_PRIVATE_STRING,
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
- ])('marks %s visibility by default', async (visibility) => {
+ ])('marks %s visibility by default', (visibility) => {
createComponent({
props: { snippetGid: '' },
selectedLevel: visibility,
@@ -339,7 +328,7 @@ describe('Snippet Edit app', () => {
it('should redirect to snippet view on successful mutation', async () => {
await createComponentAndSubmit();
- expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
+ expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL); // eslint-disable-line import/no-deprecated
});
describe('when there are errors after creating a new snippet', () => {
@@ -347,7 +336,7 @@ describe('Snippet Edit app', () => {
projectPath
${'project/path'}
${''}
- `('should flash error (projectPath=$projectPath)', async ({ projectPath }) => {
+ `('should alert error (projectPath=$projectPath)', async ({ projectPath }) => {
mutateSpy.mockResolvedValue(createMutationResponseWithErrors('createSnippet'));
await createComponentAndLoad({
@@ -360,7 +349,7 @@ describe('Snippet Edit app', () => {
await waitForPromises();
- expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
expect(createAlert).toHaveBeenCalledWith({
message: `Can't create snippet: ${TEST_MUTATION_ERROR}`,
});
@@ -373,7 +362,7 @@ describe('Snippet Edit app', () => {
${'project/path'}
${''}
`(
- 'should flash error with (snippet=$snippetGid, projectPath=$projectPath)',
+ 'should alert error with (snippet=$snippetGid, projectPath=$projectPath)',
async ({ projectPath }) => {
mutateSpy.mockResolvedValue(createMutationResponseWithErrors('updateSnippet'));
@@ -384,7 +373,7 @@ describe('Snippet Edit app', () => {
},
});
- expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
expect(createAlert).toHaveBeenCalledWith({
message: `Can't update snippet: ${TEST_MUTATION_ERROR}`,
});
@@ -402,10 +391,10 @@ describe('Snippet Edit app', () => {
});
it('should not redirect', () => {
- expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
});
- it('should flash', () => {
+ it('should alert', () => {
// Apollo automatically wraps the resolver's error in a NetworkError
expect(createAlert).toHaveBeenCalledWith({
message: `Can't update snippet: ${TEST_API_ERROR.message}`,
diff --git a/spec/frontend/snippets/components/embed_dropdown_spec.js b/spec/frontend/snippets/components/embed_dropdown_spec.js
index ed5ea6cab8a..d8c6ad3278a 100644
--- a/spec/frontend/snippets/components/embed_dropdown_spec.js
+++ b/spec/frontend/snippets/components/embed_dropdown_spec.js
@@ -17,11 +17,6 @@ describe('snippets/components/embed_dropdown', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findSectionsData = () => {
const sections = [];
let current = {};
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index 032dcf8e5f5..45a7c7b0b4a 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -50,10 +50,6 @@ describe('Snippet view app', () => {
stubPerformanceWebAPI();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders loader while the query is in flight', () => {
createComponent({ loading: true });
expect(findLoadingIcon().exists()).toBe(true);
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
index a650353093d..58f47e8b0dc 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -56,11 +56,6 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
const triggerBlobDelete = (idx) => findBlobEdits().at(idx).vm.$emit('delete');
const triggerBlobUpdate = (idx, props) => findBlobEdits().at(idx).vm.$emit('blob-updated', props);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('multi-file snippets rendering', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 82c4a37ccc9..b699e056576 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -4,14 +4,14 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import { createAlert } from '~/flash';
+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 { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
const TEST_ID = 'blob_local_7';
const TEST_PATH = 'foo/bar/test.md';
@@ -62,8 +62,6 @@ describe('Snippet Blob Edit component', () => {
});
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
axiosMock.restore();
});
@@ -123,7 +121,7 @@ describe('Snippet Blob Edit component', () => {
createComponent();
});
- it('should call flash', async () => {
+ it('should call alert', async () => {
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index c7ff8c21d80..05ff64c2296 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,5 +1,6 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
import {
Blob as BlobMock,
SimpleViewerMock,
@@ -7,6 +8,7 @@ import {
RichBlobContentMock,
SimpleBlobContentMock,
} from 'jest/blob/components/mock_data';
+import GetBlobContent from 'shared_queries/snippet/snippet_blob_content.query.graphql';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import {
@@ -17,9 +19,13 @@ import {
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
describe('Blob Embeddable', () => {
let wrapper;
+ let requestHandlers;
+
const snippet = {
id: 'gid://foo.bar/snippet',
webUrl: 'https://foo.bar',
@@ -29,23 +35,47 @@ describe('Blob Embeddable', () => {
activeViewerType: SimpleViewerMock.type,
};
+ const mockDefaultHandler = ({ path, nodes } = { path: BlobMock.path }) => {
+ const renderedNodes = nodes || [
+ { __typename: 'Blob', path, richData: 'richData', plainData: 'plainData' },
+ ];
+
+ return jest.fn().mockResolvedValue({
+ data: {
+ snippets: {
+ __typename: 'Snippet',
+ id: '1',
+ nodes: [
+ {
+ __typename: 'Snippet',
+ id: '2',
+ blobs: {
+ __typename: 'Blob',
+ hasUnretrievableBlobs: false,
+ nodes: renderedNodes,
+ },
+ },
+ ],
+ },
+ },
+ });
+ };
+
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ requestHandlers = handler;
+ return createMockApollo([[GetBlobContent, requestHandlers]]);
+ };
+
function createComponent({
snippetProps = {},
data = dataMock,
blob = BlobMock,
- contentLoading = false,
+ handler = mockDefaultHandler(),
} = {}) {
- const $apollo = {
- queries: {
- blobContent: {
- loading: contentLoading,
- refetch: jest.fn(),
- skip: true,
- },
- },
- };
-
- wrapper = mount(SnippetBlobView, {
+ wrapper = shallowMount(SnippetBlobView, {
+ apolloProvider: createMockApolloProvider(handler),
propsData: {
snippet: {
...snippet,
@@ -58,45 +88,56 @@ describe('Blob Embeddable', () => {
...data,
};
},
- mocks: { $apollo },
+ stubs: {
+ BlobHeader,
+ BlobContent,
+ },
});
}
- afterEach(() => {
- wrapper.destroy();
- });
+ const findBlobHeader = () => wrapper.findComponent(BlobHeader);
+ const findBlobContent = () => wrapper.findComponent(BlobContent);
+ const findSimpleViewer = () => wrapper.findComponent(SimpleViewer);
+ const findRichViewer = () => wrapper.findComponent(RichViewer);
describe('rendering', () => {
it('renders correct components', () => {
createComponent();
- expect(wrapper.findComponent(BlobHeader).exists()).toBe(true);
- expect(wrapper.findComponent(BlobContent).exists()).toBe(true);
+ expect(findBlobHeader().exists()).toBe(true);
+ expect(findBlobContent().exists()).toBe(true);
});
- it('sets simple viewer correctly', () => {
+ it('sets simple viewer correctly', async () => {
createComponent();
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ await waitForPromises();
+
+ expect(findSimpleViewer().exists()).toBe(true);
});
- it('sets rich viewer correctly', () => {
+ it('sets rich viewer correctly', async () => {
const data = { ...dataMock, activeViewerType: RichViewerMock.type };
createComponent({
data,
});
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ await waitForPromises();
+ expect(findRichViewer().exists()).toBe(true);
});
it('correctly switches viewer type', async () => {
createComponent();
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ await waitForPromises();
+
+ expect(findSimpleViewer().exists()).toBe(true);
- wrapper.vm.switchViewer(RichViewerMock.type);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, RichViewerMock.type);
+ await waitForPromises();
- await nextTick();
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
- await wrapper.vm.switchViewer(SimpleViewerMock.type);
+ expect(findRichViewer().exists()).toBe(true);
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, SimpleViewerMock.type);
+ await waitForPromises();
+
+ expect(findSimpleViewer().exists()).toBe(true);
});
it('passes information about render error down to blob header', () => {
@@ -110,7 +151,7 @@ describe('Blob Embeddable', () => {
},
});
- expect(wrapper.findComponent(BlobHeader).props('hasRenderError')).toBe(true);
+ expect(findBlobHeader().props('hasRenderError')).toBe(true);
});
describe('bob content in multi-file scenario', () => {
@@ -123,47 +164,38 @@ describe('Blob Embeddable', () => {
richData: 'Another Rich Foo',
};
+ const MixedSimpleBlobContentMock = {
+ ...SimpleBlobContentMock,
+ richData: '<h1>Rich</h1>',
+ };
+
+ const MixedRichBlobContentMock = {
+ ...RichBlobContentMock,
+ plainData: 'Plain',
+ };
+
it.each`
- snippetBlobs | description | currentBlob | expectedContent
- ${[SimpleBlobContentMock]} | ${'one existing textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
- ${[RichBlobContentMock]} | ${'one existing rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
- ${[SimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
- ${[SimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
- ${[SimpleBlobContentMock, SimpleBlobContentMock2]} | ${'textual blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
- ${[RichBlobContentMock, RichBlobContentMock2]} | ${'rich blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
+ snippetBlobs | description | currentBlob | expectedContent | activeViewerType
+ ${[SimpleBlobContentMock]} | ${'one existing textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData} | ${SimpleViewerMock.type}
+ ${[RichBlobContentMock]} | ${'one existing rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData} | ${RichViewerMock.type}
+ ${[SimpleBlobContentMock, MixedRichBlobContentMock]} | ${'mixed blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData} | ${SimpleViewerMock.type}
+ ${[MixedSimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData} | ${RichViewerMock.type}
+ ${[SimpleBlobContentMock, SimpleBlobContentMock2]} | ${'textual blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData} | ${SimpleViewerMock.type}
+ ${[RichBlobContentMock, RichBlobContentMock2]} | ${'rich blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData} | ${RichViewerMock.type}
`(
'renders correct content for $description',
- async ({ snippetBlobs, currentBlob, expectedContent }) => {
- const apolloData = {
- snippets: {
- nodes: [
- {
- blobs: {
- nodes: snippetBlobs,
- },
- },
- ],
- },
- };
+ async ({ snippetBlobs, currentBlob, expectedContent, activeViewerType }) => {
createComponent({
+ handler: mockDefaultHandler({ path: currentBlob.path, nodes: snippetBlobs }),
+ data: { activeViewerType },
blob: {
...BlobMock,
path: currentBlob.path,
},
});
+ await waitForPromises();
- // mimic apollo's update
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- blobContent: wrapper.vm.onContentUpdate(apolloData),
- });
-
- await nextTick();
-
- const findContent = () => wrapper.findComponent(BlobContent);
-
- expect(findContent().props('content')).toBe(expectedContent);
+ expect(findBlobContent().props('content')).toBe(expectedContent);
},
);
});
@@ -178,28 +210,32 @@ describe('Blob Embeddable', () => {
window.location.hash = '#LC2';
});
- it('renders simple viewer by default', () => {
+ it('renders simple viewer by default', async () => {
createComponent({
data: {},
});
+ await waitForPromises();
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ expect(findBlobHeader().props('activeViewerType')).toBe(SimpleViewerMock.type);
+ expect(findSimpleViewer().exists()).toBe(true);
});
describe('switchViewer()', () => {
it('switches to the passed viewer', async () => {
createComponent();
+ await waitForPromises();
+
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, RichViewerMock.type);
+ await waitForPromises();
- wrapper.vm.switchViewer(RichViewerMock.type);
+ expect(findBlobHeader().props('activeViewerType')).toBe(RichViewerMock.type);
+ expect(findRichViewer().exists()).toBe(true);
- await nextTick();
- expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, SimpleViewerMock.type);
+ await waitForPromises();
- await wrapper.vm.switchViewer(SimpleViewerMock.type);
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ expect(findBlobHeader().props('activeViewerType')).toBe(SimpleViewerMock.type);
+ expect(findSimpleViewer().exists()).toBe(true);
});
});
});
@@ -209,28 +245,32 @@ describe('Blob Embeddable', () => {
window.location.hash = '#last-headline';
});
- it('renders rich viewer by default', () => {
+ it('renders rich viewer by default', async () => {
createComponent({
data: {},
});
+ await waitForPromises();
- expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ expect(findBlobHeader().props('activeViewerType')).toBe(RichViewerMock.type);
+ expect(findRichViewer().exists()).toBe(true);
});
describe('switchViewer()', () => {
it('switches to the passed viewer', async () => {
createComponent();
+ await waitForPromises();
- wrapper.vm.switchViewer(SimpleViewerMock.type);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, SimpleViewerMock.type);
+ await waitForPromises();
- await nextTick();
- expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type);
- expect(wrapper.findComponent(SimpleViewer).exists()).toBe(true);
+ expect(findBlobHeader().props('activeViewerType')).toBe(SimpleViewerMock.type);
+ expect(findSimpleViewer().exists()).toBe(true);
- await wrapper.vm.switchViewer(RichViewerMock.type);
- expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type);
- expect(wrapper.findComponent(RichViewer).exists()).toBe(true);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE, RichViewerMock.type);
+ await waitForPromises();
+
+ expect(findBlobHeader().props('activeViewerType')).toBe(RichViewerMock.type);
+ expect(findRichViewer().exists()).toBe(true);
});
});
});
@@ -239,19 +279,21 @@ describe('Blob Embeddable', () => {
describe('functionality', () => {
describe('render error', () => {
- const findContentEl = () => wrapper.findComponent(BlobContent);
-
it('correctly sets blob on the blob-content-error component', () => {
createComponent();
- expect(findContentEl().props('blob')).toEqual(BlobMock);
+ expect(findBlobContent().props('blob')).toEqual(BlobMock);
});
- it(`refetches blob content on ${BLOB_RENDER_EVENT_LOAD} event`, () => {
+ it(`refetches blob content on ${BLOB_RENDER_EVENT_LOAD} event`, async () => {
createComponent();
+ await waitForPromises();
+
+ expect(requestHandlers).toHaveBeenCalledTimes(1);
+
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_LOAD);
+ await waitForPromises();
- expect(wrapper.vm.$apollo.queries.blobContent.refetch).not.toHaveBeenCalled();
- findContentEl().vm.$emit(BLOB_RENDER_EVENT_LOAD);
- expect(wrapper.vm.$apollo.queries.blobContent.refetch).toHaveBeenCalledTimes(1);
+ expect(requestHandlers).toHaveBeenCalledTimes(2);
});
it(`sets '${SimpleViewerMock.type}' as active on ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => {
@@ -261,7 +303,7 @@ describe('Blob Embeddable', () => {
},
});
- findContentEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE);
+ findBlobContent().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE);
expect(wrapper.vm.activeViewerType).toEqual(SimpleViewerMock.type);
});
});
diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js
index ff75515e71a..2b42eba19c2 100644
--- a/spec/frontend/snippets/components/snippet_description_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js
@@ -30,10 +30,6 @@ describe('Snippet Description Edit component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/snippets/components/snippet_description_view_spec.js b/spec/frontend/snippets/components/snippet_description_view_spec.js
index 14f116f2aaf..3c5d50ccaa6 100644
--- a/spec/frontend/snippets/components/snippet_description_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_description_view_spec.js
@@ -17,10 +17,6 @@ describe('Snippet Description component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index c930c9f635b..4bf64bfd3cd 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,8 +1,9 @@
-import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
+import { GlModal, GlButton, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
+import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
@@ -10,31 +11,41 @@ import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/delete_snippet.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
-import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/alert';
+import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
+import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
+import { getCanCreateProjectSnippetMock, getCanCreatePersonalSnippetMock } from '../mock_data';
-jest.mock('~/flash');
+const ERROR_MSG = 'Foo bar';
+const ERR = { message: ERROR_MSG };
+
+const MUTATION_TYPES = {
+ RESOLVE: jest.fn().mockResolvedValue({ data: { destroySnippet: { errors: [] } } }),
+ REJECT: jest.fn().mockRejectedValue(ERR),
+};
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
describe('Snippet header component', () => {
let wrapper;
let snippet;
- let mutationTypes;
- let mutationVariables;
let mock;
+ let mockApollo;
- let errorMsg;
- let err;
- const originalRelativeUrlRoot = gon.relative_url_root;
const reportAbusePath = '/-/snippets/42/mark_as_spam';
const canReportSpam = true;
const GlEmoji = { template: '<img/>' };
function createComponent({
- loading = false,
permissions = {},
- mutationRes = mutationTypes.RESOLVE,
snippetProps = {},
provide = {},
+ canCreateProjectSnippetMock = jest.fn().mockResolvedValue(getCanCreateProjectSnippetMock()),
+ canCreatePersonalSnippetMock = jest.fn().mockResolvedValue(getCanCreatePersonalSnippetMock()),
+ deleteSnippetMock = MUTATION_TYPES.RESOLVE,
} = {}) {
const defaultProps = Object.assign(snippet, snippetProps);
if (permissions) {
@@ -42,17 +53,14 @@ describe('Snippet header component', () => {
...permissions,
});
}
- const $apollo = {
- queries: {
- canCreateSnippet: {
- loading,
- },
- },
- mutate: mutationRes,
- };
+
+ mockApollo = createMockApollo([
+ [CanCreateProjectSnippet, canCreateProjectSnippetMock],
+ [CanCreatePersonalSnippet, canCreatePersonalSnippetMock],
+ [DeleteSnippetMutation, deleteSnippetMock],
+ ]);
wrapper = mount(SnippetHeader, {
- mocks: { $apollo },
provide: {
reportAbusePath,
canReportSpam,
@@ -64,9 +72,9 @@ describe('Snippet header component', () => {
},
},
stubs: {
- ApolloMutation,
GlEmoji,
},
+ apolloProvider: mockApollo,
});
}
@@ -91,6 +99,7 @@ describe('Snippet header component', () => {
title: x.attributes('title'),
text: x.text(),
}));
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
beforeEach(() => {
gon.relative_url_root = '/foo/';
@@ -113,28 +122,12 @@ describe('Snippet header component', () => {
createdAt: new Date(differenceInMilliseconds(32 * 24 * 3600 * 1000)).toISOString(),
};
- mutationVariables = {
- mutation: DeleteSnippetMutation,
- variables: {
- id: snippet.id,
- },
- };
-
- errorMsg = 'Foo bar';
- err = { message: errorMsg };
-
- mutationTypes = {
- RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
- REJECT: jest.fn(() => Promise.reject(err)),
- };
-
mock = new MockAdapter(axios);
});
afterEach(() => {
- wrapper.destroy();
+ mockApollo = null;
mock.restore();
- gon.relative_url_root = originalRelativeUrlRoot;
});
it('renders itself', () => {
@@ -238,15 +231,16 @@ describe('Snippet header component', () => {
});
it('with canCreateSnippet permission, renders create button', async () => {
- createComponent();
+ createComponent({
+ canCreateProjectSnippetMock: jest
+ .fn()
+ .mockResolvedValue(getCanCreateProjectSnippetMock(true)),
+ canCreatePersonalSnippetMock: jest
+ .fn()
+ .mockResolvedValue(getCanCreatePersonalSnippetMock(true)),
+ });
- // TODO: we should avoid `wrapper.setData` since they
- // are component internals. Let's use the apollo mock helpers
- // in a follow-up.
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ canCreateSnippet: true });
- await nextTick();
+ await waitForPromises();
expect(findButtonsAsModel()).toEqual(
expect.arrayContaining([
@@ -262,7 +256,7 @@ describe('Snippet header component', () => {
});
describe('submit snippet as spam', () => {
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
@@ -271,7 +265,7 @@ describe('Snippet header component', () => {
${200} | ${VARIANT_SUCCESS} | ${i18n.snippetSpamSuccess}
${500} | ${VARIANT_DANGER} | ${i18n.snippetSpamFailure}
`(
- 'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
+ 'renders a "$variant" alert message with "$text" message for a request with a "$request" response',
async ({ request, variant, text }) => {
const submitAsSpamBtn = findButtons().at(2);
mock.onPost(reportAbusePath).reply(request);
@@ -329,21 +323,37 @@ describe('Snippet header component', () => {
});
describe('Delete mutation', () => {
- it('dispatches a mutation to delete the snippet with correct variables', () => {
+ const deleteSnippet = async () => {
+ // Click delete action
+ findButtons().at(1).trigger('click');
+ await nextTick();
+
+ expect(findDeleteModal().props().visible).toBe(true);
+
+ // Click delete button in delete modal
+ document.querySelector('[data-testid="delete-snippet"').click();
+ await waitForPromises();
+ };
+
+ it('dispatches a mutation to delete the snippet with correct variables', async () => {
createComponent();
- wrapper.vm.deleteSnippet();
- expect(mutationTypes.RESOLVE).toHaveBeenCalledWith(mutationVariables);
+
+ await deleteSnippet();
+
+ expect(MUTATION_TYPES.RESOLVE).toHaveBeenCalledWith({
+ id: snippet.id,
+ });
});
it('sets error message if mutation fails', async () => {
- createComponent({ mutationRes: mutationTypes.REJECT });
+ createComponent({ deleteSnippetMock: MUTATION_TYPES.REJECT });
expect(Boolean(wrapper.vm.errorMessage)).toBe(false);
- wrapper.vm.deleteSnippet();
-
- await waitForPromises();
+ await deleteSnippet();
- expect(wrapper.vm.errorMessage).toEqual(errorMsg);
+ expect(document.querySelector('[data-testid="delete-alert"').textContent.trim()).toBe(
+ ERROR_MSG,
+ );
});
describe('in case of successful mutation, closes modal and redirects to correct listing', () => {
@@ -353,15 +363,16 @@ describe('Snippet header component', () => {
createComponent({
snippetProps,
});
- wrapper.vm.closeDeleteModal = jest.fn();
- wrapper.vm.deleteSnippet();
- await nextTick();
+ await deleteSnippet();
};
it('redirects to dashboard/snippets for personal snippet', async () => {
await createDeleteSnippet();
- expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
+
+ // Check that the modal is hidden after deleting the snippet
+ expect(findDeleteModal().props().visible).toBe(false);
+
expect(window.location.pathname).toBe(`${gon.relative_url_root}dashboard/snippets`);
});
@@ -372,7 +383,10 @@ describe('Snippet header component', () => {
fullPath,
},
});
- expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
+
+ // Check that the modal is hidden after deleting the snippet
+ expect(findDeleteModal().props().visible).toBe(false);
+
expect(window.location.pathname).toBe(`${fullPath}/-/snippets`);
});
});
diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js
index 7c40735d64e..0a3b57c9244 100644
--- a/spec/frontend/snippets/components/snippet_title_spec.js
+++ b/spec/frontend/snippets/components/snippet_title_spec.js
@@ -26,10 +26,6 @@ describe('Snippet header component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders itself', () => {
createComponent();
expect(wrapper.find('.snippet-header').exists()).toBe(true);
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 29eb002ef4a..70eb719f706 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -51,10 +51,6 @@ describe('Snippet Visibility Edit component', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
diff --git a/spec/frontend/snippets/mock_data.js b/spec/frontend/snippets/mock_data.js
new file mode 100644
index 00000000000..7546fa575c6
--- /dev/null
+++ b/spec/frontend/snippets/mock_data.js
@@ -0,0 +1,19 @@
+export const getCanCreateProjectSnippetMock = (createSnippet = false) => ({
+ data: {
+ project: {
+ userPermissions: {
+ createSnippet,
+ },
+ },
+ },
+});
+
+export const getCanCreatePersonalSnippetMock = (createSnippet = false) => ({
+ data: {
+ currentUser: {
+ userPermissions: {
+ createSnippet,
+ },
+ },
+ },
+});
diff --git a/spec/frontend/streaming/chunk_writer_spec.js b/spec/frontend/streaming/chunk_writer_spec.js
new file mode 100644
index 00000000000..2aadb332838
--- /dev/null
+++ b/spec/frontend/streaming/chunk_writer_spec.js
@@ -0,0 +1,214 @@
+import { ChunkWriter } from '~/streaming/chunk_writer';
+import { RenderBalancer } from '~/streaming/render_balancer';
+
+jest.mock('~/streaming/render_balancer');
+
+describe('ChunkWriter', () => {
+ let accumulator = '';
+ let write;
+ let close;
+ let abort;
+ let config;
+ let render;
+
+ const createChunk = (text) => {
+ const encoder = new TextEncoder();
+ return encoder.encode(text);
+ };
+
+ const createHtmlStream = () => {
+ write = jest.fn((part) => {
+ accumulator += part;
+ });
+ close = jest.fn();
+ abort = jest.fn();
+ return {
+ write,
+ close,
+ abort,
+ };
+ };
+
+ const createWriter = () => {
+ return new ChunkWriter(createHtmlStream(), config);
+ };
+
+ const pushChunks = (...chunks) => {
+ const writer = createWriter();
+ chunks.forEach((chunk) => {
+ writer.write(createChunk(chunk));
+ });
+ writer.close();
+ };
+
+ afterAll(() => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined;
+ });
+
+ beforeEach(() => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = 100;
+ accumulator = '';
+ config = undefined;
+ render = jest.fn((cb) => {
+ while (cb()) {
+ // render until 'false'
+ }
+ });
+ RenderBalancer.mockImplementation(() => ({ render }));
+ });
+
+ describe('when chunk length must be "1"', () => {
+ beforeEach(() => {
+ config = { minChunkSize: 1, maxChunkSize: 1 };
+ });
+
+ it('splits big chunks into smaller ones', () => {
+ const text = 'foobar';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(text.length);
+ });
+
+ it('handles small emoji chunks', () => {
+ const text = 'foo👀bar👨‍👩‍👧baz👧👧🏻👧🏼👧🏽👧🏾👧🏿';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(createChunk(text).length);
+ });
+ });
+
+ describe('when chunk length must not be lower than "5" and exceed "10"', () => {
+ beforeEach(() => {
+ config = { minChunkSize: 5, maxChunkSize: 10 };
+ });
+
+ it('joins small chunks', () => {
+ const text = '12345';
+ pushChunks(...text.split(''));
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(1);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('handles overflow with small chunks', () => {
+ const text = '123456789';
+ pushChunks(...text.split(''));
+ expect(accumulator).toBe(text);
+ expect(write).toHaveBeenCalledTimes(2);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls flush on small chunks', () => {
+ global.JEST_DEBOUNCE_THROTTLE_TIMEOUT = undefined;
+ const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator');
+ const text = '1';
+ pushChunks(text);
+ expect(accumulator).toBe(text);
+ expect(flushAccumulator).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls flush on large chunks', () => {
+ const flushAccumulator = jest.spyOn(ChunkWriter.prototype, 'flushAccumulator');
+ const text = '1234567890123';
+ const writer = createWriter();
+ writer.write(createChunk(text));
+ jest.runAllTimers();
+ expect(accumulator).toBe(text);
+ expect(flushAccumulator).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('chunk balancing', () => {
+ let increase;
+ let decrease;
+ let renderOnce;
+
+ beforeEach(() => {
+ render = jest.fn((cb) => {
+ let next = true;
+ renderOnce = () => {
+ if (!next) return;
+ next = cb();
+ };
+ });
+ RenderBalancer.mockImplementation(({ increase: inc, decrease: dec }) => {
+ increase = jest.fn(inc);
+ decrease = jest.fn(dec);
+ return {
+ render,
+ };
+ });
+ });
+
+ describe('when frame time exceeds low limit', () => {
+ beforeEach(() => {
+ config = {
+ minChunkSize: 1,
+ maxChunkSize: 5,
+ balanceRate: 10,
+ };
+ });
+
+ it('increases chunk size', () => {
+ const text = '111222223';
+ const writer = createWriter();
+ const chunk = createChunk(text);
+
+ writer.write(chunk);
+
+ renderOnce();
+ increase();
+ renderOnce();
+ renderOnce();
+
+ writer.close();
+
+ expect(accumulator).toBe(text);
+ expect(write.mock.calls).toMatchObject([['111'], ['22222'], ['3']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when frame time exceeds high limit', () => {
+ beforeEach(() => {
+ config = {
+ minChunkSize: 1,
+ maxChunkSize: 10,
+ balanceRate: 2,
+ };
+ });
+
+ it('decreases chunk size', () => {
+ const text = '1111112223345';
+ const writer = createWriter();
+ const chunk = createChunk(text);
+
+ writer.write(chunk);
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ decrease();
+
+ renderOnce();
+ renderOnce();
+
+ writer.close();
+
+ expect(accumulator).toBe(text);
+ expect(write.mock.calls).toMatchObject([['111111'], ['222'], ['33'], ['4'], ['5']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ it('calls abort on htmlStream', () => {
+ const writer = createWriter();
+ writer.abort();
+ expect(abort).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/streaming/handle_streamed_anchor_link_spec.js b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js
new file mode 100644
index 00000000000..ef17957b2fc
--- /dev/null
+++ b/spec/frontend/streaming/handle_streamed_anchor_link_spec.js
@@ -0,0 +1,132 @@
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
+import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { scrollToElement } from '~/lib/utils/common_utils';
+import LineHighlighter from '~/blob/line_highlighter';
+import { TEST_HOST } from 'spec/test_constants';
+
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/blob/line_highlighter');
+
+describe('handleStreamedAnchorLink', () => {
+ const ANCHOR_START = 'L100';
+ const ANCHOR_END = '300';
+ const findRoot = () => document.querySelector('#root');
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('when single line anchor is given', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(`${TEST_HOST}#${ANCHOR_START}`);
+ });
+
+ describe('when element is present', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"><div id="${ANCHOR_START}"></div></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when element is streamed', () => {
+ let stop;
+ const insertElement = () => {
+ findRoot().insertAdjacentHTML('afterbegin', `<div id="${ANCHOR_START}"></div>`);
+ };
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="root"></div>');
+ stop = handleStreamedAnchorLink(findRoot());
+ });
+
+ afterEach(() => {
+ stop = undefined;
+ });
+
+ it('scrolls to the anchor when inserted', async () => {
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ expect(LineHighlighter).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't scroll to the anchor when destroyed", async () => {
+ stop();
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when line range anchor is given', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = new URL(`${TEST_HOST}#${ANCHOR_START}-${ANCHOR_END}`);
+ });
+
+ describe('when last element is present', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"><div id="L${ANCHOR_END}"></div></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when last element is streamed', () => {
+ let stop;
+ const insertElement = () => {
+ findRoot().insertAdjacentHTML(
+ 'afterbegin',
+ `<div id="${ANCHOR_START}"></div><div id="L${ANCHOR_END}"></div>`,
+ );
+ };
+
+ beforeEach(() => {
+ setHTMLFixture('<div id="root"></div>');
+ stop = handleStreamedAnchorLink(findRoot());
+ });
+
+ afterEach(() => {
+ stop = undefined;
+ });
+
+ it('scrolls to the anchor when inserted', async () => {
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).toHaveBeenCalledTimes(1);
+ expect(LineHighlighter).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't scroll to the anchor when destroyed", async () => {
+ stop();
+ insertElement();
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when anchor is not given', () => {
+ beforeEach(() => {
+ setHTMLFixture(`<div id="root"></div>`);
+ handleStreamedAnchorLink(findRoot());
+ });
+
+ it('does nothing', async () => {
+ await waitForPromises();
+ expect(scrollToElement).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/streaming/html_stream_spec.js b/spec/frontend/streaming/html_stream_spec.js
new file mode 100644
index 00000000000..115a9ddc803
--- /dev/null
+++ b/spec/frontend/streaming/html_stream_spec.js
@@ -0,0 +1,46 @@
+import { HtmlStream } from '~/streaming/html_stream';
+import { ChunkWriter } from '~/streaming/chunk_writer';
+
+jest.mock('~/streaming/chunk_writer');
+
+describe('HtmlStream', () => {
+ let write;
+ let close;
+ let streamingElement;
+
+ beforeEach(() => {
+ write = jest.fn();
+ close = jest.fn();
+ jest.spyOn(Document.prototype, 'write').mockImplementation(write);
+ jest.spyOn(Document.prototype, 'close').mockImplementation(close);
+ jest.spyOn(Document.prototype, 'querySelector').mockImplementation(() => {
+ streamingElement = document.createElement('div');
+ return streamingElement;
+ });
+ });
+
+ it('attaches to original document', () => {
+ // eslint-disable-next-line no-new
+ new HtmlStream(document.body);
+ expect(document.body.contains(streamingElement)).toBe(true);
+ });
+
+ it('can write to a document', () => {
+ const htmlStream = new HtmlStream(document.body);
+ htmlStream.write('foo');
+ htmlStream.close();
+ expect(write.mock.calls).toEqual([['<streaming-element>'], ['foo'], ['</streaming-element>']]);
+ expect(close).toHaveBeenCalledTimes(1);
+ });
+
+ it('returns chunked writer', () => {
+ const htmlStream = new HtmlStream(document.body).withChunkWriter();
+ expect(htmlStream).toBeInstanceOf(ChunkWriter);
+ });
+
+ it('closes on abort', () => {
+ const htmlStream = new HtmlStream(document.body);
+ htmlStream.abort();
+ expect(close).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/streaming/rate_limit_stream_requests_spec.js b/spec/frontend/streaming/rate_limit_stream_requests_spec.js
new file mode 100644
index 00000000000..02e3cf93014
--- /dev/null
+++ b/spec/frontend/streaming/rate_limit_stream_requests_spec.js
@@ -0,0 +1,155 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
+
+describe('rateLimitStreamRequests', () => {
+ const encoder = new TextEncoder('utf-8');
+ const createStreamResponse = (content = 'foo') =>
+ new ReadableStream({
+ pull(controller) {
+ controller.enqueue(encoder.encode(content));
+ controller.close();
+ },
+ });
+
+ const createFactory = (content) => {
+ return jest.fn(() => {
+ return Promise.resolve(createStreamResponse(content));
+ });
+ };
+
+ it('does nothing for zero total requests', () => {
+ const factory = jest.fn();
+ const requests = rateLimitStreamRequests({
+ factory,
+ total: 0,
+ });
+ expect(factory).toHaveBeenCalledTimes(0);
+ expect(requests.length).toBe(0);
+ });
+
+ it('does not exceed total requests', () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ immediateCount: 100,
+ maxConcurrentRequests: 100,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(2);
+ });
+
+ it('creates immediate requests', () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(2);
+ });
+
+ it('returns correct values', async () => {
+ const fixture = 'foobar';
+ const factory = createFactory(fixture);
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 2,
+ });
+
+ const decoder = new TextDecoder('utf-8');
+ let result = '';
+ for await (const stream of requests) {
+ await stream.pipeTo(
+ new WritableStream({
+ // eslint-disable-next-line no-loop-func
+ write(content) {
+ result += decoder.decode(content);
+ },
+ }),
+ );
+ }
+
+ expect(result).toBe(fixture + fixture);
+ });
+
+ it('delays rate limited requests', async () => {
+ const factory = createFactory();
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 2,
+ total: 3,
+ });
+ expect(factory).toHaveBeenCalledTimes(2);
+ expect(requests.length).toBe(3);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(3);
+ });
+
+ it('runs next request after previous has been fulfilled', async () => {
+ let res;
+ const factory = jest
+ .fn()
+ .mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ res = resolve;
+ }),
+ )
+ .mockImplementationOnce(() => Promise.resolve(createStreamResponse()));
+ const requests = rateLimitStreamRequests({
+ factory,
+ maxConcurrentRequests: 1,
+ total: 2,
+ });
+ expect(factory).toHaveBeenCalledTimes(1);
+ expect(requests.length).toBe(2);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(1);
+
+ res(createStreamResponse());
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(2);
+ });
+
+ it('uses timer to schedule next request', async () => {
+ let res;
+ const factory = jest
+ .fn()
+ .mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ res = resolve;
+ }),
+ )
+ .mockImplementationOnce(() => Promise.resolve(createStreamResponse()));
+ const requests = rateLimitStreamRequests({
+ factory,
+ immediateCount: 1,
+ maxConcurrentRequests: 2,
+ total: 2,
+ timeout: 9999,
+ });
+ expect(factory).toHaveBeenCalledTimes(1);
+ expect(requests.length).toBe(2);
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(1);
+
+ jest.runAllTimers();
+
+ await waitForPromises();
+
+ expect(factory).toHaveBeenCalledTimes(2);
+ res(createStreamResponse());
+ });
+});
diff --git a/spec/frontend/streaming/render_balancer_spec.js b/spec/frontend/streaming/render_balancer_spec.js
new file mode 100644
index 00000000000..dae0c98d678
--- /dev/null
+++ b/spec/frontend/streaming/render_balancer_spec.js
@@ -0,0 +1,69 @@
+import { RenderBalancer } from '~/streaming/render_balancer';
+
+const HIGH_FRAME_TIME = 100;
+const LOW_FRAME_TIME = 10;
+
+describe('renderBalancer', () => {
+ let frameTime = 0;
+ let frameTimeDelta = 0;
+ let decrease;
+ let increase;
+
+ const createBalancer = () => {
+ decrease = jest.fn();
+ increase = jest.fn();
+ return new RenderBalancer({
+ highFrameTime: HIGH_FRAME_TIME,
+ lowFrameTime: LOW_FRAME_TIME,
+ increase,
+ decrease,
+ });
+ };
+
+ const renderTimes = (times) => {
+ const balancer = createBalancer();
+ return new Promise((resolve) => {
+ let counter = 0;
+ balancer.render(() => {
+ if (counter === times) {
+ resolve(counter);
+ return false;
+ }
+ counter += 1;
+ return true;
+ });
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
+ frameTime += frameTimeDelta;
+ cb(frameTime);
+ });
+ });
+
+ afterEach(() => {
+ window.requestAnimationFrame.mockRestore();
+ frameTime = 0;
+ frameTimeDelta = 0;
+ });
+
+ it('renders in a loop', async () => {
+ const count = await renderTimes(5);
+ expect(count).toBe(5);
+ });
+
+ it('calls decrease', async () => {
+ frameTimeDelta = 200;
+ await renderTimes(5);
+ expect(decrease).toHaveBeenCalled();
+ expect(increase).not.toHaveBeenCalled();
+ });
+
+ it('calls increase', async () => {
+ frameTimeDelta = 1;
+ await renderTimes(5);
+ expect(increase).toHaveBeenCalled();
+ expect(decrease).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/streaming/render_html_streams_spec.js b/spec/frontend/streaming/render_html_streams_spec.js
new file mode 100644
index 00000000000..55cef0ea469
--- /dev/null
+++ b/spec/frontend/streaming/render_html_streams_spec.js
@@ -0,0 +1,96 @@
+import { ReadableStream } from 'node:stream/web';
+import { renderHtmlStreams } from '~/streaming/render_html_streams';
+import { HtmlStream } from '~/streaming/html_stream';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/streaming/html_stream');
+jest.mock('~/streaming/constants', () => {
+ return {
+ HIGH_FRAME_TIME: 0,
+ LOW_FRAME_TIME: 0,
+ MAX_CHUNK_SIZE: 1,
+ MIN_CHUNK_SIZE: 1,
+ };
+});
+
+const firstStreamContent = 'foobar';
+const secondStreamContent = 'bazqux';
+
+describe('renderHtmlStreams', () => {
+ let htmlWriter;
+ const encoder = new TextEncoder();
+ const createSingleChunkStream = (chunk) => {
+ const encoded = encoder.encode(chunk);
+ const stream = new ReadableStream({
+ pull(controller) {
+ controller.enqueue(encoded);
+ controller.close();
+ },
+ });
+ return [stream, encoded];
+ };
+
+ beforeEach(() => {
+ htmlWriter = {
+ write: jest.fn(),
+ close: jest.fn(),
+ abort: jest.fn(),
+ };
+ jest.spyOn(HtmlStream.prototype, 'withChunkWriter').mockReturnValue(htmlWriter);
+ });
+
+ it('renders a single stream', async () => {
+ const [stream, encoded] = createSingleChunkStream(firstStreamContent);
+
+ await renderHtmlStreams([Promise.resolve(stream)], document.body);
+
+ expect(htmlWriter.write).toHaveBeenCalledWith(encoded);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('renders stream sequence', async () => {
+ const [stream1, encoded1] = createSingleChunkStream(firstStreamContent);
+ const [stream2, encoded2] = createSingleChunkStream(secondStreamContent);
+
+ await renderHtmlStreams([Promise.resolve(stream1), Promise.resolve(stream2)], document.body);
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it("doesn't wait for the whole sequence to resolve before streaming", async () => {
+ const [stream1, encoded1] = createSingleChunkStream(firstStreamContent);
+ const [stream2, encoded2] = createSingleChunkStream(secondStreamContent);
+
+ let res;
+ const delayedStream = new Promise((resolve) => {
+ res = resolve;
+ });
+
+ renderHtmlStreams([Promise.resolve(stream1), delayedStream], document.body);
+
+ await waitForPromises();
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(0);
+
+ res(stream2);
+ await waitForPromises();
+
+ expect(htmlWriter.write.mock.calls).toMatchObject([[encoded1], [encoded2]]);
+ expect(htmlWriter.close).toHaveBeenCalledTimes(1);
+ });
+
+ it('closes HtmlStream on error', async () => {
+ const [stream1] = createSingleChunkStream(firstStreamContent);
+ const error = new Error();
+
+ try {
+ await renderHtmlStreams([Promise.resolve(stream1), Promise.reject(error)], document.body);
+ } catch (err) {
+ expect(err).toBe(error);
+ }
+
+ expect(htmlWriter.abort).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/context_switcher_spec.js b/spec/frontend/super_sidebar/components/context_switcher_spec.js
new file mode 100644
index 00000000000..7928ee6400c
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/context_switcher_spec.js
@@ -0,0 +1,309 @@
+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 { 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 persistentLinks = [
+ { title: 'Explore', link: '/explore', icon: 'compass', link_classes: 'persistent-link-class' },
+];
+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,
+ propsData: {
+ persistentLinks,
+ 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 persistent links', () => {
+ const navItems = findNavItems();
+ const firstNavItem = navItems.at(0);
+
+ expect(navItems.length).toBe(persistentLinks.length);
+ expect(firstNavItem.props('item')).toBe(persistentLinks[0]);
+ expect(firstNavItem.props('linkClasses')).toEqual({
+ [persistentLinks[0].link_classes]: persistentLinks[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("passes Popper.js' options to the disclosure dropdown", () => {
+ expect(findDisclosureDropdown().props('popperOptions')).toMatchObject({
+ modifiers: expect.any(Array),
+ });
+ });
+
+ 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
new file mode 100644
index 00000000000..7172b60d0fa
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/context_switcher_toggle_spec.js
@@ -0,0 +1,50 @@
+import { GlAvatar } 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 findGlAvatar = () => wrapper.getComponent(GlAvatar);
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMountExtended(ContextSwitcherToggle, {
+ 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/counter_spec.js b/spec/frontend/super_sidebar/components/counter_spec.js
index 8f514540413..77f77eae1c2 100644
--- a/spec/frontend/super_sidebar/components/counter_spec.js
+++ b/spec/frontend/super_sidebar/components/counter_spec.js
@@ -49,4 +49,15 @@ describe('Counter component', () => {
expect(findButton().exists()).toBe(false);
});
});
+
+ it.each([
+ ['99+', '99+'],
+ ['110%', '110%'],
+ [100, '99+'],
+ [10, '10'],
+ [0, ''],
+ ])('formats count %p as %p', (count, result) => {
+ createWrapper({ count });
+ expect(findButton().text()).toBe(result);
+ });
});
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
index b24c6b8de7f..456085e23da 100644
--- a/spec/frontend/super_sidebar/components/create_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -1,5 +1,13 @@
-import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import {
+ GlDisclosureDropdown,
+ GlTooltip,
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+} from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
import { createNewMenuGroups } from '../mock_data';
@@ -8,13 +16,24 @@ describe('CreateMenu component', () => {
let wrapper;
const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findGlDisclosureDropdownGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup);
+ const findGlDisclosureDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
const findGlTooltip = () => wrapper.findComponent(GlTooltip);
+ const closeAndFocusMock = jest.fn();
+
const createWrapper = () => {
wrapper = shallowMountExtended(CreateMenu, {
propsData: {
groups: createNewMenuGroups,
},
+ stubs: {
+ InviteMembersTrigger,
+ GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
+ methods: { closeAndFocus: closeAndFocusMock },
+ }),
+ },
});
};
@@ -23,17 +42,61 @@ describe('CreateMenu component', () => {
createWrapper();
});
+ it('passes popper options to the dropdown', () => {
+ createWrapper();
+
+ expect(findGlDisclosureDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-147, 4] } }],
+ });
+ });
+
it("sets the toggle's label", () => {
expect(findGlDisclosureDropdown().props('toggleText')).toBe(__('Create new...'));
});
+ it('has correct amount of dropdown groups', () => {
+ const items = findGlDisclosureDropdownGroups();
- it('passes the groups to the disclosure dropdown', () => {
- expect(findGlDisclosureDropdown().props('items')).toBe(createNewMenuGroups);
+ expect(items.exists()).toBe(true);
+ expect(items).toHaveLength(createNewMenuGroups.length);
+ });
+
+ it('has correct amount of dropdown items', () => {
+ const items = findGlDisclosureDropdownItems();
+ const numberOfMenuItems = createNewMenuGroups
+ .map((group) => group.items.length)
+ .reduce((a, b) => a + b);
+
+ expect(items.exists()).toBe(true);
+ expect(items).toHaveLength(numberOfMenuItems);
+ });
+
+ it('renders the invite member trigger', () => {
+ 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);
+ });
+
+ it('shows the tooltip when the dropdown is closed', async () => {
+ findGlDisclosureDropdown().vm.$emit('shown');
+ findGlDisclosureDropdown().vm.$emit('hidden');
+ await nextTick();
+
+ expect(findGlTooltip().exists()).toBe(true);
+ });
+
+ it('closes the dropdown when invite members modal is opened', () => {
+ findInviteMembersTrigger().vm.$emit('modal-opened');
+ expect(closeAndFocusMock).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/frequent_items_list_spec.js b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
new file mode 100644
index 00000000000..5329a8f5da3
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/frequent_items_list_spec.js
@@ -0,0 +1,79 @@
+import { shallowMountExtended } 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 createWrapper = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(FrequentItemsList, {
+ propsData: {
+ title,
+ pristineText,
+ storageKey,
+ maxItems,
+ ...props,
+ },
+ });
+ };
+
+ 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', () => {
+ it('remove-item event emission from items-list causes list item to be removed', async () => {
+ const localStorageProjects = findItemsList().props('items');
+
+ await findItemsList().vm.$emit('remove-item', localStorageProjects[0]);
+
+ 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/components/global_search_autocomplete_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
new file mode 100644
index 00000000000..aac321bd8e0
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_autocomplete_items_spec.js
@@ -0,0 +1,128 @@
+import {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ GlLoadingIcon,
+ GlAvatar,
+ GlAlert,
+} from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
+
+import {
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
+} from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchAutocompleteItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState, mockGetters, props) => {
+ const store = new Vuex.Store({
+ state: {
+ loading: false,
+ ...initialState,
+ },
+ getters: {
+ autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ ...mockGetters,
+ },
+ });
+
+ wrapper = shallowMount(GlobalSearchAutocompleteItems, {
+ store,
+ propsData: {
+ ...props,
+ },
+ stubs: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ },
+ });
+ };
+
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemTitles = () =>
+ findItems().wrappers.map((w) => w.find('[data-testid="autocomplete-item-name"]').text());
+ const findItemSubTitles = () =>
+ findItems()
+ .wrappers.map((w) => w.find('[data-testid="autocomplete-item-namespace"]'))
+ .filter((w) => w.exists())
+ .map((w) => w.text());
+ const findItemLinks = () => findItems().wrappers.map((w) => w.find('a').attributes('href'));
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAvatars = () => wrapper.findAllComponents(GlAvatar).wrappers.map((w) => w.props('src'));
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
+
+ describe('template', () => {
+ describe('when loading is true', () => {
+ beforeEach(() => {
+ createComponent({ loading: true });
+ });
+
+ it('renders GlLoadingIcon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render autocomplete options', () => {
+ expect(findItems()).toHaveLength(0);
+ });
+ });
+
+ describe('when api returns error', () => {
+ beforeEach(() => {
+ createComponent({ autocompleteError: true });
+ });
+
+ it('renders Alert', () => {
+ expect(findGlAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('when loading is false', () => {
+ beforeEach(() => {
+ createComponent({ loading: false });
+ });
+
+ it('does not render GlLoadingIcon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('Search results items', () => {
+ it('renders item for each option in autocomplete option', () => {
+ expect(findItems()).toHaveLength(MOCK_SORTED_AUTOCOMPLETE_OPTIONS.length);
+ });
+
+ it('renders titles correctly', () => {
+ const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.text);
+ expect(findItemTitles()).toStrictEqual(expectedTitles);
+ });
+
+ it('renders sub-titles correctly', () => {
+ const expectedSubTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.filter((o) => o.value).map(
+ (o) => o.namespace,
+ );
+
+ expect(findItemSubTitles()).toStrictEqual(expectedSubTitles);
+ });
+
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.href);
+ expect(findItemLinks()).toStrictEqual(expectedLinks);
+ });
+
+ it('renders avatars', () => {
+ const expectedAvatars = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.avatar_url).filter(
+ Boolean,
+ );
+ expect(findAvatars()).toStrictEqual(expectedAvatars);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
new file mode 100644
index 00000000000..52e9aa52c14
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_default_items_spec.js
@@ -0,0 +1,75 @@
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
+import { MOCK_SEARCH_CONTEXT, MOCK_DEFAULT_SEARCH_OPTIONS } from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchDefaultItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState, props) => {
+ const store = new Vuex.Store({
+ state: {
+ searchContext: MOCK_SEARCH_CONTEXT,
+ ...initialState,
+ },
+ getters: {
+ defaultSearchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ },
+ });
+
+ wrapper = shallowMountExtended(GlobalSearchDefaultItems, {
+ store,
+ propsData: {
+ ...props,
+ },
+ stubs: {
+ GlDisclosureDropdownGroup,
+ },
+ });
+ };
+
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemsData = () => findItems().wrappers.map((w) => w.props('item'));
+
+ describe('template', () => {
+ describe('Dropdown items', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders item for each option in defaultSearchOptions', () => {
+ expect(findItems()).toHaveLength(MOCK_DEFAULT_SEARCH_OPTIONS.length);
+ });
+
+ it('provides the `item` prop to the `GlDisclosureDropdownItem` component', () => {
+ expect(findItemsData()).toStrictEqual(MOCK_DEFAULT_SEARCH_OPTIONS);
+ });
+ });
+
+ describe.each`
+ group | project | groupHeader
+ ${null} | ${null} | ${'All GitLab'}
+ ${{ name: 'Test Group' }} | ${null} | ${'Test Group'}
+ ${{ name: 'Test Group' }} | ${{ name: 'Test Project' }} | ${'Test Project'}
+ `('Group Header', ({ group, project, groupHeader }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createComponent({
+ searchContext: {
+ group,
+ project,
+ },
+ });
+ });
+
+ it(`should render as ${groupHeader}`, () => {
+ expect(wrapper.text()).toContain(groupHeader);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
new file mode 100644
index 00000000000..4976f3be4cd
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_scoped_items_spec.js
@@ -0,0 +1,91 @@
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlToken, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { trimText } from 'helpers/text_helper';
+import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import { truncate } from '~/lib/utils/text_utility';
+import { SCOPE_TOKEN_MAX_LENGTH } from '~/super_sidebar/components/global_search/constants';
+import { MSG_IN_ALL_GITLAB } from '~/vue_shared/global_search/constants';
+import {
+ MOCK_SEARCH,
+ MOCK_SCOPED_SEARCH_GROUP,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+} from '../mock_data';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchScopedItems', () => {
+ let wrapper;
+
+ const createComponent = (initialState, mockGetters, props) => {
+ const store = new Vuex.Store({
+ state: {
+ search: MOCK_SEARCH,
+ ...initialState,
+ },
+ getters: {
+ scopedSearchGroup: () => MOCK_SCOPED_SEARCH_GROUP,
+ autocompleteGroupedSearchOptions: () => MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ ...mockGetters,
+ },
+ });
+
+ wrapper = shallowMount(GlobalSearchScopedItems, {
+ store,
+ propsData: {
+ ...props,
+ },
+ stubs: {
+ GlDisclosureDropdownGroup,
+ GlDisclosureDropdownItem,
+ },
+ });
+ };
+
+ const findItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+ const findItemsText = () => findItems().wrappers.map((w) => trimText(w.text()));
+ const findScopeTokens = () => wrapper.findAllComponents(GlToken);
+ const findScopeTokensText = () => findScopeTokens().wrappers.map((w) => trimText(w.text()));
+ const findScopeTokensIcons = () =>
+ findScopeTokens().wrappers.map((w) => w.findAllComponents(GlIcon));
+ const findItemLinks = () => findItems().wrappers.map((w) => w.find('a').attributes('href'));
+
+ describe('Search results scoped items', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders item for each item in scopedSearchGroup', () => {
+ expect(findItems()).toHaveLength(MOCK_SCOPED_SEARCH_GROUP.items.length);
+ });
+
+ it('renders titles correctly', () => {
+ findItemsText().forEach((title) => expect(title).toContain(MOCK_SEARCH));
+ });
+
+ it('renders scope names correctly', () => {
+ const expectedTitles = MOCK_SCOPED_SEARCH_GROUP.items.map((o) =>
+ truncate(trimText(`in ${o.scope || o.description}`), SCOPE_TOKEN_MAX_LENGTH),
+ );
+
+ expect(findScopeTokensText()).toStrictEqual(expectedTitles);
+ });
+
+ it('renders scope icons correctly', () => {
+ findScopeTokensIcons().forEach((icon, i) => {
+ const w = icon.wrappers[0];
+ expect(w?.attributes('name')).toBe(MOCK_SCOPED_SEARCH_GROUP.items[i].icon);
+ });
+ });
+
+ it(`renders scope ${MSG_IN_ALL_GITLAB} correctly`, () => {
+ expect(findScopeTokens().at(-1).findComponent(GlIcon).exists()).toBe(false);
+ });
+
+ it('renders links correctly', () => {
+ const expectedLinks = MOCK_SCOPED_SEARCH_GROUP.items.map((o) => o.href);
+ expect(findItemLinks()).toStrictEqual(expectedLinks);
+ });
+ });
+});
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
new file mode 100644
index 00000000000..f78e141afad
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -0,0 +1,372 @@
+import { GlModal, GlSearchBoxByType, GlToken, GlIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { s__, sprintf } from '~/locale';
+import GlobalSearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
+import GlobalSearchAutocompleteItems from '~/super_sidebar/components/global_search/components/global_search_autocomplete_items.vue';
+import GlobalSearchDefaultItems from '~/super_sidebar/components/global_search/components/global_search_default_items.vue';
+import GlobalSearchScopedItems from '~/super_sidebar/components/global_search/components/global_search_scoped_items.vue';
+import {
+ SEARCH_INPUT_DESCRIPTION,
+ SEARCH_RESULTS_DESCRIPTION,
+ ICON_PROJECT,
+ ICON_GROUP,
+ ICON_SUBGROUP,
+ SCOPE_TOKEN_MAX_LENGTH,
+ IS_SEARCHING,
+ SEARCH_SHORTCUTS_MIN_CHARACTERS,
+} from '~/super_sidebar/components/global_search/constants';
+import { truncate } from '~/lib/utils/text_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { ENTER_KEY } from '~/lib/utils/keys';
+import {
+ MOCK_SEARCH,
+ MOCK_SEARCH_QUERY,
+ MOCK_USERNAME,
+ MOCK_DEFAULT_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_SEARCH_CONTEXT_FULL,
+ MOCK_PROJECT,
+ MOCK_GROUP,
+} from '../mock_data';
+
+Vue.use(Vuex);
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
+
+describe('GlobalSearchModal', () => {
+ let wrapper;
+
+ const actionSpies = {
+ setSearch: jest.fn(),
+ fetchAutocompleteOptions: jest.fn(),
+ clearAutocomplete: jest.fn(),
+ };
+
+ const deafaultMockState = {
+ searchContext: {
+ project: MOCK_PROJECT,
+ group: MOCK_GROUP,
+ },
+ };
+
+ const createComponent = (initialState, mockGetters, stubs) => {
+ const store = new Vuex.Store({
+ state: {
+ ...deafaultMockState,
+ ...initialState,
+ },
+ actions: actionSpies,
+ getters: {
+ searchQuery: () => MOCK_SEARCH_QUERY,
+ searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ scopedSearchOptions: () => MOCK_SCOPED_SEARCH_OPTIONS,
+ ...mockGetters,
+ },
+ });
+
+ wrapper = shallowMountExtended(GlobalSearchModal, {
+ store,
+ stubs,
+ });
+ };
+
+ const formatScopeName = (scopeName) => {
+ if (!scopeName) {
+ return false;
+ }
+ const searchResultsScope = s__('GlobalSearch|in %{scope}');
+ return truncate(
+ sprintf(searchResultsScope, {
+ scope: scopeName,
+ }),
+ SCOPE_TOKEN_MAX_LENGTH,
+ );
+ };
+
+ const findGlobalSearchModal = () => wrapper.findComponent(GlModal);
+
+ const findGlobalSearchForm = () => wrapper.findByTestId('global-search-form');
+ const findGlobalSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findScopeToken = () => wrapper.findComponent(GlToken);
+ const findGlobalSearchDefaultItems = () => wrapper.findComponent(GlobalSearchDefaultItems);
+ const findGlobalSearchScopedItems = () => wrapper.findComponent(GlobalSearchScopedItems);
+ const findGlobalSearchAutocompleteItems = () =>
+ wrapper.findComponent(GlobalSearchAutocompleteItems);
+ const findSearchInputDescription = () => wrapper.find(`#${SEARCH_INPUT_DESCRIPTION}`);
+ const findSearchResultsDescription = () => wrapper.findByTestId(SEARCH_RESULTS_DESCRIPTION);
+
+ describe('template', () => {
+ describe('always renders', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Global Search Input', () => {
+ expect(findGlobalSearchInput().exists()).toBe(true);
+ });
+
+ it('Search Input Description', () => {
+ expect(findSearchInputDescription().exists()).toBe(true);
+ });
+
+ it('Search Results Description', () => {
+ expect(findSearchResultsDescription().exists()).toBe(true);
+ });
+ });
+
+ describe.each`
+ search | showDefault | showScoped | showAutocomplete
+ ${null} | ${true} | ${false} | ${false}
+ ${''} | ${true} | ${false} | ${false}
+ ${'t'} | ${false} | ${false} | ${true}
+ ${'te'} | ${false} | ${false} | ${true}
+ ${'tes'} | ${false} | ${true} | ${true}
+ ${MOCK_SEARCH} | ${false} | ${true} | ${true}
+ `('Global Search Result Items', ({ search, showDefault, showScoped, showAutocomplete }) => {
+ describe(`when search is ${search}`, () => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent({ search }, {});
+ findGlobalSearchInput().vm.$emit('click');
+ });
+
+ it(`should${showDefault ? '' : ' not'} render the Default Items`, () => {
+ expect(findGlobalSearchDefaultItems().exists()).toBe(showDefault);
+ });
+
+ it(`should${showScoped ? '' : ' not'} render the Scoped Items`, () => {
+ expect(findGlobalSearchScopedItems().exists()).toBe(showScoped);
+ });
+
+ it(`should${showAutocomplete ? '' : ' not'} render the Autocomplete Items`, () => {
+ expect(findGlobalSearchAutocompleteItems().exists()).toBe(showAutocomplete);
+ });
+ });
+ });
+
+ describe.each`
+ username | search | loading | searchOptions | expectedDesc
+ ${null} | ${'gi'} | ${false} | ${[]} | ${GlobalSearchModal.i18n.MIN_SEARCH_TERM}
+ ${MOCK_USERNAME} | ${'gi'} | ${false} | ${[]} | ${GlobalSearchModal.i18n.MIN_SEARCH_TERM}
+ ${MOCK_USERNAME} | ${''} | ${false} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${`${MOCK_DEFAULT_SEARCH_OPTIONS.length} default results provided. Use the up and down arrow keys to navigate search results list.`}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${true} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${GlobalSearchModal.i18n.SEARCH_RESULTS_LOADING}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${false} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${`Results updated. ${MOCK_SCOPED_SEARCH_OPTIONS.length} results available. Use the up and down arrow keys to navigate search results list, or ENTER to submit.`}
+ ${MOCK_USERNAME} | ${MOCK_SEARCH} | ${true} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${GlobalSearchModal.i18n.SEARCH_RESULTS_LOADING}
+ `(
+ 'Search Results Description',
+ ({ username, search, loading, searchOptions, expectedDesc }) => {
+ describe(`search is "${search}" and loading is ${loading}`, () => {
+ beforeEach(() => {
+ window.gon.current_username = username;
+ createComponent(
+ {
+ search,
+ loading,
+ },
+ {
+ searchOptions: () => searchOptions,
+ },
+ );
+ });
+
+ it(`sets description to ${expectedDesc}`, () => {
+ expect(findSearchResultsDescription().text()).toBe(expectedDesc);
+ });
+ });
+ },
+ );
+
+ describe('input box', () => {
+ describe.each`
+ search | searchOptions | hasToken
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[1]]} | ${true}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${true}
+ ${'te'} | ${[MOCK_SCOPED_SEARCH_OPTIONS[5]]} | ${false}
+ ${'x'} | ${[]} | ${false}
+ `('token', ({ search, searchOptions, hasToken }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent(
+ { search },
+ {
+ searchOptions: () => searchOptions,
+ },
+ );
+ findGlobalSearchInput().vm.$emit('click');
+ });
+
+ it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
+ searchOptions[0]?.html_id
+ }"`, () => {
+ expect(findScopeToken().exists()).toBe(hasToken);
+ });
+
+ it(`text ${hasToken ? 'is correctly' : 'is NOT'} rendered when text is "${
+ searchOptions[0]?.scope || searchOptions[0]?.description
+ }"`, () => {
+ expect(findScopeToken().exists() && findScopeToken().text()).toBe(
+ formatScopeName(searchOptions[0]?.scope || searchOptions[0]?.description),
+ );
+ });
+ });
+ });
+
+ describe('form', () => {
+ describe.each`
+ searchContext | search | searchOptions
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${null} | ${null} | ${[]}
+ `('wrapper', ({ searchContext, search, searchOptions }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
+ });
+
+ const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
+
+ it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
+ if (isSearching) {
+ expect(findGlobalSearchForm().classes()).toContain(IS_SEARCHING);
+ return;
+ }
+ if (!isSearching) {
+ expect(findGlobalSearchForm().classes()).not.toContain(IS_SEARCHING);
+ }
+ });
+ });
+ });
+
+ describe.each`
+ search | searchOptions | hasIcon | iconName
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} | ${ICON_PROJECT}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} | ${ICON_GROUP}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} | ${ICON_SUBGROUP}
+ ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false} | ${false}
+ `('token', ({ search, searchOptions, hasIcon, iconName }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent(
+ { search },
+ {
+ searchOptions: () => searchOptions,
+ },
+ );
+ findGlobalSearchInput().vm.$emit('click');
+ });
+
+ it(`icon for data set type "${searchOptions[0]?.html_id}" ${
+ hasIcon ? 'is' : 'is NOT'
+ } rendered`, () => {
+ expect(findScopeToken().findComponent(GlIcon).exists()).toBe(hasIcon);
+ });
+
+ it(`render ${iconName ? `"${iconName}"` : 'NO'} icon for data set type "${
+ searchOptions[0]?.html_id
+ }"`, () => {
+ expect(
+ findScopeToken().findComponent(GlIcon).exists() &&
+ findScopeToken().findComponent(GlIcon).attributes('name'),
+ ).toBe(iconName);
+ });
+ });
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent();
+ window.gon.current_username = MOCK_USERNAME;
+ });
+
+ describe('Global Search Input', () => {
+ describe('onInput', () => {
+ describe('when search has text', () => {
+ beforeEach(() => {
+ findGlobalSearchInput().vm.$emit('input', MOCK_SEARCH);
+ });
+
+ it('calls setSearch with search term', () => {
+ expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), MOCK_SEARCH);
+ });
+
+ it('calls fetchAutocompleteOptions', () => {
+ expect(actionSpies.fetchAutocompleteOptions).toHaveBeenCalled();
+ });
+
+ it('does not call clearAutocomplete', () => {
+ expect(actionSpies.clearAutocomplete).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when search is emptied', () => {
+ beforeEach(() => {
+ findGlobalSearchInput().vm.$emit('input', '');
+ });
+
+ it('calls setSearch with empty term', () => {
+ expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), '');
+ });
+
+ it('does not call fetchAutocompleteOptions', () => {
+ expect(actionSpies.fetchAutocompleteOptions).not.toHaveBeenCalled();
+ });
+
+ it('calls clearAutocomplete', () => {
+ expect(actionSpies.clearAutocomplete).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('Submitting a search', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('onKey-enter submits a search', () => {
+ findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+
+ expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
+
+ describe('with less than min characters', () => {
+ beforeEach(() => {
+ createComponent({ search: 'x' });
+ });
+
+ it('onKey-enter will NOT submit a search', () => {
+ findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+
+ expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
+ });
+ });
+ });
+
+ describe('Modal events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should emit `shown` event when modal shown`', () => {
+ findGlobalSearchModal().vm.$emit('shown');
+ expect(wrapper.emitted('shown')).toHaveLength(1);
+ });
+
+ it('should emit `hidden` event when modal hidden`', () => {
+ findGlobalSearchModal().vm.$emit('hidden');
+ expect(wrapper.emitted('hidden')).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js
new file mode 100644
index 00000000000..0884fce567c
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -0,0 +1,456 @@
+import {
+ ICON_PROJECT,
+ ICON_GROUP,
+ ICON_SUBGROUP,
+} from '~/super_sidebar/components/global_search/constants';
+
+import {
+ PROJECTS_CATEGORY,
+ GROUPS_CATEGORY,
+ MSG_ISSUES_ASSIGNED_TO_ME,
+ MSG_ISSUES_IVE_CREATED,
+ MSG_MR_ASSIGNED_TO_ME,
+ MSG_MR_IM_REVIEWER,
+ MSG_MR_IVE_CREATED,
+ MSG_IN_ALL_GITLAB,
+} from '~/vue_shared/global_search/constants';
+
+export const MOCK_USERNAME = 'anyone';
+
+export const MOCK_SEARCH_PATH = '/search';
+
+export const MOCK_ISSUE_PATH = '/dashboard/issues';
+
+export const MOCK_MR_PATH = '/dashboard/merge_requests';
+
+export const MOCK_ALL_PATH = '/';
+
+export const MOCK_AUTOCOMPLETE_PATH = '/autocomplete';
+
+export const MOCK_PROJECT = {
+ id: 123,
+ name: 'MockProject',
+ path: '/mock-project',
+};
+
+export const MOCK_PROJECT_LONG = {
+ id: 124,
+ name: 'Mock Project Name That Is Ridiculously Long And It Goes Forever',
+ path: '/mock-project-name-that-is-ridiculously-long-and-it-goes-forever',
+};
+
+export const MOCK_GROUP = {
+ id: 321,
+ name: 'MockGroup',
+ path: '/mock-group',
+};
+
+export const MOCK_SUBGROUP = {
+ id: 322,
+ name: 'MockSubGroup',
+ path: `${MOCK_GROUP}/mock-subgroup`,
+};
+
+export const MOCK_SEARCH_QUERY = 'http://gitlab.com/search?search=test';
+
+export const MOCK_SEARCH = 'test';
+
+export const MOCK_SEARCH_CONTEXT = {
+ project: null,
+ project_metadata: {},
+ group: null,
+ group_metadata: {},
+};
+
+export const MOCK_SEARCH_CONTEXT_FULL = {
+ group: {
+ id: 31,
+ name: 'testGroup',
+ full_name: 'testGroup',
+ },
+ group_metadata: {
+ group_path: 'testGroup',
+ name: 'testGroup',
+ issues_path: '/groups/testGroup/-/issues',
+ mr_path: '/groups/testGroup/-/merge_requests',
+ },
+};
+
+export const MOCK_DEFAULT_SEARCH_OPTIONS = [
+ {
+ text: MSG_ISSUES_ASSIGNED_TO_ME,
+ href: `${MOCK_ISSUE_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ },
+ {
+ text: MSG_ISSUES_IVE_CREATED,
+ href: `${MOCK_ISSUE_PATH}/?author_username=${MOCK_USERNAME}`,
+ },
+ {
+ text: MSG_MR_ASSIGNED_TO_ME,
+ href: `${MOCK_MR_PATH}/?assignee_username=${MOCK_USERNAME}`,
+ },
+ {
+ text: MSG_MR_IM_REVIEWER,
+ href: `${MOCK_MR_PATH}/?reviewer_username=${MOCK_USERNAME}`,
+ },
+ {
+ text: MSG_MR_IVE_CREATED,
+ href: `${MOCK_MR_PATH}/?author_username=${MOCK_USERNAME}`,
+ },
+];
+export const MOCK_SCOPED_SEARCH_OPTIONS_DEF = [
+ {
+ text: 'scoped-in-project',
+ scope: MOCK_PROJECT.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ href: MOCK_PROJECT.path,
+ },
+ {
+ text: 'scoped-in-group',
+ scope: MOCK_GROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_GROUP,
+ href: MOCK_GROUP.path,
+ },
+ {
+ text: 'scoped-in-all',
+ description: MSG_IN_ALL_GITLAB,
+ href: MOCK_ALL_PATH,
+ },
+];
+export const MOCK_SCOPED_SEARCH_OPTIONS = [
+ {
+ text: 'scoped-in-project',
+ scope: MOCK_PROJECT.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ url: MOCK_PROJECT.path,
+ },
+ {
+ text: 'scoped-in-project-long',
+ scope: MOCK_PROJECT_LONG.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ url: MOCK_PROJECT_LONG.path,
+ },
+ {
+ text: 'scoped-in-group',
+ scope: MOCK_GROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_GROUP,
+ url: MOCK_GROUP.path,
+ },
+ {
+ text: 'scoped-in-subgroup',
+ scope: MOCK_SUBGROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_SUBGROUP,
+ url: MOCK_SUBGROUP.path,
+ },
+ {
+ text: 'scoped-in-all',
+ description: MSG_IN_ALL_GITLAB,
+ url: MOCK_ALL_PATH,
+ },
+];
+
+export const MOCK_SCOPED_SEARCH_GROUP = {
+ items: [
+ {
+ text: 'scoped-in-project',
+ scope: MOCK_PROJECT.name,
+ scopeCategory: PROJECTS_CATEGORY,
+ icon: ICON_PROJECT,
+ href: MOCK_PROJECT.path,
+ },
+ {
+ text: 'scoped-in-group',
+ scope: MOCK_GROUP.name,
+ scopeCategory: GROUPS_CATEGORY,
+ icon: ICON_GROUP,
+ href: MOCK_GROUP.path,
+ },
+ {
+ text: 'scoped-in-all',
+ description: MSG_IN_ALL_GITLAB,
+ href: MOCK_ALL_PATH,
+ },
+ ],
+};
+
+export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
+ {
+ category: 'Projects',
+ id: 1,
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
+ url: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
+ },
+ {
+ avatar_url: '/groups/avatar/1/avatar.png',
+ category: 'Groups',
+ id: 1,
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
+ url: 'group/1',
+ },
+ {
+ avatar_url: '/project/avatar/2/avatar.png',
+ category: 'Projects',
+ id: 2,
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
+ url: 'project/2',
+ },
+ {
+ category: 'Help',
+ label: 'GitLab Help',
+ url: 'help/gitlab',
+ },
+];
+
+export const MOCK_AUTOCOMPLETE_OPTIONS = [
+ {
+ category: 'Projects',
+ id: 1,
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
+ url: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
+ },
+ {
+ category: 'Groups',
+ id: 1,
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
+ url: 'group/1',
+ avatar_url: '/groups/avatar/1/avatar.png',
+ },
+ {
+ category: 'Projects',
+ id: 2,
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
+ url: 'project/2',
+ avatar_url: '/project/avatar/2/avatar.png',
+ },
+ {
+ category: 'Help',
+ label: 'GitLab Help',
+ url: 'help/gitlab',
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
+ {
+ name: 'Groups',
+ items: [
+ {
+ category: 'Groups',
+ id: 1,
+ label: 'Gitlab Org / MockGroup1',
+ namespace: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
+ text: 'MockGroup1',
+ href: 'group/1',
+ avatar_url: '/groups/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockGroup1',
+ },
+ ],
+ },
+ {
+ name: 'Projects',
+ items: [
+ {
+ category: 'Projects',
+ id: 1,
+ label: 'Gitlab Org / MockProject1',
+ namespace: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
+ text: 'MockProject1',
+ href: 'project/1',
+ avatar_url: '/project/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockProject1',
+ },
+ {
+ category: 'Projects',
+ id: 2,
+ value: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ namespace: 'Gitlab Org / MockProject2',
+ text: 'MockProject2',
+ href: 'project/2',
+ avatar_url: '/project/avatar/2/avatar.png',
+ avatar_size: 32,
+ entity_id: 2,
+ entity_name: 'MockProject2',
+ },
+ ],
+ },
+ {
+ name: 'Help',
+ items: [
+ {
+ category: 'Help',
+ label: 'GitLab Help',
+ text: 'GitLab Help',
+ href: 'help/gitlab',
+ avatar_size: 16,
+ entity_name: 'GitLab Help',
+ },
+ ],
+ },
+];
+
+export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
+ {
+ category: 'Groups',
+ id: 1,
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
+ text: 'MockGroup1',
+ href: 'group/1',
+ namespace: 'Gitlab Org / MockGroup1',
+ avatar_url: '/groups/avatar/1/avatar.png',
+ avatar_size: 32,
+ entity_id: 1,
+ entity_name: 'MockGroup1',
+ },
+ {
+ avatar_size: 32,
+ avatar_url: '/project/avatar/1/avatar.png',
+ category: 'Projects',
+ entity_id: 1,
+ entity_name: 'MockProject1',
+ href: 'project/1',
+ id: 1,
+ label: 'Gitlab Org / MockProject1',
+ namespace: 'Gitlab Org / MockProject1',
+ text: 'MockProject1',
+ value: 'MockProject1',
+ },
+ {
+ avatar_size: 32,
+ avatar_url: '/project/avatar/2/avatar.png',
+ category: 'Projects',
+ entity_id: 2,
+ entity_name: 'MockProject2',
+ href: 'project/2',
+ id: 2,
+ label: 'Gitlab Org / MockProject2',
+ namespace: 'Gitlab Org / MockProject2',
+ text: 'MockProject2',
+ value: 'MockProject2',
+ },
+ {
+ avatar_size: 16,
+ entity_name: 'GitLab Help',
+ category: 'Help',
+ label: 'GitLab Help',
+ text: 'GitLab Help',
+ href: 'help/gitlab',
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_HELP = [
+ {
+ category: 'Help',
+ data: [
+ {
+ html_id: 'autocomplete-Help-1',
+ category: 'Help',
+ text: 'Rake Tasks Help',
+ label: 'Rake Tasks Help',
+ href: '/help/raketasks/index',
+ },
+ {
+ html_id: 'autocomplete-Help-2',
+ category: 'Help',
+ text: 'System Hooks Help',
+ label: 'System Hooks Help',
+ href: '/help/system_hooks/system_hooks',
+ },
+ ],
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP = [
+ {
+ category: 'Settings',
+ data: [
+ {
+ html_id: 'autocomplete-Settings-0',
+ category: 'Settings',
+ label: 'User settings',
+ url: '/-/profile',
+ },
+ {
+ html_id: 'autocomplete-Settings-3',
+ category: 'Settings',
+ label: 'Admin Section',
+ url: '/admin',
+ },
+ ],
+ },
+ {
+ category: 'Help',
+ data: [
+ {
+ html_id: 'autocomplete-Help-1',
+ category: 'Help',
+ label: 'Rake Tasks Help',
+ url: '/help/raketasks/index',
+ },
+ {
+ html_id: 'autocomplete-Help-2',
+ category: 'Help',
+ label: 'System Hooks Help',
+ url: '/help/system_hooks/system_hooks',
+ },
+ ],
+ },
+];
+
+export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_2 = [
+ {
+ category: 'Groups',
+ data: [
+ {
+ html_id: 'autocomplete-Groups-0',
+ category: 'Groups',
+ id: 148,
+ label: 'Jashkenas / Test Subgroup / test-subgroup',
+ url: '/jashkenas/test-subgroup/test-subgroup',
+ avatar_url: '',
+ },
+ {
+ html_id: 'autocomplete-Groups-1',
+ category: 'Groups',
+ id: 147,
+ label: 'Jashkenas / Test Subgroup',
+ url: '/jashkenas/test-subgroup',
+ avatar_url: '',
+ },
+ ],
+ },
+ {
+ category: 'Projects',
+ data: [
+ {
+ html_id: 'autocomplete-Projects-2',
+ category: 'Projects',
+ id: 1,
+ value: 'Gitlab Test',
+ label: 'Gitlab Org / Gitlab Test',
+ url: '/gitlab-org/gitlab-test',
+ avatar_url: '/uploads/-/system/project/avatar/1/icons8-gitlab-512.png',
+ },
+ ],
+ },
+];
diff --git a/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
new file mode 100644
index 00000000000..f6d8e1f26eb
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/store/actions_spec.js
@@ -0,0 +1,111 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import * as actions from '~/super_sidebar/components/global_search/store/actions';
+import * as types from '~/super_sidebar/components/global_search/store/mutation_types';
+import initState from '~/super_sidebar/components/global_search/store/state';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import {
+ MOCK_SEARCH,
+ MOCK_AUTOCOMPLETE_OPTIONS_RES,
+ MOCK_AUTOCOMPLETE_PATH,
+ MOCK_PROJECT,
+ MOCK_SEARCH_CONTEXT,
+ MOCK_SEARCH_PATH,
+ MOCK_MR_PATH,
+ MOCK_ISSUE_PATH,
+} from '../mock_data';
+
+describe('Global Search Store Actions', () => {
+ let state;
+ let mock;
+
+ const createState = (initialState) =>
+ initState({
+ searchPath: MOCK_SEARCH_PATH,
+ issuesPath: MOCK_ISSUE_PATH,
+ mrPath: MOCK_MR_PATH,
+ autocompletePath: MOCK_AUTOCOMPLETE_PATH,
+ searchContext: MOCK_SEARCH_CONTEXT,
+ ...initialState,
+ });
+
+ afterEach(() => {
+ state = null;
+ mock.restore();
+ });
+
+ describe.each`
+ axiosMock | type | expectedMutations
+ ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
+ ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
+ `('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations }) => {
+ describe(`on ${type}`, () => {
+ beforeEach(() => {
+ state = createState({});
+ mock = new MockAdapter(axios);
+ mock[axiosMock.method]().reply(axiosMock.code, axiosMock.res);
+ });
+ it(`should dispatch the correct mutations`, () => {
+ return testAction({
+ action: actions.fetchAutocompleteOptions,
+ state,
+ expectedMutations,
+ });
+ });
+ });
+ });
+
+ describe.each`
+ project | ref | fetchType | expectedPath
+ ${null} | ${null} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}`}
+ ${MOCK_PROJECT} | ${null} | ${'generic'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&filter=generic`}
+ ${null} | ${MOCK_PROJECT.id} | ${'generic'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_ref=${MOCK_PROJECT.id}&filter=generic`}
+ ${MOCK_PROJECT} | ${MOCK_PROJECT.id} | ${'search'} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=${MOCK_PROJECT.id}&filter=search`}
+ `('autocompleteQuery', ({ project, ref, fetchType, expectedPath }) => {
+ describe(`when project is ${project?.name} and project ref is ${ref}`, () => {
+ beforeEach(() => {
+ state = createState({
+ search: MOCK_SEARCH,
+ searchContext: {
+ project,
+ ref,
+ },
+ });
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(actions.autocompleteQuery({ state, fetchType })).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe('clearAutocomplete', () => {
+ beforeEach(() => {
+ state = createState({});
+ });
+
+ it('calls the CLEAR_AUTOCOMPLETE mutation', () => {
+ return testAction({
+ action: actions.clearAutocomplete,
+ state,
+ expectedMutations: [{ type: types.CLEAR_AUTOCOMPLETE }],
+ });
+ });
+ });
+
+ describe('setSearch', () => {
+ beforeEach(() => {
+ state = createState({});
+ });
+
+ it('calls the SET_SEARCH mutation', () => {
+ return testAction({
+ action: actions.setSearch,
+ payload: MOCK_SEARCH,
+ state,
+ expectedMutations: [{ type: types.SET_SEARCH, payload: MOCK_SEARCH }],
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
new file mode 100644
index 00000000000..68583d04b31
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/store/getters_spec.js
@@ -0,0 +1,334 @@
+import * as getters from '~/super_sidebar/components/global_search/store/getters';
+import initState from '~/super_sidebar/components/global_search/store/state';
+import {
+ MOCK_USERNAME,
+ MOCK_SEARCH_PATH,
+ MOCK_ISSUE_PATH,
+ MOCK_MR_PATH,
+ MOCK_AUTOCOMPLETE_PATH,
+ MOCK_SEARCH_CONTEXT,
+ MOCK_DEFAULT_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS,
+ MOCK_SCOPED_SEARCH_GROUP,
+ MOCK_PROJECT,
+ MOCK_GROUP,
+ MOCK_ALL_PATH,
+ MOCK_SEARCH,
+ MOCK_AUTOCOMPLETE_OPTIONS,
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SORTED_AUTOCOMPLETE_OPTIONS,
+ MOCK_SCOPED_SEARCH_OPTIONS_DEF,
+} from '../mock_data';
+
+describe('Global Search Store Getters', () => {
+ let state;
+
+ const createState = (initialState) => {
+ state = initState({
+ searchPath: MOCK_SEARCH_PATH,
+ issuesPath: MOCK_ISSUE_PATH,
+ mrPath: MOCK_MR_PATH,
+ autocompletePath: MOCK_AUTOCOMPLETE_PATH,
+ searchContext: MOCK_SEARCH_CONTEXT,
+ ...initialState,
+ });
+ };
+
+ afterEach(() => {
+ state = null;
+ });
+
+ describe.each`
+ group | project | scope | forSnippets | codeSearch | ref | expectedPath
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`}
+ ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`}
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`}
+ ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`}
+ ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`}
+ `('searchQuery', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => {
+ describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope,
+ for_snippets: forSnippets,
+ code_search: codeSearch,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.searchQuery(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | group_metadata | project | project_metadata | expectedPath
+ ${null} | ${null} | ${null} | ${null} | ${MOCK_ISSUE_PATH}
+ ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${null} | ${null} | ${'group/path'}
+ ${{ name: 'Test Group' }} | ${{ issues_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ issues_path: 'project/path' }} | ${'project/path'}
+ `('scopedIssuesPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ group_metadata,
+ project,
+ project_metadata,
+ },
+ });
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.scopedIssuesPath(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | group_metadata | project | project_metadata | expectedPath
+ ${null} | ${null} | ${null} | ${null} | ${MOCK_MR_PATH}
+ ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${null} | ${null} | ${'group/path'}
+ ${{ name: 'Test Group' }} | ${{ mr_path: 'group/path' }} | ${{ name: 'Test Project' }} | ${{ mr_path: 'project/path' }} | ${'project/path'}
+ `('scopedMRPath', ({ group, group_metadata, project, project_metadata, expectedPath }) => {
+ describe(`when group is ${group?.name} and project is ${project?.name}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ group_metadata,
+ project,
+ project_metadata,
+ },
+ });
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.scopedMRPath(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | project | scope | forSnippets | codeSearch | ref | expectedPath
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`}
+ ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`}
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`}
+ ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`}
+ ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&project_id=${MOCK_PROJECT.id}&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`}
+ `('projectUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => {
+ describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope,
+ for_snippets: forSnippets,
+ code_search: codeSearch,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.projectUrl(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | project | scope | forSnippets | codeSearch | ref | expectedPath
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`}
+ ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`}
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`}
+ ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`}
+ ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&group_id=${MOCK_GROUP.id}&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`}
+ `('groupUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => {
+ describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope,
+ for_snippets: forSnippets,
+ code_search: codeSearch,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.groupUrl(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe.each`
+ group | project | scope | forSnippets | codeSearch | ref | expectedPath
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${null} | ${null} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&snippets=true`}
+ ${null} | ${null} | ${null} | ${false} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&search_code=true`}
+ ${null} | ${null} | ${null} | ${false} | ${false} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&repository_ref=test-branch`}
+ ${MOCK_GROUP} | ${null} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${null} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${null} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${false} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${false} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${null} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true&search_code=true`}
+ ${MOCK_GROUP} | ${MOCK_PROJECT} | ${'issues'} | ${true} | ${true} | ${'test-branch'} | ${`${MOCK_SEARCH_PATH}?search=${MOCK_SEARCH}&nav_source=navbar&scope=issues&snippets=true&search_code=true&repository_ref=test-branch`}
+ `('allUrl', ({ group, project, scope, forSnippets, codeSearch, ref, expectedPath }) => {
+ describe(`when group is ${group?.name}, project is ${project?.name}, scope is ${scope}, for_snippets is ${forSnippets}, code_search is ${codeSearch}, and ref is ${ref}`, () => {
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ group,
+ project,
+ scope,
+ for_snippets: forSnippets,
+ code_search: codeSearch,
+ ref,
+ },
+ });
+ state.search = MOCK_SEARCH;
+ });
+
+ it(`should return ${expectedPath}`, () => {
+ expect(getters.allUrl(state)).toBe(expectedPath);
+ });
+ });
+ });
+
+ describe('defaultSearchOptions', () => {
+ const mockGetters = {
+ scopedIssuesPath: MOCK_ISSUE_PATH,
+ scopedMRPath: MOCK_MR_PATH,
+ };
+
+ beforeEach(() => {
+ createState();
+ window.gon.current_username = MOCK_USERNAME;
+ });
+
+ it('returns the correct array', () => {
+ expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_DEFAULT_SEARCH_OPTIONS,
+ );
+ });
+
+ it('returns the correct array if issues path is false', () => {
+ mockGetters.scopedIssuesPath = undefined;
+ expect(getters.defaultSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_DEFAULT_SEARCH_OPTIONS.slice(2, MOCK_DEFAULT_SEARCH_OPTIONS.length),
+ );
+ });
+ });
+
+ describe('scopedSearchOptions', () => {
+ const mockGetters = {
+ projectUrl: MOCK_PROJECT.path,
+ groupUrl: MOCK_GROUP.path,
+ allUrl: MOCK_ALL_PATH,
+ };
+
+ beforeEach(() => {
+ createState({
+ searchContext: {
+ project: MOCK_PROJECT,
+ group: MOCK_GROUP,
+ },
+ });
+ });
+
+ it('returns the correct array', () => {
+ expect(getters.scopedSearchOptions(state, mockGetters)).toStrictEqual(
+ MOCK_SCOPED_SEARCH_OPTIONS_DEF,
+ );
+ });
+ });
+
+ describe('autocompleteGroupedSearchOptions', () => {
+ beforeEach(() => {
+ createState();
+ state.autocompleteOptions = MOCK_AUTOCOMPLETE_OPTIONS;
+ });
+
+ it('returns the correct grouped array', () => {
+ expect(getters.autocompleteGroupedSearchOptions(state)).toStrictEqual(
+ MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
+ );
+ });
+ });
+
+ describe.each`
+ search | defaultSearchOptions | scopedSearchOptions | autocompleteGroupedSearchOptions | expectedArray
+ ${null} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_GROUP} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_DEFAULT_SEARCH_OPTIONS}
+ ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${[]} | ${MOCK_SCOPED_SEARCH_OPTIONS}
+ ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS}
+ ${MOCK_SEARCH} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)}
+ ${1} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]}
+ ${'('} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${[]} | ${[]} | ${[]}
+ ${'t'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS}
+ ${'te'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SORTED_AUTOCOMPLETE_OPTIONS}
+ ${'tes'} | ${MOCK_DEFAULT_SEARCH_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${MOCK_GROUPED_AUTOCOMPLETE_OPTIONS} | ${MOCK_SCOPED_SEARCH_OPTIONS.concat(MOCK_SORTED_AUTOCOMPLETE_OPTIONS)}
+ `(
+ 'searchOptions',
+ ({
+ search,
+ defaultSearchOptions,
+ scopedSearchOptions,
+ autocompleteGroupedSearchOptions,
+ expectedArray,
+ }) => {
+ describe(`when search is ${search} and the defaultSearchOptions${
+ defaultSearchOptions.length ? '' : ' do not'
+ } exist, scopedSearchOptions${
+ scopedSearchOptions.length ? '' : ' do not'
+ } exist, and autocompleteGroupedSearchOptions${
+ autocompleteGroupedSearchOptions.length ? '' : ' do not'
+ } exist`, () => {
+ const mockGetters = {
+ defaultSearchOptions,
+ scopedSearchOptions,
+ autocompleteGroupedSearchOptions,
+ };
+
+ beforeEach(() => {
+ createState();
+ state.search = search;
+ });
+
+ it(`should return the correct combined array`, () => {
+ expect(getters.searchOptions(state, mockGetters)).toStrictEqual(expectedArray);
+ });
+ });
+ },
+ );
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
new file mode 100644
index 00000000000..4d275cf86c7
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/store/mutations_spec.js
@@ -0,0 +1,63 @@
+import * as types from '~/super_sidebar/components/global_search/store/mutation_types';
+import mutations from '~/super_sidebar/components/global_search/store/mutations';
+import createState from '~/super_sidebar/components/global_search/store/state';
+import {
+ MOCK_SEARCH,
+ MOCK_AUTOCOMPLETE_OPTIONS_RES,
+ MOCK_AUTOCOMPLETE_OPTIONS,
+} from '../mock_data';
+
+describe('Header Search Store Mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState({});
+ });
+
+ describe('REQUEST_AUTOCOMPLETE', () => {
+ it('sets loading to true and empties autocompleteOptions array', () => {
+ mutations[types.REQUEST_AUTOCOMPLETE](state);
+
+ expect(state.loading).toBe(true);
+ expect(state.autocompleteOptions).toStrictEqual([]);
+ expect(state.autocompleteError).toBe(false);
+ });
+ });
+
+ describe('RECEIVE_AUTOCOMPLETE_SUCCESS', () => {
+ it('sets loading to false and then formats and sets the autocompleteOptions array', () => {
+ mutations[types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, MOCK_AUTOCOMPLETE_OPTIONS_RES);
+
+ expect(state.loading).toBe(false);
+ expect(state.autocompleteOptions).toEqual(MOCK_AUTOCOMPLETE_OPTIONS);
+ expect(state.autocompleteError).toBe(false);
+ });
+ });
+
+ describe('RECEIVE_AUTOCOMPLETE_ERROR', () => {
+ it('sets loading to false and empties autocompleteOptions array', () => {
+ mutations[types.RECEIVE_AUTOCOMPLETE_ERROR](state);
+
+ expect(state.loading).toBe(false);
+ expect(state.autocompleteOptions).toStrictEqual([]);
+ expect(state.autocompleteError).toBe(true);
+ });
+ });
+
+ describe('CLEAR_AUTOCOMPLETE', () => {
+ it('empties autocompleteOptions array', () => {
+ mutations[types.CLEAR_AUTOCOMPLETE](state);
+
+ expect(state.autocompleteOptions).toStrictEqual([]);
+ expect(state.autocompleteError).toBe(false);
+ });
+ });
+
+ describe('SET_SEARCH', () => {
+ it('sets search to value', () => {
+ mutations[types.SET_SEARCH](state, MOCK_SEARCH);
+
+ expect(state.search).toBe(MOCK_SEARCH);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
new file mode 100644
index 00000000000..3b12063e733
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/global_search/utils_spec.js
@@ -0,0 +1,60 @@
+import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
+import {
+ LARGE_AVATAR_PX,
+ SMALL_AVATAR_PX,
+} from '~/super_sidebar/components/global_search/constants';
+import {
+ GROUPS_CATEGORY,
+ PROJECTS_CATEGORY,
+ MERGE_REQUEST_CATEGORY,
+ ISSUES_CATEGORY,
+ RECENT_EPICS_CATEGORY,
+} from '~/vue_shared/global_search/constants';
+
+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);
+ });
+
+ 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 avatar entityId to ${entityId}`, () => {
+ expect(formattedItem.entity_id).toBe(entityId);
+ });
+
+ it(`should set avatar entityName to ${entityName}`, () => {
+ expect(formattedItem.entity_name).toBe(entityName);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/groups_list_spec.js b/spec/frontend/super_sidebar/components/groups_list_spec.js
new file mode 100644
index 00000000000..4fa3303c12f
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/groups_list_spec.js
@@ -0,0 +1,90 @@
+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/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
index bc847a3e159..808c30436a3 100644
--- a/spec/frontend/super_sidebar/components/help_center_spec.js
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -1,21 +1,25 @@
-import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { within } from '@testing-library/dom';
import toggleWhatsNewDrawer from '~/whats_new';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import HelpCenter from '~/super_sidebar/components/help_center.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
+import { DOMAIN, PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { STORAGE_KEY } from '~/whats_new/utils/notification';
+import { helpCenterState } from '~/super_sidebar/constants';
+import { mockTracking } from 'helpers/tracking_helper';
import { sidebarData } from '../mock_data';
jest.mock('~/whats_new');
describe('HelpCenter component', () => {
let wrapper;
+ let trackingSpy;
const GlEmoji = { template: '<img/>' };
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdownGroup = (i = 0) => {
return wrapper.findAllComponents(GlDisclosureDropdownGroup).at(i);
};
@@ -28,26 +32,58 @@ describe('HelpCenter component', () => {
propsData: { sidebarData },
stubs: { GlEmoji },
});
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
+ const trackingAttrs = (label) => {
+ return {
+ 'data-track-action': 'click_link',
+ 'data-track-property': 'nav_help_menu',
+ 'data-track-label': label,
+ };
+ };
+
+ const DEFAULT_HELP_ITEMS = [
+ { text: HelpCenter.i18n.help, href: helpPagePath(), extraAttrs: trackingAttrs('help') },
+ {
+ text: HelpCenter.i18n.support,
+ href: sidebarData.support_path,
+ extraAttrs: trackingAttrs('support'),
+ },
+ {
+ text: HelpCenter.i18n.docs,
+ href: `https://docs.${DOMAIN}`,
+ extraAttrs: trackingAttrs('gitlab_documentation'),
+ },
+ {
+ text: HelpCenter.i18n.plans,
+ href: `${PROMO_URL}/pricing`,
+ extraAttrs: trackingAttrs('compare_gitlab_plans'),
+ },
+ {
+ text: HelpCenter.i18n.forum,
+ href: `https://forum.${DOMAIN}/`,
+ extraAttrs: trackingAttrs('community_forum'),
+ },
+ {
+ text: HelpCenter.i18n.contribute,
+ href: helpPagePath('', { anchor: 'contributing-to-gitlab' }),
+ extraAttrs: trackingAttrs('contribute_to_gitlab'),
+ },
+ {
+ text: HelpCenter.i18n.feedback,
+ href: `${PROMO_URL}/submit-feedback`,
+ extraAttrs: trackingAttrs('submit_feedback'),
+ },
+ ];
+
describe('default', () => {
beforeEach(() => {
createWrapper(sidebarData);
});
it('renders menu items', () => {
- expect(findDropdownGroup(0).props('group').items).toEqual([
- { text: HelpCenter.i18n.help, href: helpPagePath() },
- { text: HelpCenter.i18n.support, href: sidebarData.support_path },
- { text: HelpCenter.i18n.docs, href: 'https://docs.gitlab.com' },
- { text: HelpCenter.i18n.plans, href: `${PROMO_URL}/pricing` },
- { text: HelpCenter.i18n.forum, href: 'https://forum.gitlab.com/' },
- {
- text: HelpCenter.i18n.contribute,
- href: helpPagePath('', { anchor: 'contributing-to-gitlab' }),
- },
- { text: HelpCenter.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' },
- ]);
+ expect(findDropdownGroup(0).props('group').items).toEqual(DEFAULT_HELP_ITEMS);
expect(findDropdownGroup(1).props('group').items).toEqual([
expect.objectContaining({ text: HelpCenter.i18n.shortcuts }),
@@ -55,6 +91,44 @@ describe('HelpCenter component', () => {
]);
});
+ it('passes popper options to the dropdown', () => {
+ expect(findDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-4, 4] } }],
+ });
+ });
+
+ describe('with show_tanuki_bot true', () => {
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, show_tanuki_bot: true });
+ jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ });
+
+ it('shows Ask GitLab Chat with the help items', () => {
+ expect(findDropdownGroup(0).props('group').items).toEqual([
+ expect.objectContaining({
+ icon: 'tanuki',
+ text: HelpCenter.i18n.chat,
+ extraAttrs: trackingAttrs('tanuki_bot_help_dropdown'),
+ }),
+ ...DEFAULT_HELP_ITEMS,
+ ]);
+ });
+
+ describe('when Ask GitLab Chat button is clicked', () => {
+ beforeEach(() => {
+ findButton('Ask GitLab Chat').click();
+ });
+
+ it('closes the dropdown', () => {
+ expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
+ });
+
+ it('sets helpCenterState.showTanukiBotChatDrawer to true', () => {
+ expect(helpCenterState.showTanukiBotChatDrawer).toBe(true);
+ });
+ });
+ });
+
describe('with Gitlab version check feature enabled', () => {
beforeEach(() => {
createWrapper({ ...sidebarData, show_version_check: true });
@@ -62,30 +136,53 @@ describe('HelpCenter component', () => {
it('shows version information as first item', () => {
expect(findDropdownGroup(0).props('group').items).toEqual([
- { text: HelpCenter.i18n.version, href: helpPagePath('update/index'), version: '16.0' },
+ {
+ text: HelpCenter.i18n.version,
+ href: helpPagePath('update/index'),
+ version: '16.0',
+ extraAttrs: trackingAttrs('version_help_dropdown'),
+ },
]);
});
});
describe('showKeyboardShortcuts', () => {
+ let button;
+
beforeEach(() => {
jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
- window.toggleShortcutsHelp = jest.fn();
- findButton('Keyboard shortcuts ?').click();
+
+ button = findButton('Keyboard shortcuts ?');
});
it('closes the dropdown', () => {
+ button.click();
expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
});
it('shows the keyboard shortcuts modal', () => {
- expect(window.toggleShortcutsHelp).toHaveBeenCalled();
+ // This relies on the event delegation set up by the Shortcuts class in
+ // ~/behaviors/shortcuts/shortcuts.js.
+ expect(button.classList.contains('js-shortcuts-modal-trigger')).toBe(true);
+ });
+
+ it('should have Snowplow tracking attributes', () => {
+ expect(findButton('Keyboard shortcuts ?').dataset).toEqual(
+ expect.objectContaining({
+ trackAction: 'click_button',
+ trackLabel: 'keyboard_shortcuts_help',
+ trackProperty: 'nav_help_menu',
+ }),
+ );
});
});
describe('showWhatsNew', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, show_version_check: true });
+ });
findButton("What's new 5").click();
});
@@ -102,6 +199,18 @@ describe('HelpCenter component', () => {
expect(toggleWhatsNewDrawer).toHaveBeenCalledTimes(2);
expect(toggleWhatsNewDrawer).toHaveBeenLastCalledWith();
});
+
+ it('should have Snowplow tracking attributes', () => {
+ createWrapper({ ...sidebarData, display_whats_new: true });
+
+ expect(findButton("What's new 5").dataset).toEqual(
+ expect.objectContaining({
+ trackAction: 'click_button',
+ trackLabel: 'whats_new',
+ trackProperty: 'nav_help_menu',
+ }),
+ );
+ });
});
describe('shouldShowWhatsNewNotification', () => {
@@ -148,5 +257,23 @@ describe('HelpCenter component', () => {
});
});
});
+
+ describe('toggle dropdown', () => {
+ it('should track Snowplow event when dropdown is shown', () => {
+ findDropdown().vm.$emit('shown');
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: 'show_help_dropdown',
+ property: 'nav_help_menu',
+ });
+ });
+
+ it('should track Snowplow event when dropdown is hidden', () => {
+ findDropdown().vm.$emit('hidden');
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_toggle', {
+ label: 'hide_help_dropdown',
+ property: 'nav_help_menu',
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/items_list_spec.js b/spec/frontend/super_sidebar/components/items_list_spec.js
new file mode 100644
index 00000000000..d5e8043cce9
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/items_list_spec.js
@@ -0,0 +1,101 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMountExtended, mountExtended } 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 = {}, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(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);
+ });
+
+ describe('item removal', () => {
+ const findRemoveButton = () => wrapper.findByTestId('item-remove');
+ const mockProject = {
+ ...firstMockedProject,
+ title: firstMockedProject.name,
+ };
+
+ beforeEach(() => {
+ createWrapper({
+ props: {
+ items: [mockProject],
+ },
+ mountFn: mountExtended,
+ });
+ });
+
+ it('renders the remove button', () => {
+ const itemRemoveButton = findRemoveButton();
+
+ expect(itemRemoveButton.exists()).toBe(true);
+ expect(itemRemoveButton.attributes('title')).toBe('Remove');
+ expect(itemRemoveButton.findComponent(GlIcon).props('name')).toBe('dash');
+ });
+
+ it('emits `remove-item` event with item param when remove button is clicked', () => {
+ const itemRemoveButton = findRemoveButton();
+
+ itemRemoveButton.vm.$emit(
+ 'click',
+ { stopPropagation: jest.fn(), preventDefault: jest.fn() },
+ mockProject,
+ );
+
+ expect(wrapper.emitted('remove-item')).toEqual([[mockProject]]);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/menu_section_spec.js b/spec/frontend/super_sidebar/components/menu_section_spec.js
new file mode 100644
index 00000000000..556e07a2e31
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/menu_section_spec.js
@@ -0,0 +1,102 @@
+import { GlCollapse } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import MenuSection from '~/super_sidebar/components/menu_section.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { stubComponent } from 'helpers/stub_component';
+
+describe('MenuSection component', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.find('button');
+ const findCollapse = () => wrapper.getComponent(GlCollapse);
+ const findNavItems = () => wrapper.findAllComponents(NavItem);
+ const createWrapper = (item, otherProps) => {
+ wrapper = shallowMountExtended(MenuSection, {
+ propsData: { item, ...otherProps },
+ stubs: {
+ GlCollapse: stubComponent(GlCollapse, {
+ props: ['visible'],
+ }),
+ },
+ });
+ };
+
+ it('renders its title', () => {
+ createWrapper({ title: 'Asdf' });
+ expect(findButton().text()).toBe('Asdf');
+ });
+
+ it('renders all its subitems', () => {
+ createWrapper({
+ title: 'Asdf',
+ items: [
+ { title: 'Item1', href: '/item1' },
+ { title: 'Item2', href: '/item2' },
+ ],
+ });
+ expect(findNavItems().length).toBe(2);
+ });
+
+ it('associates button with list with aria-controls', () => {
+ createWrapper({ title: 'Asdf' });
+ expect(findButton().attributes('aria-controls')).toBe('asdf');
+ expect(findCollapse().attributes('id')).toBe('asdf');
+ });
+
+ describe('collapse behavior', () => {
+ describe('when active', () => {
+ it('is expanded', () => {
+ createWrapper({ title: 'Asdf', is_active: true });
+ expect(findCollapse().props('visible')).toBe(true);
+ });
+ });
+
+ describe('when set to expanded', () => {
+ it('is expanded', () => {
+ createWrapper({ title: 'Asdf' }, { expanded: true });
+ expect(findButton().attributes('aria-expanded')).toBe('true');
+ expect(findCollapse().props('visible')).toBe(true);
+ });
+ });
+
+ describe('when not active nor set to expanded', () => {
+ it('is not expanded', () => {
+ createWrapper({ title: 'Asdf' });
+ expect(findButton().attributes('aria-expanded')).toBe('false');
+ expect(findCollapse().props('visible')).toBe(false);
+ });
+ });
+ });
+
+ describe('`separated` prop', () => {
+ describe('by default (false)', () => {
+ it('does not render a separator', () => {
+ createWrapper({ title: 'Asdf' });
+ expect(wrapper.find('hr').exists()).toBe(false);
+ });
+ });
+
+ describe('when set to true', () => {
+ it('does render a separator', () => {
+ createWrapper({ title: 'Asdf' }, { separated: true });
+ expect(wrapper.find('hr').exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('`tag` prop', () => {
+ describe('by default', () => {
+ it('renders as <div> tag', () => {
+ createWrapper({ title: 'Asdf' });
+ expect(wrapper.element.tagName).toBe('DIV');
+ });
+ });
+
+ describe('when set to "li"', () => {
+ it('renders as <li> tag', () => {
+ createWrapper({ title: 'Asdf' }, { tag: 'li' });
+ expect(wrapper.element.tagName).toBe('LI');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
index fe87c4be9c3..53d47397eb3 100644
--- a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
@@ -1,6 +1,7 @@
import { GlBadge, GlDisclosureDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
+import { userCounts } from '~/super_sidebar/user_counts_manager';
import { mergeRequestMenuGroup } from '../mock_data';
describe('MergeRequestMenu component', () => {
@@ -8,30 +9,37 @@ describe('MergeRequestMenu component', () => {
const findGlBadge = (at) => wrapper.findAllComponents(GlBadge).at(at);
const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findLink = () => wrapper.findByRole('link');
+ const findLink = (name) => wrapper.findByRole('link', { name });
- const createWrapper = () => {
+ const createWrapper = (items) => {
wrapper = mountExtended(MergeRequestMenu, {
propsData: {
- items: mergeRequestMenuGroup,
+ items,
},
});
};
describe('default', () => {
beforeEach(() => {
- createWrapper();
+ createWrapper(mergeRequestMenuGroup);
});
it('passes the items to the disclosure dropdown', () => {
expect(findGlDisclosureDropdown().props('items')).toBe(mergeRequestMenuGroup);
});
- it('renders item text and count in link', () => {
- const { text, href, count } = mergeRequestMenuGroup[0].items[0];
- expect(findLink().text()).toContain(text);
- expect(findLink().text()).toContain(String(count));
- expect(findLink().attributes('href')).toBe(href);
+ it.each(mergeRequestMenuGroup[0].items)('renders item text and count in link', (item) => {
+ const index = mergeRequestMenuGroup[0].items.indexOf(item);
+ const { text, href, count, extraAttrs } = mergeRequestMenuGroup[0].items[index];
+ const link = findLink(new RegExp(text));
+
+ expect(link.text()).toContain(text);
+ expect(link.text()).toContain(String(count));
+ expect(link.attributes('href')).toBe(href);
+ expect(link.attributes('data-track-action')).toBe(extraAttrs['data-track-action']);
+ expect(link.attributes('data-track-label')).toBe(extraAttrs['data-track-label']);
+ expect(link.attributes('data-track-property')).toBe(extraAttrs['data-track-property']);
+ expect(link.attributes('class')).toContain(extraAttrs.class);
});
it('renders item count string in badge', () => {
@@ -42,5 +50,21 @@ describe('MergeRequestMenu component', () => {
it('renders 0 string when count is empty', () => {
expect(findGlBadge(1).text()).toBe(String(0));
});
+
+ it('renders value from userCounts if `userCount` prop is defined', () => {
+ userCounts.assigned_merge_requests = 5;
+ mergeRequestMenuGroup[0].items[0].userCount = 'assigned_merge_requests';
+ createWrapper(mergeRequestMenuGroup);
+
+ expect(findGlBadge(0).text()).toBe(String(userCounts.assigned_merge_requests));
+ });
+
+ it('renders item count if unknown `userCount` prop is defined', () => {
+ const { count } = mergeRequestMenuGroup[0].items[0];
+ mergeRequestMenuGroup[0].items[0].userCount = 'foobar';
+ createWrapper(mergeRequestMenuGroup);
+
+ expect(findGlBadge(0).text()).toBe(String(count));
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/nav_item_link_spec.js b/spec/frontend/super_sidebar/components/nav_item_link_spec.js
new file mode 100644
index 00000000000..5cc1bd01d0f
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/nav_item_link_spec.js
@@ -0,0 +1,37 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import NavItemLink from '~/super_sidebar/components/nav_item_link.vue';
+
+describe('NavItemLink component', () => {
+ let wrapper;
+
+ const createWrapper = (item) => {
+ wrapper = shallowMountExtended(NavItemLink, {
+ propsData: {
+ item,
+ },
+ });
+ };
+
+ describe('when `item` has `is_active` set to `false`', () => {
+ it('renders an anchor tag without active CSS class and `aria-current` attribute', () => {
+ createWrapper({ title: 'foo', link: '/foo', is_active: false });
+
+ expect(wrapper.attributes()).toEqual({
+ href: '/foo',
+ class: '',
+ });
+ });
+ });
+
+ describe('when `item` has `is_active` set to `true`', () => {
+ it('renders an anchor tag with active CSS class and `aria-current="page"`', () => {
+ createWrapper({ title: 'foo', link: '/foo', is_active: true });
+
+ expect(wrapper.attributes()).toEqual({
+ href: '/foo',
+ class: 'gl-bg-t-gray-a-08',
+ 'aria-current': 'page',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js b/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js
new file mode 100644
index 00000000000..a7ca56325fe
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/nav_item_router_link_spec.js
@@ -0,0 +1,56 @@
+import { RouterLinkStub } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import NavItemRouterLink from '~/super_sidebar/components/nav_item_router_link.vue';
+
+describe('NavItemRouterLink component', () => {
+ let wrapper;
+
+ const createWrapper = ({ item, routerLinkSlotProps = {} }) => {
+ wrapper = mountExtended(NavItemRouterLink, {
+ propsData: {
+ item,
+ },
+ stubs: {
+ RouterLink: {
+ ...RouterLinkStub,
+ render() {
+ const children = this.$scopedSlots.default({
+ href: '/foo',
+ isActive: false,
+ navigate: jest.fn(),
+ ...routerLinkSlotProps,
+ });
+ return children;
+ },
+ },
+ },
+ });
+ };
+
+ describe('when `RouterLink` is not active', () => {
+ it('renders an anchor tag without active CSS class and `aria-current` attribute', () => {
+ createWrapper({ item: { title: 'foo', to: { name: 'foo' } } });
+
+ expect(wrapper.attributes()).toEqual({
+ href: '/foo',
+ custom: '',
+ });
+ });
+ });
+
+ describe('when `RouterLink` is active', () => {
+ it('renders an anchor tag with active CSS class and `aria-current="page"`', () => {
+ createWrapper({
+ item: { title: 'foo', to: { name: 'foo' } },
+ routerLinkSlotProps: { isActive: true },
+ });
+
+ expect(wrapper.findComponent(RouterLinkStub).props('activeClass')).toBe('gl-bg-t-gray-a-08');
+ expect(wrapper.attributes()).toEqual({
+ href: '/foo',
+ 'aria-current': 'page',
+ custom: '',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
new file mode 100644
index 00000000000..54ac4965ad8
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -0,0 +1,156 @@
+import { GlBadge } from '@gitlab/ui';
+import { RouterLinkStub } from '@vue/test-utils';
+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';
+import NavItemLink from '~/super_sidebar/components/nav_item_link.vue';
+import {
+ CLICK_MENU_ITEM_ACTION,
+ TRACKING_UNKNOWN_ID,
+ TRACKING_UNKNOWN_PANEL,
+} from '~/super_sidebar/constants';
+
+describe('NavItem component', () => {
+ let wrapper;
+
+ const findLink = () => wrapper.findByTestId('nav-item-link');
+ const findPill = () => wrapper.findComponent(GlBadge);
+ const findNavItemRouterLink = () => extendedWrapper(wrapper.findComponent(NavItemRouterLink));
+ const findNavItemLink = () => extendedWrapper(wrapper.findComponent(NavItemLink));
+
+ const createWrapper = ({ item, props = {}, provide = {}, routerLinkSlotProps = {} }) => {
+ wrapper = mountExtended(NavItem, {
+ propsData: {
+ item,
+ ...props,
+ },
+ provide,
+ stubs: {
+ RouterLink: {
+ ...RouterLinkStub,
+ render() {
+ const children = this.$scopedSlots.default({
+ href: '/foo',
+ isActive: false,
+ navigate: jest.fn(),
+ ...routerLinkSlotProps,
+ });
+ return children;
+ },
+ },
+ },
+ });
+ };
+
+ describe('pills', () => {
+ it.each([0, 5, 3.4, 'foo', '10%'])('item with pill_data `%p` renders a pill', (pillCount) => {
+ createWrapper({ item: { title: 'Foo', pill_count: pillCount } });
+
+ expect(findPill().text()).toEqual(pillCount.toString());
+ });
+
+ it.each([null, undefined, false, true, '', NaN, Number.POSITIVE_INFINITY])(
+ 'item with pill_data `%p` renders no pill',
+ (pillCount) => {
+ createWrapper({ item: { title: 'Foo', pill_count: pillCount } });
+
+ expect(findPill().exists()).toEqual(false);
+ },
+ );
+ });
+
+ it('applies custom link classes', () => {
+ const customClass = 'customClass';
+ createWrapper({
+ item: { title: 'Foo' },
+ props: {
+ linkClasses: {
+ [customClass]: true,
+ },
+ },
+ });
+
+ expect(findLink().attributes('class')).toContain(customClass);
+ });
+
+ it('applies custom classes set in the backend', () => {
+ const customClass = 'customBackendClass';
+ createWrapper({ item: { title: 'Foo', link_classes: customClass } });
+
+ expect(findLink().attributes('class')).toContain(customClass);
+ });
+
+ it('applies data-method specified in the backend', () => {
+ const method = 'post';
+ createWrapper({ item: { title: 'Foo', data_method: method } });
+
+ expect(findLink().attributes('data-method')).toContain(method);
+ });
+
+ describe('Data Tracking Attributes', () => {
+ it.each`
+ id | panelType | eventLabel | eventProperty | eventExtra
+ ${'abc'} | ${'xyz'} | ${'abc'} | ${'nav_panel_xyz'} | ${undefined}
+ ${undefined} | ${'xyz'} | ${TRACKING_UNKNOWN_ID} | ${'nav_panel_xyz'} | ${'{"title":"Foo"}'}
+ ${'abc'} | ${undefined} | ${'abc'} | ${TRACKING_UNKNOWN_PANEL} | ${'{"title":"Foo"}'}
+ ${undefined} | ${undefined} | ${TRACKING_UNKNOWN_ID} | ${TRACKING_UNKNOWN_PANEL} | ${'{"title":"Foo"}'}
+ `(
+ 'adds appropriate data tracking labels for id=$id and panelType=$panelType',
+ ({ id, eventLabel, panelType, eventProperty, eventExtra }) => {
+ createWrapper({ item: { title: 'Foo', id }, props: {}, provide: { panelType } });
+
+ expect(findLink().attributes('data-track-action')).toBe(CLICK_MENU_ITEM_ACTION);
+ expect(findLink().attributes('data-track-label')).toBe(eventLabel);
+ expect(findLink().attributes('data-track-property')).toBe(eventProperty);
+ expect(findLink().attributes('data-track-extra')).toBe(eventExtra);
+ },
+ );
+ });
+
+ describe('when `item` prop has `to` attribute', () => {
+ describe('when `RouterLink` is not active', () => {
+ it('renders `NavItemRouterLink` with active indicator hidden', () => {
+ createWrapper({ item: { title: 'Foo', to: { name: 'foo' } } });
+
+ expect(findNavItemRouterLink().findByTestId('active-indicator').classes()).toContain(
+ 'gl-bg-transparent',
+ );
+ });
+ });
+
+ describe('when `RouterLink` is active', () => {
+ it('renders `NavItemRouterLink` with active indicator shown', () => {
+ createWrapper({
+ item: { title: 'Foo', to: { name: 'foo' } },
+ routerLinkSlotProps: { isActive: true },
+ });
+
+ expect(findNavItemRouterLink().findByTestId('active-indicator').classes()).toContain(
+ 'gl-bg-blue-500',
+ );
+ });
+ });
+ });
+
+ describe('when `item` prop has `link` attribute', () => {
+ describe('when `item` has `is_active` set to `false`', () => {
+ it('renders `NavItemLink` with active indicator hidden', () => {
+ createWrapper({ item: { title: 'Foo', link: '/foo', is_active: false } });
+
+ expect(findNavItemLink().findByTestId('active-indicator').classes()).toContain(
+ 'gl-bg-transparent',
+ );
+ });
+ });
+
+ describe('when `item` has `is_active` set to `true`', () => {
+ it('renders `NavItemLink` with active indicator shown', () => {
+ createWrapper({ item: { title: 'Foo', link: '/foo', is_active: true } });
+
+ expect(findNavItemLink().findByTestId('active-indicator').classes()).toContain(
+ 'gl-bg-blue-500',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/pinned_section_spec.js b/spec/frontend/super_sidebar/components/pinned_section_spec.js
new file mode 100644
index 00000000000..fd6e2b7343e
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/pinned_section_spec.js
@@ -0,0 +1,75 @@
+import { nextTick } from 'vue';
+import Cookies from '~/lib/utils/cookies';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
+import NavItem from '~/super_sidebar/components/nav_item.vue';
+import { SIDEBAR_PINS_EXPANDED_COOKIE, SIDEBAR_COOKIE_EXPIRATION } from '~/super_sidebar/constants';
+import { setCookie } from '~/lib/utils/common_utils';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ getCookie: jest.requireActual('~/lib/utils/common_utils').getCookie,
+ setCookie: jest.fn(),
+}));
+
+describe('PinnedSection component', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.find('button');
+
+ const createWrapper = () => {
+ wrapper = mountExtended(PinnedSection, {
+ propsData: {
+ items: [{ title: 'Pin 1', href: '/page1' }],
+ },
+ });
+ };
+
+ describe('expanded', () => {
+ describe('when cookie is not set', () => {
+ it('is expanded by default', () => {
+ createWrapper();
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(true);
+ });
+ });
+
+ describe('when cookie is set to false', () => {
+ beforeEach(() => {
+ Cookies.set(SIDEBAR_PINS_EXPANDED_COOKIE, 'false');
+ createWrapper();
+ });
+
+ it('is collapsed', () => {
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(false);
+ });
+
+ it('updates the cookie when expanding the section', async () => {
+ findToggle().trigger('click');
+ await nextTick();
+
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_PINS_EXPANDED_COOKIE, true, {
+ expires: SIDEBAR_COOKIE_EXPIRATION,
+ });
+ });
+ });
+
+ describe('when cookie is set to true', () => {
+ beforeEach(() => {
+ Cookies.set(SIDEBAR_PINS_EXPANDED_COOKIE, 'true');
+ createWrapper();
+ });
+
+ it('is expanded', () => {
+ expect(wrapper.findComponent(NavItem).isVisible()).toBe(true);
+ });
+
+ it('updates the cookie when collapsing the section', async () => {
+ findToggle().trigger('click');
+ await nextTick();
+
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_PINS_EXPANDED_COOKIE, false, {
+ expires: SIDEBAR_COOKIE_EXPIRATION,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/projects_list_spec.js b/spec/frontend/super_sidebar/components/projects_list_spec.js
new file mode 100644
index 00000000000..93a414e1e8c
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/projects_list_spec.js
@@ -0,0 +1,85 @@
+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
new file mode 100644
index 00000000000..daec5c2a9b4
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/search_results_spec.js
@@ -0,0 +1,69 @@
+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_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
new file mode 100644
index 00000000000..9b726b620dd
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
@@ -0,0 +1,184 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
+import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
+import { PANELS_WITH_PINS } from '~/super_sidebar/constants';
+import { sidebarData } from '../mock_data';
+
+const menuItems = [
+ { id: 1, title: 'No subitems' },
+ { id: 2, title: 'With subitems', items: [{ id: 21, title: 'Pinned subitem' }] },
+ { id: 3, title: 'Empty subitems array', items: [] },
+ { id: 4, title: 'Also with subitems', items: [{ id: 41, title: 'Subitem' }] },
+];
+
+describe('SidebarMenu component', () => {
+ let wrapper;
+
+ const createWrapper = (mockData) => {
+ wrapper = mountExtended(SidebarMenu, {
+ propsData: {
+ items: mockData.current_menu_items,
+ pinnedItemIds: mockData.pinned_items,
+ panelType: mockData.panel_type,
+ updatePinsUrl: mockData.update_pins_url,
+ },
+ });
+ };
+
+ const findPinnedSection = () => wrapper.findComponent(PinnedSection);
+ const findMainMenuSeparator = () => wrapper.findByTestId('main-menu-separator');
+
+ describe('computed', () => {
+ describe('supportsPins', () => {
+ it('is true for the project sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'project' });
+ expect(wrapper.vm.supportsPins).toBe(true);
+ });
+
+ it('is true for the group sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'group' });
+ expect(wrapper.vm.supportsPins).toBe(true);
+ });
+
+ it('is false for any other sidebar', () => {
+ createWrapper({ ...sidebarData, panel_type: 'your_work' });
+ expect(wrapper.vm.supportsPins).toEqual(false);
+ });
+ });
+
+ describe('flatPinnableItems', () => {
+ it('returns all subitems in a flat array', () => {
+ createWrapper({ ...sidebarData, current_menu_items: menuItems });
+ expect(wrapper.vm.flatPinnableItems).toEqual([
+ { id: 21, title: 'Pinned subitem' },
+ { id: 41, title: 'Subitem' },
+ ]);
+ });
+ });
+
+ describe('staticItems', () => {
+ describe('when the sidebar supports pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: PANELS_WITH_PINS[0],
+ });
+ });
+
+ it('makes everything that has no subitems a static item', () => {
+ expect(wrapper.vm.staticItems.map((i) => i.title)).toEqual([
+ 'No subitems',
+ 'Empty subitems array',
+ ]);
+ });
+ });
+
+ describe('when the sidebar does not support pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: 'explore',
+ });
+ });
+
+ it('returns an empty array', () => {
+ expect(wrapper.vm.staticItems.map((i) => i.title)).toEqual([]);
+ });
+ });
+ });
+
+ describe('nonStaticItems', () => {
+ describe('when the sidebar supports pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: PANELS_WITH_PINS[0],
+ });
+ });
+
+ it('keeps items that have subitems (aka "sections") as non-static', () => {
+ expect(wrapper.vm.nonStaticItems.map((i) => i.title)).toEqual([
+ 'With subitems',
+ 'Also with subitems',
+ ]);
+ });
+ });
+
+ describe('when the sidebar does not support pins', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: 'explore',
+ });
+ });
+
+ it('keeps all items as non-static', () => {
+ expect(wrapper.vm.nonStaticItems).toEqual(menuItems);
+ });
+ });
+ });
+
+ describe('pinnedItems', () => {
+ describe('when user has no pinned item ids stored', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ pinned_items: [],
+ });
+ });
+
+ it('returns an empty array', () => {
+ expect(wrapper.vm.pinnedItems).toEqual([]);
+ });
+ });
+
+ describe('when user has some pinned item ids stored', () => {
+ beforeEach(() => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ pinned_items: [21],
+ });
+ });
+
+ it('returns the items matching the pinned ids', () => {
+ expect(wrapper.vm.pinnedItems).toEqual([{ id: 21, title: 'Pinned subitem' }]);
+ });
+ });
+ });
+ });
+
+ describe('Menu separators', () => {
+ it('should add the separator above pinned section', () => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: 'project',
+ });
+ expect(findPinnedSection().props('separated')).toBe(true);
+ });
+
+ it('should add the separator above main menu items when there is a pinned section', () => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: PANELS_WITH_PINS[0],
+ });
+ expect(findMainMenuSeparator().exists()).toBe(true);
+ });
+
+ it('should NOT add the separator above main menu items when there is no pinned section', () => {
+ createWrapper({
+ ...sidebarData,
+ current_menu_items: menuItems,
+ panel_type: 'explore',
+ });
+ expect(findMainMenuSeparator().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
new file mode 100644
index 00000000000..047dc9a6599
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
@@ -0,0 +1,207 @@
+import { mount } from '@vue/test-utils';
+import {
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+} from '~/super_sidebar/constants';
+import SidebarPeek, {
+ STATE_CLOSED,
+ STATE_WILL_OPEN,
+ STATE_OPEN,
+ STATE_WILL_CLOSE,
+} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+
+// These are measured at runtime in the browser, but statically defined here
+// since Jest does not do layout/styling.
+const X_NEAR_WINDOW_EDGE = 5;
+const X_SIDEBAR_EDGE = 10;
+const X_AWAY_FROM_SIDEBAR = 20;
+
+jest.mock('~/lib/utils/css_utils', () => ({
+ getCssClassDimensions: (className) => {
+ if (className === 'gl-w-3') {
+ return { width: X_NEAR_WINDOW_EDGE };
+ }
+
+ if (className === 'super-sidebar') {
+ return { width: X_SIDEBAR_EDGE };
+ }
+
+ throw new Error(`No mock for CSS class ${className}`);
+ },
+}));
+
+describe('SidebarPeek component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mount(SidebarPeek);
+ };
+
+ 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(() => {
+ createComponent();
+ });
+
+ it('begins in the closed state', () => {
+ expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED]);
+ });
+
+ it('does not emit duplicate events in a region', () => {
+ moveMouse(0);
+ moveMouse(1);
+ moveMouse(2);
+
+ expect(lastNChangeEvents(Infinity)).toEqual([STATE_CLOSED, STATE_WILL_OPEN]);
+ });
+
+ it('transitions to will-open when in peek region', () => {
+ moveMouse(X_NEAR_WINDOW_EDGE);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_CLOSED]);
+
+ moveMouse(X_NEAR_WINDOW_EDGE - 1);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+ });
+
+ it('transitions will-open -> open after delay', () => {
+ moveMouse(0);
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+
+ jest.advanceTimersByTime(1);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]);
+ });
+
+ it('cancels transition will-open -> open if mouse out of peek region', () => {
+ moveMouse(0);
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_OPEN_DELAY - 1);
+
+ moveMouse(X_NEAR_WINDOW_EDGE);
+
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(3)).toEqual([STATE_CLOSED, STATE_WILL_OPEN, STATE_CLOSED]);
+ });
+
+ it('transitions open -> will-close if mouse out of sidebar region', () => {
+ moveMouse(0);
+ jest.runOnlyPendingTimers();
+
+ moveMouse(X_SIDEBAR_EDGE - 1);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+
+ moveMouse(X_SIDEBAR_EDGE);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]);
+ });
+
+ it('transitions will-close -> closed after delay', () => {
+ moveMouse(0);
+ jest.runOnlyPendingTimers();
+
+ moveMouse(X_SIDEBAR_EDGE);
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]);
+
+ jest.advanceTimersByTime(1);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_CLOSE, STATE_CLOSED]);
+ });
+
+ it('cancels transition will-close -> close if mouse move in sidebar region', () => {
+ moveMouse(0);
+ jest.runOnlyPendingTimers();
+
+ moveMouse(X_SIDEBAR_EDGE);
+ jest.advanceTimersByTime(SUPER_SIDEBAR_PEEK_CLOSE_DELAY - 1);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_CLOSE]);
+
+ moveMouse(X_SIDEBAR_EDGE - 1);
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(3)).toEqual([STATE_OPEN, STATE_WILL_CLOSE, STATE_OPEN]);
+ });
+
+ it('immediately transitions open -> closed if mouse moves far away', () => {
+ moveMouse(0);
+ jest.runOnlyPendingTimers();
+
+ moveMouse(X_AWAY_FROM_SIDEBAR);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_CLOSED]);
+ });
+
+ it('immediately transitions will-close -> closed if mouse moves far away', () => {
+ moveMouse(0);
+ jest.runOnlyPendingTimers();
+
+ moveMouse(X_AWAY_FROM_SIDEBAR - 1);
+ moveMouse(X_AWAY_FROM_SIDEBAR);
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_CLOSE, STATE_CLOSED]);
+ });
+
+ it('cleans up its mousemove listener before destroy', () => {
+ moveMouse(0);
+ jest.runOnlyPendingTimers();
+
+ wrapper.destroy();
+ moveMouse(X_AWAY_FROM_SIDEBAR);
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_OPEN]);
+ });
+
+ it('cleans up its timers before destroy', () => {
+ moveMouse(0);
+
+ wrapper.destroy();
+ jest.runOnlyPendingTimers();
+
+ expect(lastNChangeEvents(1)).toEqual([STATE_WILL_OPEN]);
+ });
+
+ it('transitions will-open -> closed if cursor leaves document', () => {
+ moveMouse(0);
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_CLOSED]);
+ });
+
+ it('transitions open -> will-close if cursor leaves document', () => {
+ moveMouse(0);
+ jest.runOnlyPendingTimers();
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(2)).toEqual([STATE_OPEN, STATE_WILL_CLOSE]);
+ });
+
+ it('cleans up document mouseleave listener before destroy', () => {
+ moveMouse(0);
+
+ wrapper.destroy();
+
+ moveMouseOutOfDocument();
+
+ expect(lastNChangeEvents(1)).not.toEqual([STATE_CLOSED]);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/sidebar_portal_spec.js b/spec/frontend/super_sidebar/components/sidebar_portal_spec.js
new file mode 100644
index 00000000000..3ef1cb7e692
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/sidebar_portal_spec.js
@@ -0,0 +1,68 @@
+import { nextTick } from 'vue';
+import { mount } from '@vue/test-utils';
+import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
+import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
+
+describe('SidebarPortal', () => {
+ let targetWrapper;
+
+ const Target = {
+ components: { SidebarPortalTarget },
+ props: ['show'],
+ template: '<sidebar-portal-target v-if="show" />',
+ };
+
+ const Source = {
+ components: { SidebarPortal },
+ template: '<sidebar-portal><br data-testid="test"></sidebar-portal>',
+ };
+
+ const mountSource = () => {
+ mount(Source);
+ };
+
+ const mountTarget = ({ show = true } = {}) => {
+ targetWrapper = mount(Target, {
+ propsData: { show },
+ attachTo: document.body,
+ });
+ };
+
+ const findTestContent = () => targetWrapper.find('[data-testid="test"]');
+
+ it('renders content into the target', async () => {
+ mountTarget();
+ await nextTick();
+
+ mountSource();
+ await nextTick();
+
+ expect(findTestContent().exists()).toBe(true);
+ });
+
+ it('waits for target to be available before rendering', async () => {
+ mountSource();
+ await nextTick();
+
+ mountTarget();
+ await nextTick();
+
+ expect(findTestContent().exists()).toBe(true);
+ });
+
+ it('supports conditional rendering of target', async () => {
+ mountTarget({ show: false });
+ await nextTick();
+
+ mountSource();
+ await nextTick();
+
+ expect(findTestContent().exists()).toBe(false);
+
+ await targetWrapper.setProps({ show: true });
+ expect(findTestContent().exists()).toBe(true);
+
+ await targetWrapper.setProps({ show: false });
+ expect(findTestContent().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 45fc30c08f0..b76c637caf4 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -1,35 +1,247 @@
+import { nextTick } from 'vue';
+import { Mousetrap } from '~/lib/mousetrap';
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 { sidebarData } from '../mock_data';
+import SidebarPeekBehavior, {
+ STATE_CLOSED,
+ STATE_WILL_OPEN,
+ STATE_OPEN,
+ STATE_WILL_CLOSE,
+} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.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 {
+ toggleSuperSidebarCollapsed,
+ isCollapsed,
+} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import { stubComponent } from 'helpers/stub_component';
+import { sidebarData as mockSidebarData } from '../mock_data';
+
+const initialSidebarState = { ...sidebarState };
+
+jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager');
+const closeContextSwitcherMock = jest.fn();
+
+const trialStatusWidgetStubTestId = 'trial-status-widget';
+const TrialStatusWidgetStub = { template: `<div data-testid="${trialStatusWidgetStubTestId}" />` };
+const trialStatusPopoverStubTestId = 'trial-status-popover';
+const TrialStatusPopoverStub = {
+ template: `<div data-testid="${trialStatusPopoverStubTestId}" />`,
+};
+
+const peekClass = 'super-sidebar-peek';
+const peekHintClass = 'super-sidebar-peek-hint';
describe('SuperSidebar component', () => {
let wrapper;
+ const findSidebar = () => wrapper.findByTestId('super-sidebar');
const findUserBar = () => wrapper.findComponent(UserBar);
+ 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 findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId);
+ const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId);
+ const findSidebarMenu = () => wrapper.findComponent(SidebarMenu);
+
+ const createWrapper = ({
+ provide = {},
+ sidebarData = mockSidebarData,
+ sidebarState: state = {},
+ } = {}) => {
+ Object.assign(sidebarState, state);
- const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
+ provide: {
+ showTrialStatusWidget: false,
+ ...provide,
+ },
propsData: {
sidebarData,
- ...props,
+ },
+ stubs: {
+ ContextSwitcher: stubComponent(ContextSwitcher, {
+ methods: { close: closeContextSwitcherMock },
+ }),
+ TrialStatusWidget: TrialStatusWidgetStub,
+ TrialStatusPopover: TrialStatusPopoverStub,
},
});
};
+ beforeEach(() => {
+ Object.assign(sidebarState, initialSidebarState);
+ });
+
describe('default', () => {
- beforeEach(() => {
+ it('adds inert attribute when collapsed', () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ expect(findSidebar().attributes('inert')).toBe('inert');
+ });
+
+ it('does not add inert attribute when expanded', () => {
createWrapper();
+ expect(findSidebar().attributes('inert')).toBe(undefined);
});
it('renders UserBar with sidebarData', () => {
- expect(findUserBar().props('sidebarData')).toBe(sidebarData);
+ createWrapper();
+ expect(findUserBar().props('sidebarData')).toBe(mockSidebarData);
});
it('renders HelpCenter with sidebarData', () => {
- expect(findHelpCenter().props('sidebarData')).toBe(sidebarData);
+ createWrapper();
+ expect(findHelpCenter().props('sidebarData')).toBe(mockSidebarData);
+ });
+
+ it('does not render SidebarMenu when items are empty', () => {
+ createWrapper();
+ expect(findSidebarMenu().exists()).toBe(false);
+ });
+
+ it('renders SidebarMenu with menu items', () => {
+ const menuItems = [
+ { id: 1, title: 'Menu item 1' },
+ { id: 2, title: 'Menu item 2' },
+ ];
+ createWrapper({ sidebarData: { ...mockSidebarData, current_menu_items: menuItems } });
+ expect(findSidebarMenu().props('items')).toBe(menuItems);
+ });
+
+ it('renders SidebarPortalTarget', () => {
+ createWrapper();
+ 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;
+ const link = wrapper.find(`.${linkAttrs.css_class}`);
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(linkAttrs.href);
+ expect(link.attributes('class')).toContain('gl-display-none');
+ });
+
+ it('sets up the sidebar toggle shortcut', () => {
+ createWrapper();
+
+ isCollapsed.mockReturnValue(false);
+ Mousetrap.trigger('mod+\\');
+
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1);
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
+
+ isCollapsed.mockReturnValue(true);
+ Mousetrap.trigger('mod+\\');
+
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(2);
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
+
+ jest.spyOn(Mousetrap, 'unbind');
+
+ wrapper.destroy();
+
+ expect(Mousetrap.unbind).toHaveBeenCalledWith(['mod+\\']);
+ });
+
+ it('does not render trial status widget', () => {
+ createWrapper();
+
+ expect(findTrialStatusWidget().exists()).toBe(false);
+ expect(findTrialStatusPopover().exists()).toBe(false);
+ });
+
+ it('does not have peek behavior', () => {
+ createWrapper();
+
+ expect(findPeekBehavior().exists()).toBe(false);
+ });
+ });
+
+ describe('on collapse', () => {
+ beforeEach(() => {
+ createWrapper();
+ sidebarState.isCollapsed = true;
+ });
+
+ it('closes the context switcher', () => {
+ expect(closeContextSwitcherMock).toHaveBeenCalled();
+ });
+ });
+
+ describe('peek behavior', () => {
+ it(`initially makes sidebar inert and peekable (${STATE_CLOSED})`, () => {
+ createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
+
+ expect(findSidebar().attributes('inert')).toBe('inert');
+ expect(findSidebar().classes()).not.toContain(peekHintClass);
+ expect(findSidebar().classes()).not.toContain(peekClass);
+ });
+
+ it(`makes sidebar inert and shows peek hint when peek state is ${STATE_WILL_OPEN}`, async () => {
+ createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
+
+ findPeekBehavior().vm.$emit('change', STATE_WILL_OPEN);
+ await nextTick();
+
+ expect(findSidebar().attributes('inert')).toBe('inert');
+ expect(findSidebar().classes()).toContain(peekHintClass);
+ expect(findSidebar().classes()).not.toContain(peekClass);
+ });
+
+ it.each([STATE_OPEN, STATE_WILL_CLOSE])(
+ 'makes sidebar interactive and visible when peek state is %s',
+ async (state) => {
+ createWrapper({ sidebarState: { isCollapsed: true, isPeekable: true } });
+
+ findPeekBehavior().vm.$emit('change', state);
+ await nextTick();
+
+ expect(findSidebar().attributes('inert')).toBe(undefined);
+ expect(findSidebar().classes()).toContain(peekClass);
+ expect(findSidebar().classes()).not.toContain(peekHintClass);
+ },
+ );
+ });
+
+ describe('nav container', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('allows overflow while the context switcher is closed', () => {
+ 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', () => {
+ beforeEach(() => {
+ createWrapper({ provide: { showTrialStatusWidget: true } });
+ });
+
+ it('renders trial status widget', () => {
+ expect(findTrialStatusWidget().exists()).toBe(true);
+ expect(findTrialStatusPopover().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
new file mode 100644
index 00000000000..8bb20186e16
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -0,0 +1,106 @@
+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';
+import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS } from '~/super_sidebar/constants';
+import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue';
+import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+
+jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager.js', () => ({
+ toggleSuperSidebarCollapsed: jest.fn(),
+}));
+
+describe('SuperSidebarToggle component', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+
+ const createWrapper = ({ props = {}, sidebarState = {} } = {}) => {
+ wrapper = shallowMountExtended(SuperSidebarToggle, {
+ data() {
+ return {
+ ...sidebarState,
+ };
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('attributes', () => {
+ it('has aria-controls attribute', () => {
+ createWrapper();
+ expect(findButton().attributes('aria-controls')).toBe('super-sidebar');
+ });
+
+ it('has aria-expanded as true when expanded', () => {
+ createWrapper();
+ 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('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();
+ });
+ });
+
+ describe('toolip', () => {
+ it('displays collapse when expanded', () => {
+ createWrapper();
+ expect(getTooltip().title).toBe(__('Hide sidebar'));
+ });
+
+ it('displays expand when collapsed', () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ expect(getTooltip().title).toBe(__('Show sidebar'));
+ });
+ });
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <button class="${JS_TOGGLE_COLLAPSE_CLASS}">Hide</button>
+ <button class="${JS_TOGGLE_EXPAND_CLASS}">Show</button>
+ `);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('collapses the sidebar and focuses the other toggle', async () => {
+ createWrapper();
+ findButton().vm.$emit('click');
+ await nextTick();
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
+ expect(document.activeElement).toEqual(
+ document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`),
+ );
+ });
+
+ it('expands the sidebar and focuses the other toggle', async () => {
+ createWrapper({ sidebarState: { isCollapsed: true } });
+ findButton().vm.$emit('click');
+ await nextTick();
+ expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
+ expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`));
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index eceb792c3db..6878e724c65 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -1,28 +1,61 @@
+import { GlBadge } from '@gitlab/ui';
+import Vuex from 'vuex';
+import Vue, { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import SearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import waitForPromises from 'helpers/wait_for_promises';
+import { userCounts } from '~/super_sidebar/user_counts_manager';
import { sidebarData } from '../mock_data';
+import { MOCK_DEFAULT_SEARCH_OPTIONS } from './global_search/mock_data';
describe('UserBar component', () => {
let wrapper;
const findCreateMenu = () => wrapper.findComponent(CreateMenu);
const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
+ const findIssuesCounter = () => findCounter(0);
+ const findMRsCounter = () => findCounter(1);
+ const findTodosCounter = () => findCounter(2);
const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
+ const findBrandLogo = () => wrapper.findByTestId('brand-header-custom-logo');
+ const findCollapseButton = () => wrapper.findByTestId('super-sidebar-collapse-button');
+ const findSearchButton = () => wrapper.findByTestId('super-sidebar-search-button');
+ const findSearchModal = () => wrapper.findComponent(SearchModal);
+ const findStopImpersonationButton = () => wrapper.findByTestId('stop-impersonation-btn');
- const createWrapper = (props = {}) => {
+ Vue.use(Vuex);
+
+ const store = new Vuex.Store({
+ getters: {
+ searchOptions: () => MOCK_DEFAULT_SEARCH_OPTIONS,
+ },
+ });
+ const createWrapper = ({
+ hasCollapseButton = true,
+ extraSidebarData = {},
+ provideOverrides = {},
+ } = {}) => {
wrapper = shallowMountExtended(UserBar, {
propsData: {
- sidebarData,
- ...props,
+ hasCollapseButton,
+ sidebarData: { ...sidebarData, ...extraSidebarData },
},
provide: {
rootPath: '/',
toggleNewNavEndpoint: '/-/profile/preferences',
+ isImpersonating: false,
+ ...provideOverrides,
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
},
+ store,
});
};
@@ -40,20 +73,143 @@ describe('UserBar component', () => {
});
it('renders issues counter', () => {
- expect(findCounter(0).props('count')).toBe(sidebarData.assigned_open_issues_count);
- expect(findCounter(0).props('href')).toBe(sidebarData.issues_dashboard_path);
- expect(findCounter(0).props('label')).toBe(__('Issues'));
+ const isuesCounter = findIssuesCounter();
+ expect(isuesCounter.props('count')).toBe(userCounts.assigned_issues);
+ expect(isuesCounter.props('href')).toBe(sidebarData.issues_dashboard_path);
+ expect(isuesCounter.props('label')).toBe(__('Issues'));
+ expect(isuesCounter.attributes('data-track-action')).toBe('click_link');
+ expect(isuesCounter.attributes('data-track-label')).toBe('issues_link');
+ expect(isuesCounter.attributes('data-track-property')).toBe('nav_core_menu');
+ expect(isuesCounter.attributes('class')).toContain('dashboard-shortcuts-issues');
});
it('renders merge requests counter', () => {
- expect(findCounter(1).props('count')).toBe(sidebarData.total_merge_requests_count);
- expect(findCounter(1).props('label')).toBe(__('Merge requests'));
+ const mrsCounter = findMRsCounter();
+ expect(mrsCounter.props('count')).toBe(
+ userCounts.assigned_merge_requests + userCounts.review_requested_merge_requests,
+ );
+ expect(mrsCounter.props('label')).toBe(__('Merge requests'));
+ expect(mrsCounter.attributes('data-track-action')).toBe('click_dropdown');
+ expect(mrsCounter.attributes('data-track-label')).toBe('merge_requests_menu');
+ expect(mrsCounter.attributes('data-track-property')).toBe('nav_core_menu');
+ });
+
+ describe('Todos counter', () => {
+ it('renders it', () => {
+ const todosCounter = findTodosCounter();
+ expect(todosCounter.props('href')).toBe(sidebarData.todos_dashboard_path);
+ expect(todosCounter.props('label')).toBe(__('To-Do list'));
+ expect(todosCounter.attributes('data-track-action')).toBe('click_link');
+ expect(todosCounter.attributes('data-track-label')).toBe('todos_link');
+ expect(todosCounter.attributes('data-track-property')).toBe('nav_core_menu');
+ expect(todosCounter.attributes('class')).toContain('shortcuts-todos');
+ });
+
+ it('should update todo counter when event is emitted', async () => {
+ createWrapper();
+ const count = 100;
+ document.dispatchEvent(new CustomEvent('todo:toggle', { detail: { count } }));
+ await nextTick();
+ expect(findTodosCounter().props('count')).toBe(count);
+ });
+ });
+
+ it('renders branding logo', () => {
+ expect(findBrandLogo().exists()).toBe(true);
+ expect(findBrandLogo().attributes('src')).toBe(sidebarData.logo_url);
+ });
+
+ it('does not render the "Stop impersonating" button', () => {
+ expect(findStopImpersonationButton().exists()).toBe(false);
+ });
+
+ it('renders collapse button when hasCollapseButton is true', () => {
+ expect(findCollapseButton().exists()).toBe(true);
+ });
+
+ it('does not render collapse button when hasCollapseButton is false', () => {
+ createWrapper({ hasCollapseButton: false });
+ expect(findCollapseButton().exists()).toBe(false);
+ });
+ });
+
+ describe('GitLab Next badge', () => {
+ describe('when on canary', () => {
+ it('should render a badge to switch off GitLab Next', () => {
+ createWrapper({ extraSidebarData: { gitlab_com_and_canary: true } });
+ const badge = wrapper.findComponent(GlBadge);
+ expect(badge.text()).toBe('Next');
+ expect(badge.attributes('href')).toBe(sidebarData.canary_toggle_com_url);
+ });
+ });
+
+ describe('when not on canary', () => {
+ it('should not render the GitLab Next badge', () => {
+ createWrapper({ extraSidebarData: { gitlab_com_and_canary: false } });
+ const badge = wrapper.findComponent(GlBadge);
+ expect(badge.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Search', () => {
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
+ });
+
+ it('should render search button', () => {
+ expect(findSearchButton().exists()).toBe(true);
+ });
+
+ it('search button should have tooltip', () => {
+ const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
+ expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
+ });
+
+ it('should render search modal', () => {
+ expect(findSearchModal().exists()).toBe(true);
+ });
+
+ describe('Search tooltip', () => {
+ it('should hide search tooltip when modal is shown', async () => {
+ findSearchModal().vm.$emit('shown');
+ await nextTick();
+ const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
+ expect(tooltip.value).toBe('');
+ });
+
+ it('should add search tooltip when modal is hidden', async () => {
+ findSearchModal().vm.$emit('hidden');
+ await nextTick();
+ const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
+ expect(tooltip.value).toBe(`Search GitLab <kbd>/</kbd>`);
+ });
});
+ });
+
+ describe('While impersonating a user', () => {
+ beforeEach(() => {
+ createWrapper({ provideOverrides: { isImpersonating: true } });
+ });
+
+ it('renders the "Stop impersonating" button', () => {
+ expect(findStopImpersonationButton().exists()).toBe(true);
+ });
+
+ it('sets the correct label on the button', () => {
+ const btn = findStopImpersonationButton();
+ const label = __('Stop impersonating');
+
+ expect(btn.attributes('title')).toBe(label);
+ expect(btn.attributes('aria-label')).toBe(label);
+ });
+
+ it('sets the href and data-method attributes', () => {
+ const btn = findStopImpersonationButton();
- it('renders todos counter', () => {
- expect(findCounter(2).props('count')).toBe(sidebarData.todos_pending_count);
- expect(findCounter(2).props('href')).toBe('/dashboard/todos');
- expect(findCounter(2).props('label')).toBe(__('To-Do list'));
+ expect(btn.attributes('href')).toBe(sidebarData.stop_impersonation_path);
+ expect(btn.attributes('data-method')).toBe('delete');
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
new file mode 100644
index 00000000000..cf8f650ec8f
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -0,0 +1,502 @@
+import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserMenu from '~/super_sidebar/components/user_menu.vue';
+import UserNameGroup from '~/super_sidebar/components/user_name_group.vue';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
+import invalidUrl from '~/lib/utils/invalid_url';
+import { mockTracking } from 'helpers/tracking_helper';
+import PersistentUserCallout from '~/persistent_user_callout';
+import { userMenuMockData, userMenuMockStatus, userMenuMockPipelineMinutes } from '../mock_data';
+
+describe('UserMenu component', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const GlEmoji = { template: '<img/>' };
+ const toggleNewNavEndpoint = invalidUrl;
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const showDropdown = () => findDropdown().vm.$emit('shown');
+
+ const createWrapper = (userDataChanges = {}) => {
+ wrapper = mountExtended(UserMenu, {
+ propsData: {
+ data: {
+ ...userMenuMockData,
+ ...userDataChanges,
+ },
+ },
+ stubs: {
+ GlEmoji,
+ GlAvatar: true,
+ },
+ provide: {
+ toggleNewNavEndpoint,
+ },
+ });
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ };
+
+ it('passes popper options to the dropdown', () => {
+ createWrapper();
+
+ expect(findDropdown().props('popperOptions')).toEqual({
+ modifiers: [{ name: 'offset', options: { offset: [-211, 4] } }],
+ });
+ });
+
+ describe('Toggle button', () => {
+ let toggle;
+
+ beforeEach(() => {
+ createWrapper();
+ toggle = wrapper.findByTestId('base-dropdown-toggle');
+ });
+
+ it('renders User Avatar in a toggle', () => {
+ const avatar = toggle.findComponent(GlAvatar);
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ entityName: userMenuMockData.name,
+ src: userMenuMockData.avatar_url,
+ });
+ });
+
+ it('renders screen reader text', () => {
+ expect(toggle.find('.gl-sr-only').text()).toBe(`${userMenuMockData.name} user’s menu`);
+ });
+ });
+
+ describe('User Menu Group', () => {
+ it('renders and passes data to it', () => {
+ createWrapper();
+ const userNameGroup = wrapper.findComponent(UserNameGroup);
+ expect(userNameGroup.exists()).toBe(true);
+ expect(userNameGroup.props('user')).toEqual(userMenuMockData);
+ });
+ });
+
+ describe('User status item', () => {
+ let item;
+
+ const setItem = ({ can_update, busy, customized } = {}) => {
+ createWrapper({ status: { ...userMenuMockStatus, can_update, busy, customized } });
+ item = wrapper.findByTestId('status-item');
+ };
+
+ describe('When user cannot update the status', () => {
+ it('does not render the status menu item', () => {
+ setItem();
+ expect(item.exists()).toBe(false);
+ });
+ });
+
+ describe('When user can update the status', () => {
+ it('renders the status menu item', () => {
+ setItem({ can_update: true });
+ expect(item.exists()).toBe(true);
+ });
+
+ it('should set the CSS class for triggering status update modal', () => {
+ setItem({ can_update: true });
+ expect(item.find('.js-set-status-modal-trigger').exists()).toBe(true);
+ });
+
+ it('should close the dropdown when status modal opened', () => {
+ setItem({ can_update: true });
+ wrapper.vm.$refs.userDropdown.close = jest.fn();
+ expect(wrapper.vm.$refs.userDropdown.close).not.toHaveBeenCalled();
+ item.vm.$emit('action');
+ expect(wrapper.vm.$refs.userDropdown.close).toHaveBeenCalled();
+ });
+
+ describe('renders correct label', () => {
+ it.each`
+ busy | customized | label
+ ${false} | ${false} | ${'Set status'}
+ ${false} | ${true} | ${'Edit status'}
+ ${true} | ${false} | ${'Edit status'}
+ ${true} | ${true} | ${'Edit status'}
+ `(
+ 'when busy is "$busy" and customized is "$customized" the label is "$label"',
+ ({ busy, customized, label }) => {
+ setItem({ can_update: true, busy, customized });
+ expect(item.text()).toBe(label);
+ },
+ );
+ });
+
+ describe('Status update modal wrapper', () => {
+ const findModalWrapper = () => wrapper.find('.js-set-status-modal-wrapper');
+
+ it('renders the modal wrapper', () => {
+ setItem({ can_update: true });
+ expect(findModalWrapper().exists()).toBe(true);
+ });
+
+ describe('when user cannot update status', () => {
+ it('sets default data attributes', () => {
+ setItem({ can_update: true });
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ });
+ });
+ });
+
+ describe.each`
+ busy | customized
+ ${true} | ${true}
+ ${true} | ${false}
+ ${false} | ${true}
+ ${false} | ${false}
+ `(`when user can update status`, ({ busy, customized }) => {
+ it(`and ${busy ? 'is busy' : 'is not busy'} and status ${
+ customized ? 'is' : 'is not'
+ } customized sets user status data attributes`, () => {
+ setItem({ can_update: true, busy, customized });
+ if (busy || customized) {
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': userMenuMockStatus.emoji,
+ 'data-current-message': userMenuMockStatus.message,
+ 'data-current-availability': userMenuMockStatus.availability,
+ 'data-current-clear-status-after': userMenuMockStatus.clear_after,
+ });
+ } else {
+ expect(findModalWrapper().attributes()).toMatchObject({
+ 'data-current-emoji': '',
+ 'data-current-message': '',
+ 'data-default-emoji': 'speech_balloon',
+ });
+ }
+ });
+ });
+ });
+ });
+ });
+
+ describe('Start Ultimate trial item', () => {
+ let item;
+
+ const setItem = ({ has_start_trial } = {}) => {
+ createWrapper({ trial: { has_start_trial, url: '' } });
+ item = wrapper.findByTestId('start-trial-item');
+ };
+
+ describe('When Ultimate trial is not suggested for the user', () => {
+ it('does not render the start trial menu item', () => {
+ setItem();
+ expect(item.exists()).toBe(false);
+ });
+ });
+
+ describe('When Ultimate trial can be suggested for the user', () => {
+ it('does render the start trial menu item', () => {
+ setItem({ has_start_trial: true });
+ expect(item.exists()).toBe(true);
+ });
+ });
+
+ it('has Snowplow tracking attributes', () => {
+ setItem({ has_start_trial: true });
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'start_trial',
+ });
+ });
+
+ describe('When trial info is not provided', () => {
+ it('does not render the start trial menu item', () => {
+ createWrapper();
+
+ expect(wrapper.findByTestId('start-trial-item').exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Buy Pipeline Minutes item', () => {
+ let item;
+
+ const setItem = ({
+ show_buy_pipeline_minutes,
+ show_with_subtext,
+ show_notification_dot,
+ } = {}) => {
+ createWrapper({
+ pipeline_minutes: {
+ ...userMenuMockPipelineMinutes,
+ show_buy_pipeline_minutes,
+ show_with_subtext,
+ show_notification_dot,
+ },
+ });
+ item = wrapper.findByTestId('buy-pipeline-minutes-item');
+ };
+
+ describe('When does NOT meet the condition to buy CI minutes', () => {
+ beforeEach(() => {
+ setItem();
+ });
+
+ it('does NOT render the buy pipeline minutes item', () => {
+ expect(item.exists()).toBe(false);
+ });
+
+ it('does not track the Sentry event', () => {
+ showDropdown();
+ expect(trackingSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('When does meet the condition to buy CI minutes', () => {
+ it('does render the menu item', () => {
+ setItem({ show_buy_pipeline_minutes: true });
+ expect(item.exists()).toBe(true);
+ });
+
+ describe('Snowplow tracking attributes to track item click', () => {
+ beforeEach(() => {
+ setItem({ show_buy_pipeline_minutes: true });
+ });
+
+ it('has attributes to track item click in scope of new nav', () => {
+ expect(item.find('a').attributes()).toMatchObject({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'buy_pipeline_minutes',
+ });
+ });
+
+ it('tracks the click on the item', () => {
+ item.vm.$emit('action');
+ expect(trackingSpy).toHaveBeenCalledWith(
+ undefined,
+ userMenuMockPipelineMinutes.tracking_attrs['track-action'],
+ {
+ label: userMenuMockPipelineMinutes.tracking_attrs['track-label'],
+ property: userMenuMockPipelineMinutes.tracking_attrs['track-property'],
+ },
+ );
+ });
+ });
+
+ describe('Callout & notification dot', () => {
+ let spyFactory;
+
+ beforeEach(() => {
+ spyFactory = jest.spyOn(PersistentUserCallout, 'factory');
+ });
+
+ describe('When `show_notification_dot` is `false`', () => {
+ beforeEach(() => {
+ setItem({ show_buy_pipeline_minutes: true, show_notification_dot: false });
+ showDropdown();
+ });
+
+ it('does not set callout attributes', () => {
+ expect(item.attributes()).not.toEqual(
+ expect.objectContaining({
+ 'data-feature-id': userMenuMockPipelineMinutes.callout_attrs.feature_id,
+ 'data-dismiss-endpoint': userMenuMockPipelineMinutes.callout_attrs.dismiss_endpoint,
+ }),
+ );
+ });
+
+ it('does not initialize the Persistent Callout', () => {
+ expect(spyFactory).not.toHaveBeenCalled();
+ });
+
+ it('does not render notification dot', () => {
+ expect(wrapper.findByTestId('buy-pipeline-minutes-notification-dot').exists()).toBe(
+ false,
+ );
+ });
+ });
+
+ describe('When `show_notification_dot` is `true`', () => {
+ beforeEach(() => {
+ setItem({ show_buy_pipeline_minutes: true, show_notification_dot: true });
+ showDropdown();
+ });
+
+ it('sets the callout data attributes', () => {
+ expect(item.attributes()).toEqual(
+ expect.objectContaining({
+ 'data-feature-id': userMenuMockPipelineMinutes.callout_attrs.feature_id,
+ 'data-dismiss-endpoint': userMenuMockPipelineMinutes.callout_attrs.dismiss_endpoint,
+ }),
+ );
+ });
+
+ it('initializes the Persistent Callout', () => {
+ expect(spyFactory).toHaveBeenCalled();
+ });
+
+ it('renders notification dot', () => {
+ expect(wrapper.findByTestId('buy-pipeline-minutes-notification-dot').exists()).toBe(
+ true,
+ );
+ });
+ });
+ });
+
+ describe('Warning message', () => {
+ it('does not display the warning message when `show_with_subtext` is `false`', () => {
+ setItem({ show_buy_pipeline_minutes: true });
+
+ expect(item.text()).not.toContain(UserMenu.i18n.oneOfGroupsRunningOutOfPipelineMinutes);
+ });
+
+ it('displays the text and warning message when `show_with_subtext` is true', () => {
+ setItem({ show_buy_pipeline_minutes: true, show_with_subtext: true });
+
+ expect(item.text()).toContain(UserMenu.i18n.oneOfGroupsRunningOutOfPipelineMinutes);
+ });
+ });
+ });
+ });
+
+ describe('Edit profile item', () => {
+ let item;
+
+ beforeEach(() => {
+ createWrapper();
+ item = wrapper.findByTestId('edit-profile-item');
+ });
+
+ it('should render a link to the profile page', () => {
+ expect(item.text()).toBe(UserMenu.i18n.editProfile);
+ expect(item.find('a').attributes('href')).toBe(userMenuMockData.settings.profile_path);
+ });
+
+ 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': 'user_edit_profile',
+ });
+ });
+ });
+
+ describe('Preferences item', () => {
+ let item;
+
+ beforeEach(() => {
+ createWrapper();
+ item = wrapper.findByTestId('preferences-item');
+ });
+
+ it('should render a link to the profile page', () => {
+ expect(item.text()).toBe(UserMenu.i18n.preferences);
+ expect(item.find('a').attributes('href')).toBe(
+ userMenuMockData.settings.profile_preferences_path,
+ );
+ });
+
+ 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': 'user_preferences',
+ });
+ });
+ });
+
+ describe('GitLab Next item', () => {
+ describe('on gitlab.com', () => {
+ let item;
+
+ beforeEach(() => {
+ createWrapper({ gitlab_com_but_not_canary: true });
+ item = wrapper.findByTestId('gitlab-next-item');
+ });
+ it('should render a link to switch to GitLab Next', () => {
+ expect(item.text()).toBe(UserMenu.i18n.gitlabNext);
+ expect(item.find('a').attributes('href')).toBe(userMenuMockData.canary_toggle_com_url);
+ });
+
+ 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': 'switch_to_canary',
+ });
+ });
+ });
+
+ describe('anywhere else', () => {
+ it('should not render the GitLab Next link', () => {
+ createWrapper({ gitlab_com_but_not_canary: false });
+ const item = wrapper.findByTestId('gitlab-next-item');
+ expect(item.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('New navigation toggle item', () => {
+ it('should render menu item with new navigation toggle', () => {
+ createWrapper();
+ const toggleItem = wrapper.findComponent(NewNavToggle);
+ expect(toggleItem.exists()).toBe(true);
+ expect(toggleItem.props('endpoint')).toBe(toggleNewNavEndpoint);
+ });
+ });
+
+ 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');
+
+ it('should not render sign out group when user cannot sign out', () => {
+ createWrapper();
+ expect(findSignOutGroup().exists()).toBe(false);
+ });
+
+ describe('when user can sign out', () => {
+ beforeEach(() => {
+ createWrapper({ can_sign_out: true });
+ });
+
+ it('should render sign out group', () => {
+ expect(findSignOutGroup().exists()).toBe(true);
+ });
+
+ it('should render the menu item with a link to sign out and correct data attribute', () => {
+ expect(findSignOutGroup().find('a').attributes('href')).toBe(
+ userMenuMockData.sign_out_link,
+ );
+ expect(findSignOutGroup().find('a').attributes('data-method')).toBe('post');
+ });
+
+ it('should track Snowplow event on sign out', () => {
+ findSignOutGroup().vm.$emit('action');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
+ label: 'user_sign_out',
+ property: 'nav_user_menu',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js
new file mode 100644
index 00000000000..6e3b18d3107
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js
@@ -0,0 +1,114 @@
+import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import UserNameGroup from '~/super_sidebar/components/user_name_group.vue';
+import { userMenuMockData, userMenuMockStatus } from '../mock_data';
+
+describe('UserNameGroup component', () => {
+ let wrapper;
+
+ const findGlDisclosureDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
+ const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+ const findGlTooltip = () => wrapper.findComponent(GlTooltip);
+ const findUserStatus = () => wrapper.findByTestId('user-menu-status');
+
+ const GlEmoji = { template: '<img/>' };
+
+ const createWrapper = (userDataChanges = {}) => {
+ wrapper = shallowMountExtended(UserNameGroup, {
+ propsData: {
+ user: {
+ ...userMenuMockData,
+ ...userDataChanges,
+ },
+ },
+ stubs: {
+ GlEmoji,
+ GlDisclosureDropdownItem,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders the menu item in a separate group', () => {
+ expect(findGlDisclosureDropdownGroup().exists()).toBe(true);
+ });
+
+ it('renders menu item', () => {
+ expect(findGlDisclosureDropdownItem().exists()).toBe(true);
+ });
+
+ it('passes the item to the disclosure dropdown item', () => {
+ expect(findGlDisclosureDropdownItem().props('item')).toEqual(
+ expect.objectContaining({
+ text: userMenuMockData.name,
+ href: userMenuMockData.link_to_profile,
+ }),
+ );
+ });
+
+ it("renders user's name", () => {
+ expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.name);
+ });
+
+ it("renders user's username", () => {
+ expect(findGlDisclosureDropdownItem().text()).toContain(userMenuMockData.username);
+ });
+
+ describe('Busy status', () => {
+ it('should not render "Busy" when user is NOT busy', () => {
+ expect(findGlDisclosureDropdownItem().text()).not.toContain('Busy');
+ });
+ it('should render "Busy" when user is busy', () => {
+ createWrapper({ status: { customized: true, busy: true } });
+
+ expect(findGlDisclosureDropdownItem().text()).toContain('Busy');
+ });
+ });
+
+ describe('User status', () => {
+ describe('when not customized', () => {
+ it('should not render it', () => {
+ expect(findUserStatus().exists()).toBe(false);
+ });
+ });
+
+ describe('when customized', () => {
+ beforeEach(() => {
+ createWrapper({ status: { ...userMenuMockStatus, customized: true } });
+ });
+
+ it('should render it', () => {
+ expect(findUserStatus().exists()).toBe(true);
+ });
+
+ it('should render status emoji', () => {
+ expect(findUserStatus().findComponent(GlEmoji).attributes('data-name')).toBe(
+ userMenuMockData.status.emoji,
+ );
+ });
+
+ it('should render status message', () => {
+ expect(findUserStatus().text()).toContain(userMenuMockData.status.message);
+ });
+
+ it("sets the tooltip's target to the status container", () => {
+ expect(findGlTooltip().props('target')?.()).toBe(findUserStatus().element);
+ });
+ });
+ });
+
+ describe('Tracking', () => {
+ it('sets the tracking attributes', () => {
+ expect(findGlDisclosureDropdownItem().find('a').attributes()).toEqual(
+ expect.objectContaining({
+ 'data-track-property': 'nav_user_menu',
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'user_profile',
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 91a2dc93a47..a3a74f7aac8 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -1,3 +1,5 @@
+import invalidUrl from '~/lib/utils/invalid_url';
+
export const createNewMenuGroups = [
{
name: 'This group',
@@ -16,7 +18,7 @@ export const createNewMenuGroups = [
},
{
text: 'Invite members',
- href: '/groups/gitlab-org/-/group_members',
+ component: 'invite_members',
},
],
},
@@ -47,26 +49,51 @@ export const mergeRequestMenuGroup = [
text: 'Assigned',
href: '/dashboard/merge_requests?assignee_username=root',
count: 4,
+ extraAttrs: {
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'merge_requests_assigned',
+ 'data-track-property': 'nav_core_menu',
+ class: 'dashboard-shortcuts-merge_requests',
+ },
},
{
text: 'Review requests',
href: '/dashboard/merge_requests?reviewer_username=root',
count: 0,
+ extraAttrs: {
+ 'data-track-action': 'click_link',
+ 'data-track-label': 'merge_requests_to_review',
+ 'data-track-property': 'nav_core_menu',
+ class: 'dashboard-shortcuts-review_requests',
+ },
},
],
},
];
export const sidebarData = {
+ current_menu_items: [],
+ current_context_header: {
+ title: 'Your Work',
+ icon: 'work',
+ },
name: 'Administrator',
username: 'root',
avatar_url: 'path/to/img_administrator',
- assigned_open_issues_count: 1,
- todos_pending_count: 3,
+ logo_url: 'path/to/logo',
+ user_counts: {
+ last_update: Date.now(),
+ todos: 3,
+ assigned_issues: 1,
+ assigned_merge_requests: 3,
+ review_requested_merge_requests: 1,
+ },
issues_dashboard_path: 'path/to/issues',
- total_merge_requests_count: 4,
+ todos_dashboard_path: 'path/to/todos',
create_new_menu_groups: createNewMenuGroups,
merge_request_menu: mergeRequestMenuGroup,
+ projects_path: 'path/to/projects',
+ groups_path: 'path/to/groups',
support_path: '/support',
display_whats_new: true,
whats_new_most_recent_release_items_count: 5,
@@ -74,4 +101,193 @@ export const sidebarData = {
show_version_check: false,
gitlab_version: { major: 16, minor: 0 },
gitlab_version_check: { severity: 'success' },
+ gitlab_com_and_canary: false,
+ canary_toggle_com_url: 'https://next.gitlab.com',
+ context_switcher_links: [],
+ search: {
+ search_path: '/search',
+ },
+ pinned_items: [],
+ panel_type: 'your_work',
+ update_pins_url: 'path/to/pins',
+ stop_impersonation_path: '/admin/impersonation',
+ shortcut_links: [
+ {
+ title: 'Shortcut link',
+ href: '/shortcut-link',
+ css_class: 'shortcut-link-class',
+ },
+ ],
+};
+
+export const userMenuMockStatus = {
+ can_update: false,
+ busy: false,
+ customized: false,
+ emoji: 'art',
+ message: 'Working on user menu in super sidebar',
+ availability: 'busy',
+ clear_after: '2023-02-09 20:06:35 UTC',
+};
+
+export const userMenuMockPipelineMinutes = {
+ show_buy_pipeline_minutes: false,
+ show_notification_dot: false,
+ callout_attrs: {
+ feature_id: 'pipeline_minutes',
+ dismiss_endpoint: '/-/dismiss',
+ },
+ buy_pipeline_minutes_path: '/buy/pipeline_minutes',
+ tracking_attrs: {
+ 'track-action': 'trackAction',
+ 'track-label': 'label',
+ 'track-property': 'property',
+ },
+};
+
+export const userMenuMockData = {
+ name: 'Orange Fox',
+ username: 'thefox',
+ avatar_url: invalidUrl,
+ has_link_to_profile: true,
+ link_to_profile: '/thefox',
+ status: userMenuMockStatus,
+ settings: {
+ profile_path: invalidUrl,
+ profile_preferences_path: invalidUrl,
+ },
+ pipeline_minutes: userMenuMockPipelineMinutes,
+ can_sign_out: false,
+ sign_out_link: invalidUrl,
+ gitlab_com_but_not_canary: true,
+ canary_toggle_com_url: 'https://next.gitlab.com',
+};
+
+export const cachedFrequentProjects = JSON.stringify([
+ {
+ id: 1,
+ name: 'Cached project 1',
+ namespace: 'Cached Namespace 1 / Cached project 1',
+ webUrl: '/cached-namespace-1/cached-project-1',
+ avatarUrl: '/uploads/-/avatar1.png',
+ lastAccessedOn: 1676325329054,
+ frequency: 10,
+ },
+ {
+ id: 2,
+ name: 'Cached project 2',
+ namespace: 'Cached Namespace 2 / Cached project 2',
+ webUrl: '/cached-namespace-2/cached-project-2',
+ avatarUrl: '/uploads/-/avatar2.png',
+ lastAccessedOn: 1674314684308,
+ frequency: 8,
+ },
+ {
+ id: 3,
+ name: 'Cached project 3',
+ namespace: 'Cached Namespace 3 / Cached project 3',
+ webUrl: '/cached-namespace-3/cached-project-3',
+ avatarUrl: '/uploads/-/avatar3.png',
+ lastAccessedOn: 1664977333191,
+ frequency: 12,
+ },
+ {
+ id: 4,
+ name: 'Cached project 4',
+ namespace: 'Cached Namespace 4 / Cached project 4',
+ webUrl: '/cached-namespace-4/cached-project-4',
+ avatarUrl: '/uploads/-/avatar4.png',
+ lastAccessedOn: 1674315407569,
+ frequency: 3,
+ },
+ {
+ id: 5,
+ name: 'Cached project 5',
+ namespace: 'Cached Namespace 5 / Cached project 5',
+ webUrl: '/cached-namespace-5/cached-project-5',
+ avatarUrl: '/uploads/-/avatar5.png',
+ lastAccessedOn: 1677084729436,
+ frequency: 21,
+ },
+ {
+ id: 6,
+ name: 'Cached project 6',
+ namespace: 'Cached Namespace 6 / Cached project 6',
+ webUrl: '/cached-namespace-6/cached-project-6',
+ avatarUrl: '/uploads/-/avatar6.png',
+ lastAccessedOn: 1676325329679,
+ frequency: 5,
+ },
+]);
+
+export const cachedFrequentGroups = JSON.stringify([
+ {
+ id: 1,
+ name: 'Cached group 1',
+ namespace: 'Cached Namespace 1',
+ webUrl: '/cached-namespace-1/cached-group-1',
+ avatarUrl: '/uploads/-/avatar1.png',
+ lastAccessedOn: 1676325329054,
+ frequency: 10,
+ },
+ {
+ id: 2,
+ name: 'Cached group 2',
+ namespace: 'Cached Namespace 2',
+ webUrl: '/cached-namespace-2/cached-group-2',
+ avatarUrl: '/uploads/-/avatar2.png',
+ lastAccessedOn: 1674314684308,
+ frequency: 8,
+ },
+ {
+ id: 3,
+ name: 'Cached group 3',
+ namespace: 'Cached Namespace 3',
+ webUrl: '/cached-namespace-3/cached-group-3',
+ avatarUrl: '/uploads/-/avatar3.png',
+ lastAccessedOn: 1664977333191,
+ frequency: 12,
+ },
+ {
+ id: 4,
+ name: 'Cached group 4',
+ namespace: 'Cached Namespace 4',
+ webUrl: '/cached-namespace-4/cached-group-4',
+ avatarUrl: '/uploads/-/avatar4.png',
+ lastAccessedOn: 1674315407569,
+ 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/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
new file mode 100644
index 00000000000..909f4249e28
--- /dev/null
+++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
@@ -0,0 +1,139 @@
+import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
+import { getCookie, setCookie } from '~/lib/utils/common_utils';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { sidebarState } from '~/super_sidebar/constants';
+import {
+ SIDEBAR_COLLAPSED_CLASS,
+ SIDEBAR_COLLAPSED_COOKIE,
+ SIDEBAR_COLLAPSED_COOKIE_EXPIRATION,
+ toggleSuperSidebarCollapsed,
+ initSuperSidebarCollapsedState,
+ findPage,
+ bindSuperSidebarCollapsedEvents,
+} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+
+const { xl, sm } = breakpoints;
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ getCookie: jest.fn(),
+ setCookie: jest.fn(),
+}));
+
+const pageHasCollapsedClass = (hasClass) => {
+ if (hasClass) {
+ expect(findPage().classList).toContain(SIDEBAR_COLLAPSED_CLASS);
+ } else {
+ expect(findPage().classList).not.toContain(SIDEBAR_COLLAPSED_CLASS);
+ }
+};
+
+describe('Super Sidebar Collapsed State Manager', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <div class="page-with-super-sidebar">
+ <aside class="super-sidebar"></aside>
+ </div>
+ `);
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ describe('toggleSuperSidebarCollapsed', () => {
+ it.each`
+ collapsed | saveCookie | windowWidth | hasClass | superSidebarPeek | isPeekable
+ ${true} | ${true} | ${xl} | ${true} | ${false} | ${false}
+ ${true} | ${true} | ${xl} | ${true} | ${true} | ${true}
+ ${true} | ${false} | ${xl} | ${true} | ${false} | ${false}
+ ${true} | ${true} | ${sm} | ${true} | ${false} | ${false}
+ ${true} | ${false} | ${sm} | ${true} | ${false} | ${false}
+ ${false} | ${true} | ${xl} | ${false} | ${false} | ${false}
+ ${false} | ${true} | ${xl} | ${false} | ${true} | ${false}
+ ${false} | ${false} | ${xl} | ${false} | ${false} | ${false}
+ ${false} | ${true} | ${sm} | ${false} | ${false} | ${false}
+ ${false} | ${false} | ${sm} | ${false} | ${false} | ${false}
+ `(
+ 'when collapsed is $collapsed, saveCookie is $saveCookie, and windowWidth is $windowWidth then page class contains `page-with-super-sidebar-collapsed` is $hasClass',
+ ({ collapsed, saveCookie, windowWidth, hasClass, superSidebarPeek, isPeekable }) => {
+ jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth);
+ gon.features = { superSidebarPeek };
+
+ toggleSuperSidebarCollapsed(collapsed, saveCookie);
+
+ pageHasCollapsedClass(hasClass);
+ expect(sidebarState.isCollapsed).toBe(collapsed);
+ expect(sidebarState.isPeekable).toBe(isPeekable);
+
+ if (saveCookie && windowWidth >= xl) {
+ expect(setCookie).toHaveBeenCalledWith(SIDEBAR_COLLAPSED_COOKIE, collapsed, {
+ expires: SIDEBAR_COLLAPSED_COOKIE_EXPIRATION,
+ });
+ } else {
+ expect(setCookie).not.toHaveBeenCalled();
+ }
+ },
+ );
+ });
+
+ describe('initSuperSidebarCollapsedState', () => {
+ it.each`
+ windowWidth | cookie | hasClass
+ ${xl} | ${undefined} | ${false}
+ ${sm} | ${undefined} | ${true}
+ ${xl} | ${'true'} | ${true}
+ ${sm} | ${'true'} | ${true}
+ `(
+ 'sets page class to `page-with-super-sidebar-collapsed` when windowWidth is $windowWidth and cookie value is $cookie',
+ ({ windowWidth, cookie, hasClass }) => {
+ jest.spyOn(bp, 'windowWidth').mockReturnValue(windowWidth);
+ getCookie.mockReturnValue(cookie);
+
+ initSuperSidebarCollapsedState();
+
+ pageHasCollapsedClass(hasClass);
+ expect(setCookie).not.toHaveBeenCalled();
+ },
+ );
+
+ it('does not collapse sidebar when forceDesktopExpandedSidebar is true and windowWidth is xl', () => {
+ jest.spyOn(bp, 'windowWidth').mockReturnValue(xl);
+ initSuperSidebarCollapsedState(true);
+ expect(findPage().classList).not.toContain(SIDEBAR_COLLAPSED_CLASS);
+ });
+ });
+
+ describe('bindSuperSidebarCollapsedEvents', () => {
+ describe('handles width change', () => {
+ let removeEventListener;
+
+ afterEach(() => {
+ removeEventListener();
+ });
+
+ it.each`
+ initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize
+ ${xl} | ${sm} | ${false} | ${true}
+ ${sm} | ${xl} | ${true} | ${false}
+ ${xl} | ${xl} | ${false} | ${false}
+ ${sm} | ${sm} | ${true} | ${true}
+ `(
+ 'when changing width from $initialWindowWidth to $updatedWindowWidth expect page to have collapsed class before resize to be $hasClassBeforeResize and after resize to be $hasClassAfterResize',
+ ({ initialWindowWidth, updatedWindowWidth, hasClassBeforeResize, hasClassAfterResize }) => {
+ getCookie.mockReturnValue(undefined);
+ window.innerWidth = initialWindowWidth;
+ initSuperSidebarCollapsedState();
+
+ pageHasCollapsedClass(hasClassBeforeResize);
+
+ removeEventListener = bindSuperSidebarCollapsedEvents();
+
+ window.innerWidth = updatedWindowWidth;
+ window.dispatchEvent(new Event('resize'));
+
+ pageHasCollapsedClass(hasClassAfterResize);
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/user_counts_manager_spec.js b/spec/frontend/super_sidebar/user_counts_manager_spec.js
new file mode 100644
index 00000000000..b5074620195
--- /dev/null
+++ b/spec/frontend/super_sidebar/user_counts_manager_spec.js
@@ -0,0 +1,166 @@
+import waitForPromises from 'helpers/wait_for_promises';
+
+import * as UserApi from '~/api/user_api';
+import {
+ createUserCountsManager,
+ userCounts,
+ destroyUserCountsManager,
+} from '~/super_sidebar/user_counts_manager';
+
+jest.mock('~/api');
+
+const USER_ID = 123;
+const userCountDefaults = {
+ todos: 1,
+ assigned_issues: 2,
+ assigned_merge_requests: 3,
+ review_requested_merge_requests: 4,
+};
+
+const userCountUpdate = {
+ todos: 123,
+ assigned_issues: 456,
+ assigned_merge_requests: 789,
+ review_requested_merge_requests: 101112,
+};
+
+describe('User Merge Requests', () => {
+ let channelMock;
+ let newBroadcastChannelMock;
+
+ beforeEach(() => {
+ jest.spyOn(document, 'removeEventListener');
+ jest.spyOn(document, 'addEventListener');
+
+ global.gon.current_user_id = USER_ID;
+
+ channelMock = {
+ postMessage: jest.fn(),
+ close: jest.fn(),
+ };
+ newBroadcastChannelMock = jest.fn().mockImplementation(() => channelMock);
+
+ Object.assign(userCounts, userCountDefaults, { last_update: 0 });
+
+ global.BroadcastChannel = newBroadcastChannelMock;
+ });
+
+ describe('createUserCountsManager', () => {
+ beforeEach(() => {
+ createUserCountsManager();
+ });
+
+ it('creates BroadcastChannel which updates counts on message received', () => {
+ expect(newBroadcastChannelMock).toHaveBeenCalledWith(`user_counts_${USER_ID}`);
+ });
+
+ it('closes BroadCastchannel if called while already open', () => {
+ expect(channelMock.close).not.toHaveBeenCalled();
+
+ createUserCountsManager();
+
+ expect(channelMock.close).toHaveBeenCalled();
+ });
+
+ describe('BroadcastChannel onmessage handler', () => {
+ it('updates counts on message received', () => {
+ expect(userCounts).toMatchObject(userCountDefaults);
+
+ channelMock.onmessage({ data: { ...userCountUpdate, last_update: Date.now() } });
+
+ expect(userCounts).toMatchObject(userCountUpdate);
+ });
+
+ it('ignores updates with older data', () => {
+ expect(userCounts).toMatchObject(userCountDefaults);
+ userCounts.last_update = Date.now();
+
+ channelMock.onmessage({
+ data: { ...userCountUpdate, last_update: userCounts.last_update - 1000 },
+ });
+
+ expect(userCounts).toMatchObject(userCountDefaults);
+ });
+
+ it('ignores unknown fields', () => {
+ expect(userCounts).toMatchObject(userCountDefaults);
+
+ channelMock.onmessage({ data: { ...userCountUpdate, i_am_unknown: 5 } });
+
+ expect(userCounts).toMatchObject(userCountUpdate);
+ expect(userCounts.i_am_unknown).toBeUndefined();
+ });
+ });
+
+ it('broadcasts user counts during initialization', () => {
+ expect(channelMock.postMessage).toHaveBeenCalledWith(
+ expect.objectContaining(userCountDefaults),
+ );
+ });
+
+ it('setups event listener without leaking them', () => {
+ expect(document.removeEventListener).toHaveBeenCalledWith(
+ 'userCounts:fetch',
+ expect.any(Function),
+ );
+ expect(document.addEventListener).toHaveBeenCalledWith(
+ 'userCounts:fetch',
+ expect.any(Function),
+ );
+ });
+ });
+
+ describe('Event listener userCounts:fetch', () => {
+ beforeEach(() => {
+ jest.spyOn(UserApi, 'getUserCounts').mockResolvedValue({
+ data: { ...userCountUpdate, merge_requests: 'FOO' },
+ });
+ createUserCountsManager();
+ });
+
+ it('fetches counts from API, stores and rebroadcasts them', async () => {
+ expect(userCounts).toMatchObject(userCountDefaults);
+
+ document.dispatchEvent(new CustomEvent('userCounts:fetch'));
+ await waitForPromises();
+
+ expect(UserApi.getUserCounts).toHaveBeenCalled();
+ expect(userCounts).toMatchObject(userCountUpdate);
+ expect(channelMock.postMessage).toHaveBeenLastCalledWith(userCounts);
+ });
+ });
+
+ describe('destroyUserCountsManager', () => {
+ it('unregisters event handler', () => {
+ expect(document.removeEventListener).not.toHaveBeenCalledWith();
+
+ destroyUserCountsManager();
+
+ expect(document.removeEventListener).toHaveBeenCalledWith(
+ 'userCounts:fetch',
+ expect.any(Function),
+ );
+ });
+
+ describe('when BroadcastChannel is not opened', () => {
+ it('does nothing', () => {
+ destroyUserCountsManager();
+ expect(channelMock.close).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when BroadcastChannel is opened', () => {
+ beforeEach(() => {
+ createUserCountsManager();
+ });
+
+ it('closes BroadcastChannel', () => {
+ expect(channelMock.close).not.toHaveBeenCalled();
+
+ destroyUserCountsManager();
+
+ expect(channelMock.close).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js
new file mode 100644
index 00000000000..8c8673ddbc4
--- /dev/null
+++ b/spec/frontend/super_sidebar/utils_spec.js
@@ -0,0 +1,171 @@
+import {
+ getTopFrequentItems,
+ trackContextAccess,
+ formatContextSwitcherItems,
+ ariaCurrent,
+} from '~/super_sidebar/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 { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data';
+import { searchUserProjectsAndGroupsResponseMock } from './mock_data';
+
+describe('Super sidebar utils spec', () => {
+ describe('getTopFrequentItems', () => {
+ const maxItems = 3;
+
+ it.each([undefined, null])('returns empty array if `items` is %s', (items) => {
+ const result = getTopFrequentItems(items);
+
+ expect(result.length).toBe(0);
+ });
+
+ it('returns the requested amount of items', () => {
+ const result = getTopFrequentItems(unsortedFrequentItems, maxItems);
+
+ expect(result.length).toBe(maxItems);
+ });
+
+ it('sorts frequent items in order of frequency and lastAccessedOn', () => {
+ const result = getTopFrequentItems(unsortedFrequentItems, maxItems);
+ const expectedResult = sortedFrequentItems.slice(0, maxItems);
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('trackContextAccess', () => {
+ useLocalStorageSpy();
+
+ const username = 'root';
+ const context = {
+ namespace: 'groups',
+ item: { id: 1 },
+ };
+ const storageKey = `${username}/frequent-${context.namespace}`;
+
+ it('returns `false` if local storage is not available', () => {
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
+
+ expect(trackContextAccess()).toBe(false);
+ });
+
+ it('creates a new item if it does not exist in the local storage', () => {
+ trackContextAccess(username, context);
+
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(
+ storageKey,
+ JSON.stringify([
+ {
+ id: 1,
+ frequency: 1,
+ lastAccessedOn: Date.now(),
+ },
+ ]),
+ );
+ });
+
+ it('updates existing item if it was persisted to the local storage over 15 minutes ago', () => {
+ window.localStorage.setItem(
+ storageKey,
+ JSON.stringify([
+ {
+ id: 1,
+ frequency: 2,
+ lastAccessedOn: Date.now() - FIFTEEN_MINUTES_IN_MS - 1,
+ },
+ ]),
+ );
+ trackContextAccess(username, context);
+
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(
+ storageKey,
+ JSON.stringify([
+ {
+ id: 1,
+ frequency: 3,
+ lastAccessedOn: Date.now(),
+ },
+ ]),
+ );
+ });
+
+ it('leaves item as is if it was persisted to the local storage under 15 minutes ago', () => {
+ const jsonString = JSON.stringify([
+ {
+ id: 1,
+ frequency: 2,
+ lastAccessedOn: Date.now() - FIFTEEN_MINUTES_IN_MS,
+ },
+ ]);
+ window.localStorage.setItem(storageKey, jsonString);
+
+ expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(storageKey, jsonString);
+
+ trackContextAccess(username, context);
+
+ expect(window.localStorage.setItem).toHaveBeenCalledTimes(3);
+ expect(window.localStorage.setItem).toHaveBeenLastCalledWith(storageKey, jsonString);
+ });
+
+ it('replaces the least popular item in the local storage once the persisted items limit has been hit', () => {
+ // Add the maximum amount of items to the local storage, in increasing popularity
+ const storedItems = Array.from({ length: FREQUENT_ITEMS.MAX_COUNT }).map((_, i) => ({
+ id: i + 1,
+ frequency: i + 1,
+ lastAccessedOn: Date.now(),
+ }));
+ // The first item is considered the least popular one as it has the lowest frequency (1)
+ const [leastPopularItem] = storedItems;
+ // Persist the list to the local storage
+ const jsonString = JSON.stringify(storedItems);
+ window.localStorage.setItem(storageKey, jsonString);
+ // Track some new item that hasn't been visited yet
+ const newItem = {
+ id: FREQUENT_ITEMS.MAX_COUNT + 1,
+ };
+ trackContextAccess(username, {
+ namespace: 'groups',
+ item: newItem,
+ });
+ // Finally, retrieve the final data from the local storage
+ const finallyStoredItems = JSON.parse(window.localStorage.getItem(storageKey));
+
+ expect(finallyStoredItems).not.toEqual(expect.arrayContaining([leastPopularItem]));
+ expect(finallyStoredItems).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: newItem.id,
+ frequency: 1,
+ }),
+ ]),
+ );
+ });
+ });
+
+ 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('ariaCurrent', () => {
+ it.each`
+ isActive | expected
+ ${true} | ${'page'}
+ ${false} | ${null}
+ `('returns `$expected` when `isActive` is `$isActive`', ({ isActive, expected }) => {
+ expect(ariaCurrent(isActive)).toBe(expected);
+ });
+ });
+});
diff --git a/spec/frontend/surveys/merge_request_performance/app_spec.js b/spec/frontend/surveys/merge_request_performance/app_spec.js
index af91d8aeb6b..d03451c71a8 100644
--- a/spec/frontend/surveys/merge_request_performance/app_spec.js
+++ b/spec/frontend/surveys/merge_request_performance/app_spec.js
@@ -56,17 +56,17 @@ describe('MergeRequestExperienceSurveyApp', () => {
createWrapper();
});
- it('shows survey', async () => {
+ it('shows survey', () => {
expect(wrapper.html()).toContain('Overall, how satisfied are you with merge requests?');
expect(wrapper.findComponent(SatisfactionRate).exists()).toBe(true);
expect(wrapper.emitted().close).toBe(undefined);
});
- it('tracks render once', async () => {
+ it('tracks render once', () => {
expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
});
- it("doesn't track subsequent renders", async () => {
+ it("doesn't track subsequent renders", () => {
createWrapper();
expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments());
expect(trackingSpy).toHaveBeenCalledTimes(1);
@@ -77,15 +77,15 @@ describe('MergeRequestExperienceSurveyApp', () => {
findCloseButton().vm.$emit('click');
});
- it('triggers user callout on close', async () => {
+ it('triggers user callout on close', () => {
expect(dismiss).toHaveBeenCalledTimes(1);
});
- it('emits close event on close button click', async () => {
+ it('emits close event on close button click', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
});
- it('tracks dismissal', async () => {
+ it('tracks dismissal', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
label: 'dismiss',
extra: {
@@ -94,7 +94,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
});
});
- it('tracks subsequent renders', async () => {
+ it('tracks subsequent renders', () => {
createWrapper();
expect(trackingSpy.mock.calls).toEqual([
createRenderTrackedArguments(),
@@ -110,7 +110,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
);
});
- it('dismisses user callout on survey rate', async () => {
+ it('dismisses user callout on survey rate', () => {
const rate = wrapper.findComponent(SatisfactionRate);
expect(dismiss).not.toHaveBeenCalled();
rate.vm.$emit('rate', 5);
@@ -126,7 +126,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
);
});
- it('tracks survey rates', async () => {
+ it('tracks survey rates', () => {
const rate = wrapper.findComponent(SatisfactionRate);
rate.vm.$emit('rate', 5);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
@@ -146,7 +146,7 @@ describe('MergeRequestExperienceSurveyApp', () => {
});
});
- it('shows legal note', async () => {
+ it('shows legal note', () => {
expect(wrapper.text()).toContain(
'By continuing, you acknowledge that responses will be used to improve GitLab and in accordance with the GitLab Privacy Policy.',
);
@@ -179,11 +179,11 @@ describe('MergeRequestExperienceSurveyApp', () => {
createWrapper({ shouldShowCallout: false });
});
- it('emits close event', async () => {
+ it('emits close event', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
});
- it("doesn't track anything", async () => {
+ it("doesn't track anything", () => {
expect(trackingSpy).toHaveBeenCalledTimes(0);
});
});
@@ -195,12 +195,12 @@ describe('MergeRequestExperienceSurveyApp', () => {
document.dispatchEvent(event);
});
- it('emits close event', async () => {
+ it('emits close event', () => {
expect(wrapper.emitted()).toMatchObject({ close: [[]] });
expect(dismiss).toHaveBeenCalledTimes(1);
});
- it('tracks dismissal', async () => {
+ it('tracks dismissal', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', {
label: 'dismiss',
extra: {
diff --git a/spec/frontend/syntax_highlight_spec.js b/spec/frontend/syntax_highlight_spec.js
index 1be6c213350..a573c37ca44 100644
--- a/spec/frontend/syntax_highlight_spec.js
+++ b/spec/frontend/syntax_highlight_spec.js
@@ -1,14 +1,10 @@
-/* eslint-disable no-return-assign */
import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import syntaxHighlight from '~/syntax_highlight';
describe('Syntax Highlighter', () => {
const stubUserColorScheme = (value) => {
- if (window.gon == null) {
- window.gon = {};
- }
- return (window.gon.user_color_scheme = value);
+ window.gon.user_color_scheme = value;
};
// We have to bind `document.querySelectorAll` to `document` to not mess up the fn's context
diff --git a/spec/frontend/tags/components/delete_tag_modal_spec.js b/spec/frontend/tags/components/delete_tag_modal_spec.js
index b1726a2c0ef..8ec9925563a 100644
--- a/spec/frontend/tags/components/delete_tag_modal_spec.js
+++ b/spec/frontend/tags/components/delete_tag_modal_spec.js
@@ -44,10 +44,6 @@ const findFormInput = () => wrapper.findComponent(GlFormInput);
const findForm = () => wrapper.find('form');
describe('Delete tag modal', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Deleting a regular tag', () => {
const expectedTitle = 'Delete tag. Are you ABSOLUTELY SURE?';
const expectedMessage = "You're about to permanently delete the tag test-tag.";
@@ -73,7 +69,7 @@ describe('Delete tag modal', () => {
expect(submitFormSpy).toHaveBeenCalled();
});
- it('calls show on the modal when a `openModal` event is received through the event hub', async () => {
+ it('calls show on the modal when a `openModal` event is received through the event hub', () => {
const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show');
eventHub.$emit('openModal', {
diff --git a/spec/frontend/tags/components/sort_dropdown_spec.js b/spec/frontend/tags/components/sort_dropdown_spec.js
index b0fd98ec68e..e0ff370d313 100644
--- a/spec/frontend/tags/components/sort_dropdown_spec.js
+++ b/spec/frontend/tags/components/sort_dropdown_spec.js
@@ -26,12 +26,6 @@ describe('Tags sort dropdown', () => {
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByClick);
const findTagsDropdown = () => wrapper.findByTestId('tags-dropdown');
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('default state', () => {
beforeEach(() => {
wrapper = createWrapper();
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
index 99f61a31dbd..cab7fbe18b0 100644
--- a/spec/frontend/terms/components/app_spec.js
+++ b/spec/frontend/terms/components/app_spec.js
@@ -37,10 +37,6 @@ describe('TermsApp', () => {
isLoggedIn.mockReturnValue(true);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findFormWithAction = (path) => wrapper.find(`form[action="${path}"]`);
const findButton = (path) => findFormWithAction(path).find('button[type="submit"]');
const findScrollableViewport = () => wrapper.findByTestId('scrollable-viewport');
@@ -69,7 +65,6 @@ describe('TermsApp', () => {
describe('accept button', () => {
it('is disabled until user scrolls to the bottom of the terms', async () => {
createComponent();
-
expect(findButton(defaultProvide.paths.accept).attributes('disabled')).toBe('disabled');
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
diff --git a/spec/frontend/terraform/components/empty_state_spec.js b/spec/frontend/terraform/components/empty_state_spec.js
index 21bfff5f1be..293b59007c9 100644
--- a/spec/frontend/terraform/components/empty_state_spec.js
+++ b/spec/frontend/terraform/components/empty_state_spec.js
@@ -1,6 +1,7 @@
-import { GlEmptyState, GlLink } from '@gitlab/ui';
+import { GlEmptyState, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EmptyState from '~/terraform/components/empty_state.vue';
+import InitCommandModal from '~/terraform/components/init_command_modal.vue';
describe('EmptyStateComponent', () => {
let wrapper;
@@ -10,23 +11,33 @@ describe('EmptyStateComponent', () => {
};
const docsUrl = '/help/user/infrastructure/iac/terraform_state';
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
- const findLink = () => wrapper.findComponent(GlLink);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findCopyModal = () => wrapper.findComponent(InitCommandModal);
+ const findCopyButton = () => wrapper.find('[data-testid="terraform-state-copy-init-command"]');
beforeEach(() => {
wrapper = shallowMount(EmptyState, { propsData });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render content', () => {
expect(findEmptyState().props('title')).toBe(
"Your project doesn't have any Terraform state files",
);
});
- it('should have a link to the GitLab managed Terraform states docs', () => {
- expect(findLink().attributes('href')).toBe(docsUrl);
+ it('buttons explore documentation should have a link to the GitLab managed Terraform states docs', () => {
+ expect(findButton().attributes('href')).toBe(docsUrl);
+ });
+
+ describe('copy command button', () => {
+ it('displays a copy init command button', () => {
+ expect(findCopyButton().text()).toBe('Copy Terraform init command');
+ });
+
+ it('opens the modal on copy button click', async () => {
+ await findCopyButton().vm.$emit('click');
+
+ expect(findCopyModal().isVisible()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/terraform/components/init_command_modal_spec.js b/spec/frontend/terraform/components/init_command_modal_spec.js
index 911bb8878da..4015482b81b 100644
--- a/spec/frontend/terraform/components/init_command_modal_spec.js
+++ b/spec/frontend/terraform/components/init_command_modal_spec.js
@@ -8,6 +8,7 @@ const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
const username = 'username';
const modalId = 'fake-modal-id';
const stateName = 'aws/eu-central-1';
+const stateNamePlaceholder = '<YOUR-STATE-NAME>';
const stateNameEncoded = encodeURIComponent(stateName);
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
terraform init \\
@@ -34,53 +35,68 @@ describe('InitCommandModal', () => {
username,
};
- const findExplanatoryText = () => wrapper.findByTestId('init-command-explanatory-text');
- const findLink = () => wrapper.findComponent(GlLink);
- const findInitCommand = () => wrapper.findByTestId('terraform-init-command');
- const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
-
- beforeEach(() => {
+ const mountComponent = ({ props = propsData } = {}) => {
wrapper = shallowMountExtended(InitCommandModal, {
- propsData,
+ propsData: props,
provide: provideData,
stubs: {
GlSprintf,
},
});
- });
+ };
- afterEach(() => {
- wrapper.destroy();
- });
+ const findExplanatoryText = () => wrapper.findByTestId('init-command-explanatory-text');
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findInitCommand = () => wrapper.findByTestId('terraform-init-command');
+ const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
- describe('on rendering', () => {
- it('renders the explanatory text', () => {
- expect(findExplanatoryText().text()).toContain('personal access token');
+ describe('when has stateName', () => {
+ beforeEach(() => {
+ mountComponent();
});
- it('renders the personal access token link', () => {
- expect(findLink().attributes('href')).toBe(accessTokensPath);
- });
+ describe('on rendering', () => {
+ it('renders the explanatory text', () => {
+ expect(findExplanatoryText().text()).toContain('personal access token');
+ });
- describe('init command', () => {
- it('includes correct address', () => {
- expect(findInitCommand().text()).toContain(
- `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`,
- );
+ it('renders the personal access token link', () => {
+ expect(findLink().attributes('href')).toBe(accessTokensPath);
});
- it('includes correct username', () => {
- expect(findInitCommand().text()).toContain(`-backend-config="username=${username}"`);
+
+ describe('init command', () => {
+ it('includes correct address', () => {
+ expect(findInitCommand().text()).toContain(
+ `-backend-config="address=${terraformApiUrl}/${stateNameEncoded}"`,
+ );
+ });
+ it('includes correct username', () => {
+ expect(findInitCommand().text()).toContain(`-backend-config="username=${username}"`);
+ });
+ });
+
+ it('renders the copyToClipboard button', () => {
+ expect(findCopyButton().exists()).toBe(true);
});
});
- it('renders the copyToClipboard button', () => {
- expect(findCopyButton().exists()).toBe(true);
+ describe('when copy button is clicked', () => {
+ it('copies init command to clipboard', () => {
+ expect(findCopyButton().props('text')).toBe(modalInfoCopyStr);
+ });
});
});
+ describe('when has no stateName', () => {
+ beforeEach(() => {
+ mountComponent({ props: { modalId } });
+ });
- describe('when copy button is clicked', () => {
- it('copies init command to clipboard', () => {
- expect(findCopyButton().props('text')).toBe(modalInfoCopyStr);
+ describe('on rendering', () => {
+ it('includes correct address', () => {
+ expect(findInitCommand().text()).toContain(
+ `-backend-config="address=${terraformApiUrl}/${stateNamePlaceholder}"`,
+ );
+ });
});
});
});
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 40b7448d78d..ed85825c13a 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlModal, GlSprintf, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -85,6 +85,7 @@ describe('StatesTableActions', () => {
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
const findRemoveBtn = () => wrapper.find('[data-testid="terraform-state-remove"]');
const findRemoveModal = () => wrapper.findComponent(GlModal);
+ const findFormInput = () => wrapper.findComponent(GlFormInput);
beforeEach(() => {
return createComponent();
@@ -96,7 +97,6 @@ describe('StatesTableActions', () => {
toast = null;
unlockResponse = null;
updateStateResponse = null;
- wrapper.destroy();
});
describe('when the state is loading', () => {
@@ -143,7 +143,7 @@ describe('StatesTableActions', () => {
return waitForPromises();
});
- it('opens the modal', async () => {
+ it('opens the modal', () => {
expect(findCopyModal().exists()).toBe(true);
expect(findCopyModal().isVisible()).toBe(true);
});
@@ -296,9 +296,7 @@ describe('StatesTableActions', () => {
describe('when state name is present', () => {
beforeEach(async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ removeConfirmText: defaultProps.state.name });
+ await findFormInput().vm.$emit('input', defaultProps.state.name);
findRemoveModal().vm.$emit('ok');
diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js
index 0b3b169891b..7c783c9f717 100644
--- a/spec/frontend/terraform/components/states_table_spec.js
+++ b/spec/frontend/terraform/components/states_table_spec.js
@@ -127,7 +127,7 @@ describe('StatesTable', () => {
propsData,
provide: { projectPath: 'path/to/project' },
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
}),
);
@@ -140,11 +140,6 @@ describe('StatesTable', () => {
return createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it.each`
name | toolTipText | hasBadge | loading | lineNumber
${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0}
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index 580951e799a..ef79c51415b 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -63,11 +63,6 @@ describe('TerraformList', () => {
const findStatesTable = () => wrapper.findComponent(StatesTable);
const findTab = () => wrapper.findComponent(GlTab);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when the terraform query has succeeded', () => {
describe('when there is a list of terraform states', () => {
const states = [
@@ -115,8 +110,8 @@ describe('TerraformList', () => {
return waitForPromises();
});
- it('displays a states tab and count', () => {
- expect(findTab().text()).toContain('States');
+ it('displays a terraform states tab and count', () => {
+ expect(findTab().text()).toContain('Terraform states');
expect(findBadge().text()).toBe('2');
});
@@ -163,8 +158,8 @@ describe('TerraformList', () => {
return waitForPromises();
});
- it('displays a states tab with no count', () => {
- expect(findTab().text()).toContain('States');
+ it('displays a terraform states tab with no count', () => {
+ expect(findTab().text()).toContain('Terraform states');
expect(findBadge().exists()).toBe(false);
});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 3fb226e5ed3..d3d3e5c8c72 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,8 +1,15 @@
/* Setup for unit test environment */
// eslint-disable-next-line no-restricted-syntax
import { setImmediate } from 'timers';
+import Dexie from 'dexie';
+import { IDBKeyRange, IDBFactory } from 'fake-indexeddb';
import 'helpers/shared_test_setup';
+const indexedDB = new IDBFactory();
+
+Dexie.dependencies.indexedDB = indexedDB;
+Dexie.dependencies.IDBKeyRange = IDBKeyRange;
+
afterEach(() =>
// give Promises a bit more time so they fail the right test
// eslint-disable-next-line no-restricted-syntax
@@ -11,3 +18,9 @@ afterEach(() =>
jest.runOnlyPendingTimers();
}),
);
+
+afterEach(async () => {
+ const dbs = await indexedDB.databases();
+
+ await Promise.all(dbs.map((db) => indexedDB.deleteDatabase(db.name)));
+});
diff --git a/spec/frontend/time_tracking/components/timelog_source_cell_spec.js b/spec/frontend/time_tracking/components/timelog_source_cell_spec.js
new file mode 100644
index 00000000000..14015ee4e75
--- /dev/null
+++ b/spec/frontend/time_tracking/components/timelog_source_cell_spec.js
@@ -0,0 +1,136 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TimelogSourceCell from '~/time_tracking/components/timelog_source_cell.vue';
+import {
+ issuableStatusText,
+ STATUS_CLOSED,
+ STATUS_MERGED,
+ STATUS_OPEN,
+ STATUS_LOCKED,
+ STATUS_REOPENED,
+} from '~/issues/constants';
+
+const createIssuableTimelogMock = (
+ type,
+ { title, state, webUrl, reference } = {
+ title: 'Issuable title',
+ state: STATUS_OPEN,
+ webUrl: 'https://example.com/issuable_url',
+ reference: '#111',
+ },
+) => {
+ return {
+ timelog: {
+ project: {
+ fullPath: 'group/project',
+ },
+ [type]: {
+ title,
+ state,
+ webUrl,
+ reference,
+ },
+ },
+ };
+};
+
+describe('TimelogSourceCell component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const findTitleContainer = () => wrapper.findByTestId('title-container');
+ const findReferenceContainer = () => wrapper.findByTestId('reference-container');
+ const findStateContainer = () => wrapper.findByTestId('state-container');
+
+ const mountComponent = ({ timelog } = {}) => {
+ wrapper = shallowMountExtended(TimelogSourceCell, {
+ propsData: {
+ timelog,
+ },
+ });
+ };
+
+ describe('when the timelog is associated to an issue', () => {
+ it('shows the issue title as link to the issue', () => {
+ mountComponent(
+ createIssuableTimelogMock('issue', {
+ title: 'Issue title',
+ webUrl: 'https://example.com/issue_url',
+ }),
+ );
+
+ const titleContainer = findTitleContainer();
+
+ expect(titleContainer.text()).toBe('Issue title');
+ expect(titleContainer.attributes('href')).toBe('https://example.com/issue_url');
+ });
+
+ it('shows the issue full reference as link to the issue', () => {
+ mountComponent(
+ createIssuableTimelogMock('issue', {
+ reference: '#111',
+ webUrl: 'https://example.com/issue_url',
+ }),
+ );
+
+ const referenceContainer = findReferenceContainer();
+
+ expect(referenceContainer.text()).toBe('group/project#111');
+ expect(referenceContainer.attributes('href')).toBe('https://example.com/issue_url');
+ });
+
+ it.each`
+ state | stateDescription
+ ${STATUS_OPEN} | ${issuableStatusText[STATUS_OPEN]}
+ ${STATUS_REOPENED} | ${issuableStatusText[STATUS_REOPENED]}
+ ${STATUS_LOCKED} | ${issuableStatusText[STATUS_LOCKED]}
+ ${STATUS_CLOSED} | ${issuableStatusText[STATUS_CLOSED]}
+ `('shows $stateDescription when the state is $state', ({ state, stateDescription }) => {
+ mountComponent(createIssuableTimelogMock('issue', { state }));
+
+ expect(findStateContainer().text()).toBe(stateDescription);
+ });
+ });
+
+ describe('when the timelog is associated to a merge request', () => {
+ it('shows the merge request title as link to the merge request', () => {
+ mountComponent(
+ createIssuableTimelogMock('mergeRequest', {
+ title: 'MR title',
+ webUrl: 'https://example.com/mr_url',
+ }),
+ );
+
+ const titleContainer = findTitleContainer();
+
+ expect(titleContainer.text()).toBe('MR title');
+ expect(titleContainer.attributes('href')).toBe('https://example.com/mr_url');
+ });
+
+ it('shows the merge request full reference as link to the merge request', () => {
+ mountComponent(
+ createIssuableTimelogMock('mergeRequest', {
+ reference: '!111',
+ webUrl: 'https://example.com/mr_url',
+ }),
+ );
+
+ const referenceContainer = findReferenceContainer();
+
+ expect(referenceContainer.text()).toBe('group/project!111');
+ expect(referenceContainer.attributes('href')).toBe('https://example.com/mr_url');
+ });
+ it.each`
+ state | stateDescription
+ ${STATUS_OPEN} | ${issuableStatusText[STATUS_OPEN]}
+ ${STATUS_CLOSED} | ${issuableStatusText[STATUS_CLOSED]}
+ ${STATUS_MERGED} | ${issuableStatusText[STATUS_MERGED]}
+ `('shows $stateDescription when the state is $state', ({ state, stateDescription }) => {
+ mountComponent(createIssuableTimelogMock('mergeRequest', { state }));
+
+ expect(findStateContainer().text()).toBe(stateDescription);
+ });
+ });
+});
diff --git a/spec/frontend/time_tracking/components/timelogs_app_spec.js b/spec/frontend/time_tracking/components/timelogs_app_spec.js
new file mode 100644
index 00000000000..ca470ce63ac
--- /dev/null
+++ b/spec/frontend/time_tracking/components/timelogs_app_spec.js
@@ -0,0 +1,238 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import * as Sentry from '@sentry/browser';
+import { GlDatepicker, GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
+import getTimelogsEmptyResponse from 'test_fixtures/graphql/get_timelogs_empty_response.json';
+import getPaginatedTimelogsResponse from 'test_fixtures/graphql/get_paginated_timelogs_response.json';
+import getNonPaginatedTimelogsResponse from 'test_fixtures/graphql/get_non_paginated_timelogs_response.json';
+import { createAlert } from '~/alert';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getTimelogsQuery from '~/time_tracking/components/queries/get_timelogs.query.graphql';
+import TimelogsApp from '~/time_tracking/components/timelogs_app.vue';
+import TimelogsTable from '~/time_tracking/components/timelogs_table.vue';
+
+jest.mock('~/alert');
+jest.mock('@sentry/browser');
+
+describe('Timelogs app', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ let fakeApollo;
+
+ const findForm = () => wrapper.find('form');
+ const findUsernameInput = () => extendedWrapper(findForm()).findByTestId('form-username');
+ const findTableContainer = () => wrapper.findByTestId('table-container');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTotalTimeSpentContainer = () => wrapper.findByTestId('total-time-spent-container');
+ const findTable = () => wrapper.findComponent(TimelogsTable);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ const findFormDatePicker = (testId) =>
+ findForm()
+ .findAllComponents(GlDatepicker)
+ .filter((c) => c.attributes('data-testid') === testId);
+ const findFromDatepicker = () => findFormDatePicker('form-from-date').at(0);
+ const findToDatepicker = () => findFormDatePicker('form-to-date').at(0);
+
+ const submitForm = () => findForm().trigger('submit');
+
+ const resolvedEmptyListMock = jest.fn().mockResolvedValue(getTimelogsEmptyResponse);
+ const resolvedPaginatedListMock = jest.fn().mockResolvedValue(getPaginatedTimelogsResponse);
+ const resolvedNonPaginatedListMock = jest.fn().mockResolvedValue(getNonPaginatedTimelogsResponse);
+ const rejectedMock = jest.fn().mockRejectedValue({});
+
+ const mountComponent = ({ props, data } = {}, queryResolverMock = resolvedEmptyListMock) => {
+ fakeApollo = createMockApollo([[getTimelogsQuery, queryResolverMock]]);
+
+ wrapper = mountExtended(TimelogsApp, {
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ limitToHours: false,
+ ...props,
+ },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ beforeEach(() => {
+ createAlert.mockClear();
+ Sentry.captureException.mockClear();
+ });
+
+ afterEach(() => {
+ fakeApollo = null;
+ });
+
+ describe('the content', () => {
+ it('shows the form and the loading icon when loading', () => {
+ mountComponent();
+
+ expect(findForm().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findTableContainer().exists()).toBe(false);
+ });
+
+ it('shows the form and the table container when finished loading', async () => {
+ mountComponent();
+
+ await waitForPromises();
+
+ expect(findForm().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findTableContainer().exists()).toBe(true);
+ });
+ });
+
+ describe('the filter form', () => {
+ it('runs the query with the correct data', async () => {
+ mountComponent();
+
+ const username = 'johnsmith';
+ const fromDate = new Date('2023-02-28');
+ const toDate = new Date('2023-03-28');
+
+ findUsernameInput().vm.$emit('input', username);
+ findFromDatepicker().vm.$emit('input', fromDate);
+ findToDatepicker().vm.$emit('input', toDate);
+
+ resolvedEmptyListMock.mockClear();
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(resolvedEmptyListMock).toHaveBeenCalledWith({
+ username,
+ startDate: fromDate,
+ endDate: toDate,
+ groupId: null,
+ projectId: null,
+ first: 20,
+ last: null,
+ after: null,
+ before: null,
+ });
+ expect(createAlert).not.toHaveBeenCalled();
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('runs the query with the correct data after the date filters are cleared', async () => {
+ mountComponent();
+
+ const username = 'johnsmith';
+
+ findUsernameInput().vm.$emit('input', username);
+ findFromDatepicker().vm.$emit('clear');
+ findToDatepicker().vm.$emit('clear');
+
+ resolvedEmptyListMock.mockClear();
+
+ submitForm();
+
+ await waitForPromises();
+
+ expect(resolvedEmptyListMock).toHaveBeenCalledWith({
+ username,
+ startDate: null,
+ endDate: null,
+ groupId: null,
+ projectId: null,
+ first: 20,
+ last: null,
+ after: null,
+ before: null,
+ });
+ expect(createAlert).not.toHaveBeenCalled();
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+
+ it('shows an alert an logs to sentry when the mutation is rejected', async () => {
+ mountComponent({}, rejectedMock);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong. Please try again.',
+ });
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+ });
+
+ describe('the total time spent container', () => {
+ it('is not visible when there are no timelogs', async () => {
+ mountComponent();
+
+ await waitForPromises();
+
+ expect(findTotalTimeSpentContainer().exists()).toBe(false);
+ });
+
+ it('shows the correct value when `limitToHours` is false', async () => {
+ mountComponent({}, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findTotalTimeSpentContainer().exists()).toBe(true);
+ expect(findTotalTimeSpentContainer().text()).toBe('3d');
+ });
+
+ it('shows the correct value when `limitToHours` is true', async () => {
+ mountComponent({ props: { limitToHours: true } }, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findTotalTimeSpentContainer().exists()).toBe(true);
+ expect(findTotalTimeSpentContainer().text()).toBe('24h');
+ });
+ });
+
+ describe('the table', () => {
+ it('gets created with the right props when `limitToHours` is false', async () => {
+ mountComponent({}, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findTable().props()).toMatchObject({
+ limitToHours: false,
+ entries: getNonPaginatedTimelogsResponse.data.timelogs.nodes,
+ });
+ });
+
+ it('gets created with the right props when `limitToHours` is true', async () => {
+ mountComponent({ props: { limitToHours: true } }, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findTable().props()).toMatchObject({
+ limitToHours: true,
+ entries: getNonPaginatedTimelogsResponse.data.timelogs.nodes,
+ });
+ });
+ });
+
+ describe('the pagination element', () => {
+ it('is not visible whene there is no pagination data', async () => {
+ mountComponent({}, resolvedNonPaginatedListMock);
+
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ it('is visible whene there is pagination data', async () => {
+ mountComponent({}, resolvedPaginatedListMock);
+
+ await waitForPromises();
+ await nextTick();
+
+ expect(findPagination().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/time_tracking/components/timelogs_table_spec.js b/spec/frontend/time_tracking/components/timelogs_table_spec.js
new file mode 100644
index 00000000000..980fb79e8fb
--- /dev/null
+++ b/spec/frontend/time_tracking/components/timelogs_table_spec.js
@@ -0,0 +1,223 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlTable } from '@gitlab/ui';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import TimelogsTable from '~/time_tracking/components/timelogs_table.vue';
+import TimelogSourceCell from '~/time_tracking/components/timelog_source_cell.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import { STATUS_OPEN, STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
+
+const baseTimelogMock = {
+ timeSpent: 600,
+ project: {
+ fullPath: 'group/project',
+ },
+ user: {
+ name: 'John Smith',
+ avatarUrl: 'https://example.gitlab.com/john.jpg',
+ webPath: 'https://example.gitlab.com/john',
+ },
+ spentAt: '2023-03-27T21:00:00Z',
+ note: null,
+ summary: 'Summary from timelog field',
+ issue: {
+ title: 'Issue title',
+ webUrl: 'https://example.gitlab.com/issue_url_a',
+ state: STATUS_OPEN,
+ reference: '#111',
+ },
+ mergeRequest: null,
+};
+
+const timelogsMock = [
+ baseTimelogMock,
+ {
+ timeSpent: 3600,
+ project: {
+ fullPath: 'group/project_b',
+ },
+ user: {
+ name: 'Paul Reed',
+ avatarUrl: 'https://example.gitlab.com/paul.jpg',
+ webPath: 'https://example.gitlab.com/paul',
+ },
+ spentAt: '2023-03-28T16:00:00Z',
+ note: {
+ body: 'Summary from the body',
+ },
+ summary: null,
+ issue: {
+ title: 'Other issue title',
+ webUrl: 'https://example.gitlab.com/issue_url_b',
+ state: STATUS_CLOSED,
+ reference: '#112',
+ },
+ mergeRequest: null,
+ },
+ {
+ timeSpent: 27 * 60 * 60, // 27h or 3d 3h (3 days of 8 hours)
+ project: {
+ fullPath: 'group/project_b',
+ },
+ user: {
+ name: 'Les Gibbons',
+ avatarUrl: 'https://example.gitlab.com/les.jpg',
+ webPath: 'https://example.gitlab.com/les',
+ },
+ spentAt: '2023-03-28T18:00:00Z',
+ note: null,
+ summary: 'Other timelog summary',
+ issue: null,
+ mergeRequest: {
+ title: 'MR title',
+ webUrl: 'https://example.gitlab.com/mr_url',
+ state: STATUS_MERGED,
+ reference: '!99',
+ },
+ },
+];
+
+describe('TimelogsTable component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findTableRows = () => findTable().find('tbody').findAll('tr');
+ const findRowSpentAt = (rowIndex) =>
+ extendedWrapper(findTableRows().at(rowIndex)).findByTestId('date-container');
+ const findRowSource = (rowIndex) => findTableRows().at(rowIndex).findComponent(TimelogSourceCell);
+ const findRowUser = (rowIndex) => findTableRows().at(rowIndex).findComponent(UserAvatarLink);
+ const findRowTimeSpent = (rowIndex) =>
+ extendedWrapper(findTableRows().at(rowIndex)).findByTestId('time-spent-container');
+ const findRowSummary = (rowIndex) =>
+ extendedWrapper(findTableRows().at(rowIndex)).findByTestId('summary-container');
+
+ const mountComponent = (props = {}) => {
+ wrapper = mountExtended(TimelogsTable, {
+ propsData: {
+ entries: timelogsMock,
+ limitToHours: false,
+ ...props,
+ },
+ stubs: { GlTable },
+ });
+ };
+
+ describe('when there are no entries', () => {
+ it('show the empty table message and no rows', () => {
+ mountComponent({ entries: [] });
+
+ expect(findTable().text()).toContain('There are no records to show');
+ expect(findTableRows()).toHaveLength(1);
+ });
+ });
+
+ describe('when there are some entries', () => {
+ it('does not show the empty table message and has the correct number of rows', () => {
+ mountComponent();
+
+ expect(findTable().text()).not.toContain('There are no records to show');
+ expect(findTableRows()).toHaveLength(3);
+ });
+
+ describe('Spent at column', () => {
+ it('shows the spent at value with in the correct format', () => {
+ mountComponent();
+
+ expect(findRowSpentAt(0).text()).toBe('March 27, 2023, 21:00 (UTC: +0000)');
+ });
+ });
+
+ describe('Source column', () => {
+ it('creates the source cell component passing the right props', () => {
+ mountComponent();
+
+ expect(findRowSource(0).props()).toMatchObject({
+ timelog: timelogsMock[0],
+ });
+ expect(findRowSource(1).props()).toMatchObject({
+ timelog: timelogsMock[1],
+ });
+ expect(findRowSource(2).props()).toMatchObject({
+ timelog: timelogsMock[2],
+ });
+ });
+ });
+
+ describe('User column', () => {
+ it('creates the user avatar component passing the right props', () => {
+ mountComponent();
+
+ expect(findRowUser(0).props()).toMatchObject({
+ linkHref: timelogsMock[0].user.webPath,
+ imgSrc: timelogsMock[0].user.avatarUrl,
+ imgSize: 16,
+ imgAlt: timelogsMock[0].user.name,
+ tooltipText: timelogsMock[0].user.name,
+ username: timelogsMock[0].user.name,
+ });
+ expect(findRowUser(1).props()).toMatchObject({
+ linkHref: timelogsMock[1].user.webPath,
+ imgSrc: timelogsMock[1].user.avatarUrl,
+ imgSize: 16,
+ imgAlt: timelogsMock[1].user.name,
+ tooltipText: timelogsMock[1].user.name,
+ username: timelogsMock[1].user.name,
+ });
+ expect(findRowUser(2).props()).toMatchObject({
+ linkHref: timelogsMock[2].user.webPath,
+ imgSrc: timelogsMock[2].user.avatarUrl,
+ imgSize: 16,
+ imgAlt: timelogsMock[2].user.name,
+ tooltipText: timelogsMock[2].user.name,
+ username: timelogsMock[2].user.name,
+ });
+ });
+ });
+
+ describe('Time spent column', () => {
+ it('shows the time spent value with the correct format when `limitToHours` is false', () => {
+ mountComponent();
+
+ expect(findRowTimeSpent(0).text()).toBe('10m');
+ expect(findRowTimeSpent(1).text()).toBe('1h');
+ expect(findRowTimeSpent(2).text()).toBe('3d 3h');
+ });
+
+ it('shows the time spent value with the correct format when `limitToHours` is true', () => {
+ mountComponent({ limitToHours: true });
+
+ expect(findRowTimeSpent(0).text()).toBe('10m');
+ expect(findRowTimeSpent(1).text()).toBe('1h');
+ expect(findRowTimeSpent(2).text()).toBe('27h');
+ });
+ });
+
+ describe('Summary column', () => {
+ it('shows the summary from the note when note body is present and not empty', () => {
+ mountComponent({
+ entries: [{ ...baseTimelogMock, note: { body: 'Summary from note body' } }],
+ });
+
+ expect(findRowSummary(0).text()).toBe('Summary from note body');
+ });
+
+ it('shows the summary from the timelog note body is present but empty', () => {
+ mountComponent({
+ entries: [{ ...baseTimelogMock, note: { body: '' } }],
+ });
+
+ expect(findRowSummary(0).text()).toBe('Summary from timelog field');
+ });
+
+ it('shows the summary from the timelog note body is not present', () => {
+ mountComponent({
+ entries: [baseTimelogMock],
+ });
+
+ expect(findRowSummary(0).text()).toBe('Summary from timelog field');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/toggles/index_spec.js b/spec/frontend/toggles/index_spec.js
index f8c43e0ad0c..cccdf17a787 100644
--- a/spec/frontend/toggles/index_spec.js
+++ b/spec/frontend/toggles/index_spec.js
@@ -44,7 +44,6 @@ describe('toggles/index.js', () => {
afterEach(() => {
document.body.innerHTML = '';
instance = null;
- toggleWrapper = null;
});
describe('initToggle', () => {
@@ -53,7 +52,7 @@ describe('toggles/index.js', () => {
initToggleWithOptions();
});
- it('attaches a GlToggle to the element', async () => {
+ it('attaches a GlToggle to the element', () => {
expect(toggleWrapper).not.toBe(null);
expect(toggleWrapper.querySelector(TOGGLE_LABEL_CLASS).textContent).toBe(toggleLabel);
});
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js
index fcd1a33fa68..1ca58053e68 100644
--- a/spec/frontend/token_access/inbound_token_access_spec.js
+++ b/spec/frontend/token_access/inbound_token_access_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
import inboundAddProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql';
import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
@@ -26,7 +26,7 @@ const error = new Error(message);
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('TokenAccess component', () => {
let wrapper;
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index ab04735b985..a4b5532108a 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -70,13 +70,13 @@ export const removeProjectSuccess = {
export const updateScopeSuccess = {
data: {
- ciCdSettingsUpdate: {
+ projectCiCdSettingsUpdate: {
ciCdSettings: {
jobTokenScopeEnabled: false,
__typename: 'ProjectCiCdSetting',
},
errors: [],
- __typename: 'CiCdSettingsUpdatePayload',
+ __typename: 'ProjectCiCdSettingsUpdatePayload',
},
},
};
@@ -121,32 +121,6 @@ export const mockFields = [
},
];
-export const optInJwtQueryResponse = (optInJwt) => ({
- data: {
- project: {
- id: '1',
- ciCdSettings: {
- optInJwt,
- __typename: 'ProjectCiCdSetting',
- },
- __typename: 'Project',
- },
- },
-});
-
-export const optInJwtMutationResponse = (optInJwt) => ({
- data: {
- ciCdSettingsUpdate: {
- ciCdSettings: {
- optInJwt,
- __typename: 'ProjectCiCdSetting',
- },
- errors: [],
- __typename: 'CiCdSettingsUpdatePayload',
- },
- },
-});
-
export const inboundJobTokenScopeEnabledResponse = {
data: {
project: {
@@ -217,13 +191,13 @@ export const inboundRemoveProjectSuccess = {
export const inboundUpdateScopeSuccessResponse = {
data: {
- ciCdSettingsUpdate: {
+ projectCiCdSettingsUpdate: {
ciCdSettings: {
inboundJobTokenScopeEnabled: false,
__typename: 'ProjectCiCdSetting',
},
errors: [],
- __typename: 'CiCdSettingsUpdatePayload',
+ __typename: 'ProjectCiCdSettingsUpdatePayload',
},
},
};
diff --git a/spec/frontend/token_access/opt_in_jwt_spec.js b/spec/frontend/token_access/opt_in_jwt_spec.js
deleted file mode 100644
index 3a68f247aa6..00000000000
--- a/spec/frontend/token_access/opt_in_jwt_spec.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import { GlLink, GlLoadingIcon, GlToggle, GlSprintf } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import { OPT_IN_JWT_HELP_LINK } from '~/token_access/constants';
-import OptInJwt from '~/token_access/components/opt_in_jwt.vue';
-import getOptInJwtSettingQuery from '~/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql';
-import updateOptInJwtMutation from '~/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql';
-import { optInJwtMutationResponse, optInJwtQueryResponse } from './mock_data';
-
-const errorMessage = 'An error occurred';
-const error = new Error(errorMessage);
-
-Vue.use(VueApollo);
-
-jest.mock('~/flash');
-
-describe('OptInJwt component', () => {
- let wrapper;
-
- const failureHandler = jest.fn().mockRejectedValue(error);
- const enabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(true));
- const disabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(false));
- const updateOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtMutationResponse(true));
-
- const findHelpLink = () => wrapper.findComponent(GlLink);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findToggle = () => wrapper.findComponent(GlToggle);
- const findOptInJwtExpandedSection = () => wrapper.findByTestId('opt-in-jwt-expanded-section');
-
- const createMockApolloProvider = (requestHandlers) => {
- return createMockApollo(requestHandlers);
- };
-
- const createComponent = (requestHandlers, mountFn = shallowMountExtended, options = {}) => {
- wrapper = mountFn(OptInJwt, {
- provide: {
- fullPath: 'root/my-repo',
- },
- apolloProvider: createMockApolloProvider(requestHandlers),
- data() {
- return {
- targetProjectPath: 'root/test',
- };
- },
- ...options,
- });
- };
-
- const createShallowComponent = (requestHandlers, options = {}) =>
- createComponent(requestHandlers, shallowMountExtended, options);
- const createFullComponent = (requestHandlers, options = {}) =>
- createComponent(requestHandlers, mountExtended, options);
-
- describe('loading state', () => {
- it('shows loading state and hides toggle while waiting on query to resolve', async () => {
- createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]);
-
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findToggle().exists()).toBe(false);
-
- await waitForPromises();
-
- expect(findLoadingIcon().exists()).toBe(false);
- expect(findToggle().exists()).toBe(true);
- });
- });
-
- describe('template', () => {
- it('renders help link', async () => {
- createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]], {
- stubs: {
- GlToggle,
- GlSprintf,
- GlLink,
- },
- });
- await waitForPromises();
-
- expect(findHelpLink().exists()).toBe(true);
- expect(findHelpLink().attributes('href')).toBe(OPT_IN_JWT_HELP_LINK);
- });
- });
-
- describe('toggle JWT token access', () => {
- it('code instruction is visible when toggle is enabled', async () => {
- createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]);
-
- await waitForPromises();
-
- expect(findToggle().props('value')).toBe(true);
- expect(findOptInJwtExpandedSection().exists()).toBe(true);
- });
-
- it('code instruction is hidden when toggle is disabled', async () => {
- createShallowComponent([[getOptInJwtSettingQuery, disabledOptInJwtHandler]]);
-
- await waitForPromises();
-
- expect(findToggle().props('value')).toBe(false);
- expect(findOptInJwtExpandedSection().exists()).toBe(false);
- });
-
- describe('update JWT token access', () => {
- it('calls updateOptInJwtMutation with correct arguments', async () => {
- createFullComponent([
- [getOptInJwtSettingQuery, disabledOptInJwtHandler],
- [updateOptInJwtMutation, updateOptInJwtHandler],
- ]);
-
- await waitForPromises();
-
- findToggle().vm.$emit('change', true);
-
- expect(updateOptInJwtHandler).toHaveBeenCalledWith({
- input: {
- fullPath: 'root/my-repo',
- optInJwt: true,
- },
- });
- });
-
- it('handles update error', async () => {
- createFullComponent([
- [getOptInJwtSettingQuery, enabledOptInJwtHandler],
- [updateOptInJwtMutation, failureHandler],
- ]);
-
- await waitForPromises();
-
- findToggle().vm.$emit('change', false);
-
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({
- message: 'An error occurred while update the setting. Please try again.',
- });
- });
- });
- });
-});
diff --git a/spec/frontend/token_access/outbound_token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js
index 893a021197f..7f321495d72 100644
--- a/spec/frontend/token_access/outbound_token_access_spec.js
+++ b/spec/frontend/token_access/outbound_token_access_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue';
import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
@@ -26,7 +26,7 @@ const error = new Error(message);
Vue.use(VueApollo);
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('TokenAccess component', () => {
let wrapper;
@@ -44,15 +44,26 @@ describe('TokenAccess component', () => {
const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
const findTokenDisabledAlert = () => wrapper.findByTestId('token-disabled-alert');
+ const findDeprecationAlert = () => wrapper.findByTestId('deprecation-alert');
+ const findProjectPathInput = () => wrapper.findByTestId('project-path-input');
const createMockApolloProvider = (requestHandlers) => {
return createMockApollo(requestHandlers);
};
- const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
+ const createComponent = (
+ requestHandlers,
+ mountFn = shallowMountExtended,
+ frozenOutboundJobTokenScopes = false,
+ frozenOutboundJobTokenScopesOverride = false,
+ ) => {
wrapper = mountFn(OutboundTokenAccess, {
provide: {
fullPath: projectPath,
+ glFeatures: {
+ frozenOutboundJobTokenScopes,
+ frozenOutboundJobTokenScopesOverride,
+ },
},
apolloProvider: createMockApolloProvider(requestHandlers),
data() {
@@ -272,4 +283,59 @@ describe('TokenAccess component', () => {
expect(createAlert).toHaveBeenCalledWith({ message });
});
});
+
+ describe('with the frozenOutboundJobTokenScopes feature flag enabled', () => {
+ describe('toggle', () => {
+ it('the toggle is off and the deprecation alert is visible', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ],
+ shallowMountExtended,
+ true,
+ );
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(findToggle().props('disabled')).toBe(true);
+ expect(findDeprecationAlert().exists()).toBe(true);
+ expect(findTokenDisabledAlert().exists()).toBe(false);
+ });
+
+ it('contains a warning message about disabling the current configuration', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ],
+ mountExtended,
+ true,
+ );
+
+ await waitForPromises();
+
+ expect(findToggle().text()).toContain('Disabling this feature is a permanent change.');
+ });
+ });
+
+ describe('adding a new project', () => {
+ it('disables the input to add new projects', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ],
+ mountExtended,
+ true,
+ false,
+ );
+
+ await waitForPromises();
+
+ expect(findProjectPathInput().attributes('disabled')).toBe('disabled');
+ });
+ });
+ });
});
diff --git a/spec/frontend/token_access/token_access_app_spec.js b/spec/frontend/token_access/token_access_app_spec.js
index 7f269ee5fda..77356f1b839 100644
--- a/spec/frontend/token_access/token_access_app_spec.js
+++ b/spec/frontend/token_access/token_access_app_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue';
import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
-import OptInJwt from '~/token_access/components/opt_in_jwt.vue';
import TokenAccessApp from '~/token_access/components/token_access_app.vue';
describe('TokenAccessApp component', () => {
@@ -9,14 +8,9 @@ describe('TokenAccessApp component', () => {
const findOutboundTokenAccess = () => wrapper.findComponent(OutboundTokenAccess);
const findInboundTokenAccess = () => wrapper.findComponent(InboundTokenAccess);
- const findOptInJwt = () => wrapper.findComponent(OptInJwt);
- const createComponent = (flagState = false) => {
- wrapper = shallowMount(TokenAccessApp, {
- provide: {
- glFeatures: { ciInboundJobTokenScope: flagState },
- },
- });
+ const createComponent = () => {
+ wrapper = shallowMount(TokenAccessApp);
};
describe('default', () => {
@@ -24,20 +18,10 @@ describe('TokenAccessApp component', () => {
createComponent();
});
- it('renders the opt in jwt component', () => {
- expect(findOptInJwt().exists()).toBe(true);
- });
-
it('renders the outbound token access component', () => {
expect(findOutboundTokenAccess().exists()).toBe(true);
});
- it('does not render the inbound token access component', () => {
- expect(findInboundTokenAccess().exists()).toBe(false);
- });
- });
-
- describe('with feature flag enabled', () => {
it('renders the inbound token access component', () => {
createComponent(true);
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
index b51d8b3ccea..7654aa09b0a 100644
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -6,14 +6,19 @@ import { mockProjects, mockFields } from './mock_data';
describe('Token projects table', () => {
let wrapper;
- const createComponent = () => {
+ const defaultProps = {
+ tableFields: mockFields,
+ projects: mockProjects,
+ };
+
+ const createComponent = (props) => {
wrapper = mountExtended(TokenProjectsTable, {
provide: {
fullPath: 'root/ci-project',
},
propsData: {
- tableFields: mockFields,
- projects: mockProjects,
+ ...defaultProps,
+ ...props,
},
});
};
@@ -25,31 +30,52 @@ describe('Token projects table', () => {
const findProjectNameCell = () => wrapper.findByTestId('token-access-project-name');
const findProjectNamespaceCell = () => wrapper.findByTestId('token-access-project-namespace');
- beforeEach(() => {
+ it('displays a table', () => {
createComponent();
- });
- it('displays a table', () => {
expect(findTable().exists()).toBe(true);
});
it('displays the correct amount of table rows', () => {
+ createComponent();
+
expect(findAllTableRows()).toHaveLength(mockProjects.length);
});
it('delete project button emits event with correct project to delete', async () => {
+ createComponent();
+
await findDeleteProjectBtn().trigger('click');
expect(wrapper.emitted('removeProject')).toEqual([[mockProjects[0].fullPath]]);
});
it('does not show the remove icon if the project is locked', () => {
+ createComponent();
+
// currently two mock projects with one being a locked project
expect(findAllDeleteProjectBtn()).toHaveLength(1);
});
it('displays project and namespace cells', () => {
+ createComponent();
+
expect(findProjectNameCell().text()).toBe('merge-train-stuff');
expect(findProjectNamespaceCell().text()).toBe('root');
});
+
+ it('displays empty string for namespace when namespace is null', () => {
+ const nullNamespace = {
+ id: '1',
+ name: 'merge-train-stuff',
+ namespace: null,
+ fullPath: 'root/merge-train-stuff',
+ isLocked: false,
+ __typename: 'Project',
+ };
+
+ createComponent({ projects: [nullNamespace] });
+
+ expect(findProjectNamespaceCell().text()).toBe('');
+ });
});
diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js
index d5a63a99601..e473091f405 100644
--- a/spec/frontend/tooltips/components/tooltips_spec.js
+++ b/spec/frontend/tooltips/components/tooltips_spec.js
@@ -30,11 +30,6 @@ describe('tooltips/components/tooltips.vue', () => {
const allTooltips = () => wrapper.findAllComponents(GlTooltip);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('addTooltips', () => {
let target;
diff --git a/spec/frontend/tracking/tracking_initialization_spec.js b/spec/frontend/tracking/tracking_initialization_spec.js
index f1628ad9793..3c512cf73a7 100644
--- a/spec/frontend/tracking/tracking_initialization_spec.js
+++ b/spec/frontend/tracking/tracking_initialization_spec.js
@@ -52,14 +52,12 @@ describe('Tracking', () => {
hostname: 'app.test.com',
cookieDomain: '.test.com',
appId: '',
- userFingerprint: false,
respectDoNotTrack: true,
- forceSecureTracker: true,
eventMethod: 'post',
+ plugins: [],
contexts: { webPage: true, performanceTiming: true },
formTracking: false,
linkClickTracking: false,
- pageUnloadTimer: 10,
formTrackingConfig: {
fields: { allow: [] },
forms: { allow: [] },
@@ -80,8 +78,14 @@ describe('Tracking', () => {
it('should activate features based on what has been enabled', () => {
initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', 30, 30);
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', 'GitLab', [standardContext]);
+ expect(snowplowSpy).toHaveBeenCalledWith('enableActivityTracking', {
+ minimumVisitLength: 30,
+ heartbeatDelay: 30,
+ });
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', {
+ title: 'GitLab',
+ context: [standardContext],
+ });
expect(snowplowSpy).toHaveBeenCalledWith('setDocumentTitle', 'GitLab');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
@@ -131,10 +135,10 @@ describe('Tracking', () => {
it('includes those contexts alongside the standard context', () => {
initDefaultTrackers();
- expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', 'GitLab', [
- standardContext,
- ...experimentContexts,
- ]);
+ expect(snowplowSpy).toHaveBeenCalledWith('trackPageView', {
+ title: 'GitLab',
+ context: [standardContext, ...experimentContexts],
+ });
});
});
});
diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js
index 4871644d99f..c23790bb589 100644
--- a/spec/frontend/tracking/tracking_spec.js
+++ b/spec/frontend/tracking/tracking_spec.js
@@ -65,15 +65,14 @@ describe('Tracking', () => {
it('tracks to snowplow (our current tracking system)', () => {
Tracking.event(TEST_CATEGORY, TEST_ACTION, { label: TEST_LABEL });
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'trackStructEvent',
- TEST_CATEGORY,
- TEST_ACTION,
- TEST_LABEL,
- undefined,
- undefined,
- [standardContext],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', {
+ category: TEST_CATEGORY,
+ action: TEST_ACTION,
+ label: TEST_LABEL,
+ property: undefined,
+ value: undefined,
+ context: [standardContext],
+ });
});
it('returns `true` if the Snowplow library was called without issues', () => {
@@ -93,14 +92,13 @@ describe('Tracking', () => {
Tracking.event(TEST_CATEGORY, TEST_ACTION, { extra });
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'trackStructEvent',
- TEST_CATEGORY,
- TEST_ACTION,
- undefined,
- undefined,
- undefined,
- [
+ expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', {
+ category: TEST_CATEGORY,
+ action: TEST_ACTION,
+ label: undefined,
+ property: undefined,
+ value: undefined,
+ context: [
{
...standardContext,
data: {
@@ -109,7 +107,7 @@ describe('Tracking', () => {
},
},
],
- );
+ });
});
it('skips tracking if snowplow is unavailable', () => {
@@ -209,14 +207,16 @@ describe('Tracking', () => {
describe('.enableFormTracking', () => {
it('tells snowplow to enable form tracking, with only explicit contexts', () => {
- const config = { forms: { allow: ['form-class1'] }, fields: { allow: ['input-class1'] } };
+ const config = {
+ forms: { allow: ['form-class1'] },
+ fields: { allow: ['input-class1'] },
+ };
Tracking.enableFormTracking(config, ['_passed_context_', standardContext]);
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'enableFormTracking',
- { forms: { whitelist: ['form-class1'] }, fields: { whitelist: ['input-class1'] } },
- ['_passed_context_'],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking', {
+ options: { forms: { allowlist: ['form-class1'] }, fields: { allowlist: ['input-class1'] } },
+ context: ['_passed_context_'],
+ });
});
it('throws an error if no allow rules are provided', () => {
@@ -232,11 +232,10 @@ describe('Tracking', () => {
it('does not add empty form allow rules', () => {
Tracking.enableFormTracking({ fields: { allow: ['input-class1'] } });
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'enableFormTracking',
- { fields: { whitelist: ['input-class1'] } },
- [],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking', {
+ options: { fields: { allowlist: ['input-class1'] } },
+ context: [],
+ });
});
describe('when `document.readyState` does not equal `complete`', () => {
@@ -285,15 +284,14 @@ describe('Tracking', () => {
Tracking.flushPendingEvents();
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'trackStructEvent',
- TEST_CATEGORY,
- TEST_ACTION,
- TEST_LABEL,
- undefined,
- undefined,
- [standardContext],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', {
+ category: TEST_CATEGORY,
+ action: TEST_ACTION,
+ label: TEST_LABEL,
+ property: undefined,
+ value: undefined,
+ context: [standardContext],
+ });
});
});
@@ -457,15 +455,14 @@ describe('Tracking', () => {
value: '0',
});
- expect(snowplowSpy).toHaveBeenCalledWith(
- 'trackStructEvent',
- TEST_CATEGORY,
- 'click_input2',
- undefined,
- undefined,
- 0,
- [standardContext],
- );
+ expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', {
+ category: TEST_CATEGORY,
+ action: 'click_input2',
+ label: undefined,
+ property: undefined,
+ value: 0,
+ context: [standardContext],
+ });
});
it('handles checkbox values correctly', () => {
diff --git a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
index cb70ea4e72d..3508bf7cfde 100644
--- a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
+++ b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
@@ -23,10 +23,6 @@ describe('UsageQuotasApp', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSubTitle = () => wrapper.findByTestId('usage-quotas-page-subtitle');
it('renders the view title', () => {
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 3379af3f41c..1a200090805 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
@@ -53,10 +53,6 @@ describe('ProjectStorageApp', () => {
const findUsageQuotasHelpLink = () => wrapper.findByTestId('usage-quotas-help-link');
const findUsageGraph = () => wrapper.findComponent(UsageGraph);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with apollo fetching successful', () => {
let mockApollo;
diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
index ce489f69cad..15758c94436 100644
--- a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
@@ -2,12 +2,7 @@ import { GlTableLite, GlPopover } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ProjectStorageDetail from '~/usage_quotas/storage/components/project_storage_detail.vue';
-import {
- containerRegistryPopoverId,
- containerRegistryId,
- uploadsPopoverId,
- uploadsId,
-} from '~/usage_quotas/storage/constants';
+import { containerRegistryPopoverId, containerRegistryId } from '~/usage_quotas/storage/constants';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { projectData, projectHelpLinks } from '../mock_data';
@@ -47,16 +42,11 @@ describe('ProjectStorageDetail', () => {
const findPopoverById = (id) =>
wrapper.findAllComponents(GlPopover).filter((p) => p.attributes('data-testid') === id);
const findContainerRegistryPopover = () => findPopoverById(containerRegistryPopoverId);
- const findUploadsPopover = () => findPopoverById(uploadsPopoverId);
const findContainerRegistryWarningIcon = () => wrapper.find(`#${containerRegistryPopoverId}`);
- const findUploadsWarningIcon = () => wrapper.find(`#${uploadsPopoverId}`);
beforeEach(() => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
describe('with storage types', () => {
it.each(storageTypes)(
@@ -99,31 +89,19 @@ describe('ProjectStorageDetail', () => {
});
describe.each`
- description | mockStorageTypes | rendersContainerRegistryPopover | rendersUploadsPopover
- ${'without any storage type that has popover'} | ${[generateStorageType()]} | ${false} | ${false}
- ${'with container registry storage type'} | ${[generateStorageType(containerRegistryId)]} | ${true} | ${false}
- ${'with uploads storage type'} | ${[generateStorageType(uploadsId)]} | ${false} | ${true}
- ${'with container registry and uploads storage types'} | ${[generateStorageType(containerRegistryId), generateStorageType(uploadsId)]} | ${true} | ${true}
- `(
- '$description',
- ({ mockStorageTypes, rendersContainerRegistryPopover, rendersUploadsPopover }) => {
- beforeEach(() => {
- createComponent({ storageTypes: mockStorageTypes });
- });
-
- it(`does ${
- rendersContainerRegistryPopover ? '' : ' not'
- } render container registry warning icon and popover`, () => {
- expect(findContainerRegistryWarningIcon().exists()).toBe(rendersContainerRegistryPopover);
- expect(findContainerRegistryPopover().exists()).toBe(rendersContainerRegistryPopover);
- });
+ description | mockStorageTypes | rendersContainerRegistryPopover
+ ${'without any storage type that has popover'} | ${[generateStorageType()]} | ${false}
+ ${'with container registry storage type'} | ${[generateStorageType(containerRegistryId)]} | ${true}
+ `('$description', ({ mockStorageTypes, rendersContainerRegistryPopover }) => {
+ beforeEach(() => {
+ createComponent({ storageTypes: mockStorageTypes });
+ });
- it(`does ${
- rendersUploadsPopover ? '' : ' not'
- } render container registry warning icon and popover`, () => {
- expect(findUploadsWarningIcon().exists()).toBe(rendersUploadsPopover);
- expect(findUploadsPopover().exists()).toBe(rendersUploadsPopover);
- });
- },
- );
+ it(`does ${
+ rendersContainerRegistryPopover ? '' : ' not'
+ } render container registry warning icon and popover`, () => {
+ expect(findContainerRegistryWarningIcon().exists()).toBe(rendersContainerRegistryPopover);
+ expect(findContainerRegistryPopover().exists()).toBe(rendersContainerRegistryPopover);
+ });
+ });
});
diff --git a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
index 1eb3386bfb8..ebe4c4b7f4e 100644
--- a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
@@ -16,17 +16,12 @@ describe('StorageTypeIcon', () => {
const findGlIcon = () => wrapper.findComponent(GlIcon);
describe('rendering icon', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
expected | provided
${'doc-image'} | ${'lfsObjectsSize'}
${'snippet'} | ${'snippetsSize'}
${'infrastructure-registry'} | ${'repositorySize'}
${'package'} | ${'packagesSize'}
- ${'upload'} | ${'uploadsSize'}
${'disk'} | ${'wikiSize'}
${'disk'} | ${'anything-else'}
`(
diff --git a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
index 75b970d937a..2662711076b 100644
--- a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
@@ -28,21 +28,16 @@ describe('Storage Counter usage graph component', () => {
packagesSize: 3000,
containerRegistrySize: 2500,
lfsObjectsSize: 2000,
- buildArtifactsSize: 500,
- pipelineArtifactsSize: 500,
+ buildArtifactsSize: 700,
+ pipelineArtifactsSize: 300,
snippetsSize: 2000,
storageSize: 17000,
- uploadsSize: 1000,
},
limit: 2000,
};
mountComponent(data);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the legend in order', () => {
const types = wrapper.findAll('[data-testid="storage-type-legend"]');
@@ -55,7 +50,6 @@ describe('Storage Counter usage graph component', () => {
repositorySize,
wikiSize,
snippetsSize,
- uploadsSize,
} = data.rootStorageStatistics;
expect(types.at(0).text()).toMatchInterpolatedText(`Wiki ${numberToHumanSize(wikiSize)}`);
@@ -68,16 +62,16 @@ describe('Storage Counter usage graph component', () => {
expect(types.at(3).text()).toMatchInterpolatedText(
`Container Registry ${numberToHumanSize(containerRegistrySize)}`,
);
- expect(types.at(4).text()).toMatchInterpolatedText(
- `LFS storage ${numberToHumanSize(lfsObjectsSize)}`,
- );
+ expect(types.at(4).text()).toMatchInterpolatedText(`LFS ${numberToHumanSize(lfsObjectsSize)}`);
expect(types.at(5).text()).toMatchInterpolatedText(
`Snippets ${numberToHumanSize(snippetsSize)}`,
);
expect(types.at(6).text()).toMatchInterpolatedText(
- `Artifacts ${numberToHumanSize(buildArtifactsSize + pipelineArtifactsSize)}`,
+ `Job artifacts ${numberToHumanSize(buildArtifactsSize)}`,
+ );
+ expect(types.at(7).text()).toMatchInterpolatedText(
+ `Pipeline artifacts ${numberToHumanSize(pipelineArtifactsSize)}`,
);
- expect(types.at(7).text()).toMatchInterpolatedText(`Uploads ${numberToHumanSize(uploadsSize)}`);
});
describe('when storage type is not used', () => {
@@ -116,8 +110,8 @@ describe('Storage Counter usage graph component', () => {
'0.14705882352941177',
'0.11764705882352941',
'0.11764705882352941',
- '0.058823529411764705',
- '0.058823529411764705',
+ '0.041176470588235294',
+ '0.01764705882352941',
]);
});
});
@@ -136,8 +130,8 @@ describe('Storage Counter usage graph component', () => {
'0.14705882352941177',
'0.11764705882352941',
'0.11764705882352941',
- '0.058823529411764705',
- '0.058823529411764705',
+ '0.041176470588235294',
+ '0.01764705882352941',
]);
});
});
diff --git a/spec/frontend/usage_quotas/storage/mock_data.js b/spec/frontend/usage_quotas/storage/mock_data.js
index b1c6be10d80..b4b02f77b52 100644
--- a/spec/frontend/usage_quotas/storage/mock_data.js
+++ b/spec/frontend/usage_quotas/storage/mock_data.js
@@ -19,16 +19,25 @@ export const projectData = {
{
storageType: {
id: 'buildArtifactsSize',
- name: 'Artifacts',
- description: 'Pipeline artifacts and job artifacts, created with CI/CD.',
+ name: 'Job artifacts',
+ description: 'Job artifacts created by CI/CD.',
helpPath: '/build-artifacts',
},
value: 400000,
},
{
storageType: {
+ id: 'pipelineArtifactsSize',
+ name: 'Pipeline artifacts',
+ description: 'Pipeline artifacts created by CI/CD.',
+ helpPath: '/pipeline-artifacts',
+ },
+ value: 400000,
+ },
+ {
+ storageType: {
id: 'lfsObjectsSize',
- name: 'LFS storage',
+ name: 'LFS',
description: 'Audio samples, videos, datasets, and graphics.',
helpPath: '/lsf-objects',
},
@@ -63,15 +72,6 @@ export const projectData = {
},
{
storageType: {
- id: 'uploadsSize',
- name: 'Uploads',
- description: 'File attachments and smaller design graphics.',
- helpPath: '/uploads',
- },
- value: 900000,
- },
- {
- storageType: {
id: 'wikiSize',
name: 'Wiki',
description: 'Wiki content.',
@@ -87,11 +87,11 @@ export const projectHelpLinks = {
containerRegistry: '/container_registry',
usageQuotas: '/usage-quotas',
buildArtifacts: '/build-artifacts',
+ pipelineArtifacts: '/pipeline-artifacts',
lfsObjects: '/lsf-objects',
packages: '/packages',
repository: '/repository',
snippets: '/snippets',
- uploads: '/uploads',
wiki: '/wiki',
};
diff --git a/spec/frontend/user_lists/components/edit_user_list_spec.js b/spec/frontend/user_lists/components/edit_user_list_spec.js
index 5f067d9de3c..21a883aefe0 100644
--- a/spec/frontend/user_lists/components/edit_user_list_spec.js
+++ b/spec/frontend/user_lists/components/edit_user_list_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import EditUserList from '~/user_lists/components/edit_user_list.vue';
import UserListForm from '~/user_lists/components/user_list_form.vue';
import createStore from '~/user_lists/store/edit';
@@ -67,7 +67,7 @@ describe('user_lists/components/edit_user_list', () => {
expect(alert.text()).toContain(message);
});
- it('should not be dismissible', async () => {
+ it('should not be dismissible', () => {
expect(alert.props('dismissible')).toBe(false);
});
@@ -114,7 +114,7 @@ describe('user_lists/components/edit_user_list', () => {
});
it('should redirect to the feature flag details page', () => {
- expect(redirectTo).toHaveBeenCalledWith(userList.path);
+ expect(redirectTo).toHaveBeenCalledWith(userList.path); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/user_lists/components/new_user_list_spec.js b/spec/frontend/user_lists/components/new_user_list_spec.js
index 8683cf2463c..004cfb6ca07 100644
--- a/spec/frontend/user_lists/components/new_user_list_spec.js
+++ b/spec/frontend/user_lists/components/new_user_list_spec.js
@@ -4,7 +4,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import NewUserList from '~/user_lists/components/new_user_list.vue';
import createStore from '~/user_lists/store/new';
import { userList } from 'jest/feature_flags/mock_data';
@@ -58,7 +58,7 @@ describe('user_lists/components/new_user_list', () => {
});
it('should redirect to the feature flag details page', () => {
- expect(redirectTo).toHaveBeenCalledWith(userList.path);
+ expect(redirectTo).toHaveBeenCalledWith(userList.path); // eslint-disable-line import/no-deprecated
});
});
diff --git a/spec/frontend/user_lists/components/user_lists_spec.js b/spec/frontend/user_lists/components/user_lists_spec.js
index 161eb036361..2da2eb0dd5f 100644
--- a/spec/frontend/user_lists/components/user_lists_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_spec.js
@@ -39,11 +39,6 @@ describe('~/user_lists/components/user_lists.vue', () => {
const newButton = () => within(wrapper.element).queryAllByText('New user list');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('without permissions', () => {
const provideData = {
...mockProvide,
@@ -87,7 +82,7 @@ describe('~/user_lists/components/user_lists.vue', () => {
emptyState = wrapper.findComponent(GlEmptyState);
});
- it('should render the empty state', async () => {
+ it('should render the empty state', () => {
expect(emptyState.exists()).toBe(true);
});
diff --git a/spec/frontend/user_lists/components/user_lists_table_spec.js b/spec/frontend/user_lists/components/user_lists_table_spec.js
index 3324b040b86..96e9705f02b 100644
--- a/spec/frontend/user_lists/components/user_lists_table_spec.js
+++ b/spec/frontend/user_lists/components/user_lists_table_spec.js
@@ -22,10 +22,6 @@ describe('User Lists Table', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should display the details of a user list', () => {
expect(wrapper.find('[data-testid="ffUserListName"]').text()).toBe(userList.name);
expect(wrapper.find('[data-testid="ffUserListIds"]').text()).toBe(
diff --git a/spec/frontend/user_lists/store/edit/actions_spec.js b/spec/frontend/user_lists/store/edit/actions_spec.js
index ca56c935ea5..0fd08c1c052 100644
--- a/spec/frontend/user_lists/store/edit/actions_spec.js
+++ b/spec/frontend/user_lists/store/edit/actions_spec.js
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import * as actions from '~/user_lists/store/edit/actions';
import * as types from '~/user_lists/store/edit/mutation_types';
import createState from '~/user_lists/store/edit/state';
@@ -89,7 +89,7 @@ describe('User Lists Edit Actions', () => {
name: updatedList.name,
iid: updatedList.iid,
});
- expect(redirectTo).toHaveBeenCalledWith(userList.path);
+ expect(redirectTo).toHaveBeenCalledWith(userList.path); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/user_lists/store/new/actions_spec.js b/spec/frontend/user_lists/store/new/actions_spec.js
index fa69fa7fa66..7ecf05e380a 100644
--- a/spec/frontend/user_lists/store/new/actions_spec.js
+++ b/spec/frontend/user_lists/store/new/actions_spec.js
@@ -1,6 +1,6 @@
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import * as actions from '~/user_lists/store/new/actions';
import * as types from '~/user_lists/store/new/mutation_types';
import createState from '~/user_lists/store/new/state';
@@ -41,7 +41,7 @@ describe('User Lists Edit Actions', () => {
it('should redirect to the user list page', () => {
return testAction(actions.createUserList, createdList, state, [], [], () => {
expect(Api.createFeatureFlagUserList).toHaveBeenCalledWith('1', createdList);
- expect(redirectTo).toHaveBeenCalledWith(userList.path);
+ expect(redirectTo).toHaveBeenCalledWith(userList.path); // eslint-disable-line import/no-deprecated
});
});
});
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 8ce071c075f..3346735055d 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -1,5 +1,6 @@
import { within } from '@testing-library/dom';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlMergeRequestWithMentions from 'test_fixtures/merge_requests/merge_request_with_mentions.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import UsersCache from '~/lib/utils/users_cache';
import initUserPopovers from '~/user_popovers';
import waitForPromises from 'helpers/wait_for_promises';
@@ -10,10 +11,6 @@ jest.mock('~/api/user_api', () => ({
}));
describe('User Popovers', () => {
- let origGon;
-
- const fixtureTemplate = 'merge_requests/merge_request_with_mentions.html';
-
const selector = '.js-user-link[data-user], .js-user-link[data-user-id]';
const findFixtureLinks = () => Array.from(document.querySelectorAll(selector));
const createUserLink = () => {
@@ -42,7 +39,7 @@ describe('User Popovers', () => {
};
const setupTestSubject = () => {
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(htmlMergeRequestWithMentions);
const usersCacheSpy = () => Promise.resolve(dummyUser);
jest.spyOn(UsersCache, 'retrieveById').mockImplementation((userId) => usersCacheSpy(userId));
@@ -60,15 +57,6 @@ describe('User Popovers', () => {
});
};
- beforeEach(() => {
- origGon = window.gon;
- window.gon = {};
- });
-
- afterEach(() => {
- window.gon = origGon;
- });
-
describe('when signed out', () => {
beforeEach(() => {
setupTestSubject();
@@ -108,7 +96,7 @@ describe('User Popovers', () => {
expect(findPopovers().length).toBe(linksWithUsers.length);
});
- it('for elements added after initial load', async () => {
+ it('for elements added after initial load', () => {
const addedLinks = [createUserLink(), createUserLink()];
addedLinks.forEach((link) => {
document.body.appendChild(link);
@@ -124,7 +112,7 @@ describe('User Popovers', () => {
});
});
- it('does not initialize the popovers for group references', async () => {
+ it('does not initialize the popovers for group references', () => {
const [groupLink] = Array.from(document.querySelectorAll('.js-user-link[data-group]'));
triggerEvent('mouseover', groupLink);
@@ -133,7 +121,7 @@ describe('User Popovers', () => {
expect(findPopovers().length).toBe(0);
});
- it('does not initialize the popovers for @all references', async () => {
+ it('does not initialize the popovers for @all references', () => {
const [projectLink] = Array.from(document.querySelectorAll('.js-user-link[data-project]'));
triggerEvent('mouseover', projectLink);
@@ -142,7 +130,7 @@ describe('User Popovers', () => {
expect(findPopovers().length).toBe(0);
});
- it('does not initialize the user popovers twice for the same element', async () => {
+ it('does not initialize the user popovers twice for the same element', () => {
const [firstUserLink] = findFixtureLinks();
triggerEvent('mouseover', firstUserLink);
jest.runOnlyPendingTimers();
diff --git a/spec/frontend/validators/length_validator_spec.js b/spec/frontend/validators/length_validator_spec.js
new file mode 100644
index 00000000000..ece8238b3e3
--- /dev/null
+++ b/spec/frontend/validators/length_validator_spec.js
@@ -0,0 +1,91 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import LengthValidator, { isAboveMaxLength, isBelowMinLength } from '~/validators/length_validator';
+
+describe('length_validator', () => {
+ describe('isAboveMaxLength', () => {
+ it('should return true if the string is longer than the maximum length', () => {
+ expect(isAboveMaxLength('123456', '5')).toBe(true);
+ });
+
+ it('should return false if the string is shorter than the maximum length', () => {
+ expect(isAboveMaxLength('1234', '5')).toBe(false);
+ });
+ });
+
+ describe('isBelowMinLength', () => {
+ it('should return true if the string is shorter than the minimum length and not empty', () => {
+ expect(isBelowMinLength('1234', '5', 'false')).toBe(true);
+ });
+
+ it('should return false if the string is longer than the minimum length', () => {
+ expect(isBelowMinLength('123456', '5', 'false')).toBe(false);
+ });
+
+ it('should return false if the string is empty and allowed to be empty', () => {
+ expect(isBelowMinLength('', '5', 'true')).toBe(false);
+ });
+
+ it('should return true if the string is empty and not allowed to be empty', () => {
+ expect(isBelowMinLength('', '5', 'false')).toBe(true);
+ });
+ });
+
+ describe('LengthValidator', () => {
+ let input;
+ let validator;
+
+ beforeEach(() => {
+ setHTMLFixture(
+ '<div class="container"><input class="js-validate-length" /><span class="gl-field-error"></span></div>',
+ );
+ input = document.querySelector('input');
+ input.dataset.minLength = '3';
+ input.dataset.maxLength = '5';
+ input.dataset.minLengthMessage = 'Too short';
+ input.dataset.maxLengthMessage = 'Too long';
+ validator = new LengthValidator({ container: '.container' });
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('sets error message for input with value longer than max length', () => {
+ input.value = '123456';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBe('Too long');
+ });
+
+ it('sets error message for input with value shorter than min length', () => {
+ input.value = '12';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBe('Too short');
+ });
+
+ it('does not set error message for input with valid length', () => {
+ input.value = '123';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBeNull();
+ });
+
+ it('does not set error message for empty input if allowEmpty is true', () => {
+ input.dataset.allowEmpty = 'true';
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBeNull();
+ });
+
+ it('sets error message for empty input if allowEmpty is false', () => {
+ input.dataset.allowEmpty = 'false';
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBe('Too short');
+ });
+
+ it('sets error message for empty input if allowEmpty is not defined', () => {
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ expect(validator.errorMessage).toBe('Too short');
+ });
+ });
+});
diff --git a/spec/frontend/vue3migration/compiler_spec.js b/spec/frontend/vue3migration/compiler_spec.js
new file mode 100644
index 00000000000..3623f69fe07
--- /dev/null
+++ b/spec/frontend/vue3migration/compiler_spec.js
@@ -0,0 +1,38 @@
+import { mount } from '@vue/test-utils';
+
+import SlotsWithSameName from './components/slots_with_same_name.vue';
+import VOnceInsideVIf from './components/v_once_inside_v_if.vue';
+import KeyInsideTemplate from './components/key_inside_template.vue';
+import CommentsOnRootLevel from './components/comments_on_root_level.vue';
+import SlotWithComment from './components/slot_with_comment.vue';
+import DefaultSlotWithComment from './components/default_slot_with_comment.vue';
+
+describe('Vue.js 3 compiler edge cases', () => {
+ it('workarounds issue #6063 when same slot is used with whitespace preserve', () => {
+ expect(() => mount(SlotsWithSameName)).not.toThrow();
+ });
+
+ it('workarounds issue #7725 when v-once is used inside v-if', () => {
+ expect(() => mount(VOnceInsideVIf)).not.toThrow();
+ });
+
+ it('renders vue.js 2 component when key is inside template', () => {
+ const wrapper = mount(KeyInsideTemplate);
+ expect(wrapper.text()).toBe('12345');
+ });
+
+ it('passes attributes to component with trailing comments on root level', () => {
+ const wrapper = mount(CommentsOnRootLevel, { propsData: { 'data-testid': 'test' } });
+ expect(wrapper.html()).toBe('<div data-testid="test"></div>');
+ });
+
+ it('treats empty slots with comments as empty', () => {
+ const wrapper = mount(SlotWithComment);
+ expect(wrapper.html()).toBe('<div>Simple</div>');
+ });
+
+ it('treats empty default slot with comments as empty', () => {
+ const wrapper = mount(DefaultSlotWithComment);
+ expect(wrapper.html()).toBe('<div>Simple</div>');
+ });
+});
diff --git a/spec/frontend/vue3migration/components/comments_on_root_level.vue b/spec/frontend/vue3migration/components/comments_on_root_level.vue
new file mode 100644
index 00000000000..78222c059d5
--- /dev/null
+++ b/spec/frontend/vue3migration/components/comments_on_root_level.vue
@@ -0,0 +1,5 @@
+<template>
+ <!-- root level comment -->
+ <div><slot></slot></div>
+ <!-- root level comment -->
+</template>
diff --git a/spec/frontend/vue3migration/components/default_slot_with_comment.vue b/spec/frontend/vue3migration/components/default_slot_with_comment.vue
new file mode 100644
index 00000000000..d2589104a5d
--- /dev/null
+++ b/spec/frontend/vue3migration/components/default_slot_with_comment.vue
@@ -0,0 +1,18 @@
+<script>
+import Simple from './simple.vue';
+
+export default {
+ components: {
+ Simple,
+ },
+};
+</script>
+<template>
+ <simple>
+ <!-- slot comment typical for gitlab-ui, for example -->
+ <!-- slot comment typical for gitlab-ui, for example -->
+ <slot></slot>
+ <!-- slot comment typical for gitlab-ui, for example -->
+ <!-- slot comment typical for gitlab-ui, for example -->
+ </simple>
+</template>
diff --git a/spec/frontend/vue3migration/components/key_inside_template.vue b/spec/frontend/vue3migration/components/key_inside_template.vue
new file mode 100644
index 00000000000..af1f46c44e6
--- /dev/null
+++ b/spec/frontend/vue3migration/components/key_inside_template.vue
@@ -0,0 +1,7 @@
+<template>
+ <div>
+ <template v-for="count in 5"
+ ><span :key="count">{{ count }}</span></template
+ >
+ </div>
+</template>
diff --git a/spec/frontend/vue3migration/components/simple.vue b/spec/frontend/vue3migration/components/simple.vue
new file mode 100644
index 00000000000..1d9854b5b4d
--- /dev/null
+++ b/spec/frontend/vue3migration/components/simple.vue
@@ -0,0 +1,10 @@
+<script>
+export default {
+ name: 'Simple',
+};
+</script>
+<template>
+ <div>
+ <slot>{{ $options.name }}</slot>
+ </div>
+</template>
diff --git a/spec/frontend/vue3migration/components/slot_with_comment.vue b/spec/frontend/vue3migration/components/slot_with_comment.vue
new file mode 100644
index 00000000000..56bb41e432f
--- /dev/null
+++ b/spec/frontend/vue3migration/components/slot_with_comment.vue
@@ -0,0 +1,20 @@
+<script>
+import Simple from './simple.vue';
+
+export default {
+ components: {
+ Simple,
+ },
+};
+</script>
+<template>
+ <simple>
+ <template #default>
+ <!-- slot comment typical for gitlab-ui, for example -->
+ <!-- slot comment typical for gitlab-ui, for example -->
+ <slot></slot>
+ <!-- slot comment typical for gitlab-ui, for example -->
+ <!-- slot comment typical for gitlab-ui, for example -->
+ </template>
+ </simple>
+</template>
diff --git a/spec/frontend/vue3migration/components/slots_with_same_name.vue b/spec/frontend/vue3migration/components/slots_with_same_name.vue
new file mode 100644
index 00000000000..37604cd9f6e
--- /dev/null
+++ b/spec/frontend/vue3migration/components/slots_with_same_name.vue
@@ -0,0 +1,14 @@
+<script>
+import Simple from './simple.vue';
+
+export default {
+ name: 'SlotsWithSameName',
+ components: { Simple },
+};
+</script>
+<template>
+ <simple>
+ <template v-if="true" #default>{{ $options.name }}</template>
+ <template v-else #default>{{ $options.name }}</template>
+ </simple>
+</template>
diff --git a/spec/frontend/vue3migration/components/v_once_inside_v_if.vue b/spec/frontend/vue3migration/components/v_once_inside_v_if.vue
new file mode 100644
index 00000000000..708aa7a96c2
--- /dev/null
+++ b/spec/frontend/vue3migration/components/v_once_inside_v_if.vue
@@ -0,0 +1,12 @@
+<script>
+export default {
+ name: 'VOnceInsideVIf',
+};
+</script>
+<template>
+ <div>
+ <template v-if="true">
+ <div v-once>{{ $options.name }}</div>
+ </template>
+ </div>
+</template>
diff --git a/spec/frontend/vue_compat_test_setup.js b/spec/frontend/vue_compat_test_setup.js
new file mode 100644
index 00000000000..6eba9465c80
--- /dev/null
+++ b/spec/frontend/vue_compat_test_setup.js
@@ -0,0 +1,141 @@
+/* eslint-disable import/no-commonjs */
+const Vue = require('vue');
+const VTU = require('@vue/test-utils');
+const { installCompat: installVTUCompat, fullCompatConfig } = require('vue-test-utils-compat');
+
+function getComponentName(component) {
+ if (!component) {
+ return undefined;
+ }
+
+ return (
+ component.name ||
+ getComponentName(component.extends) ||
+ component.mixins?.find((mixin) => getComponentName(mixin))
+ );
+}
+
+function isLegacyExtendedComponent(component) {
+ return Reflect.has(component, 'super') && component.super.extend({}).super === component.super;
+}
+function unwrapLegacyVueExtendComponent(selector) {
+ return isLegacyExtendedComponent(selector) ? selector.options : selector;
+}
+
+if (global.document) {
+ const compatConfig = {
+ MODE: 2,
+
+ GLOBAL_MOUNT: 'suppress-warning',
+ GLOBAL_EXTEND: 'suppress-warning',
+ GLOBAL_PROTOTYPE: 'suppress-warning',
+ RENDER_FUNCTION: 'suppress-warning',
+
+ INSTANCE_DESTROY: 'suppress-warning',
+ INSTANCE_DELETE: 'suppress-warning',
+
+ INSTANCE_ATTRS_CLASS_STYLE: 'suppress-warning',
+ INSTANCE_CHILDREN: 'suppress-warning',
+ INSTANCE_SCOPED_SLOTS: 'suppress-warning',
+ INSTANCE_LISTENERS: 'suppress-warning',
+ INSTANCE_EVENT_EMITTER: 'suppress-warning',
+ INSTANCE_EVENT_HOOKS: 'suppress-warning',
+ INSTANCE_SET: 'suppress-warning',
+ GLOBAL_OBSERVABLE: 'suppress-warning',
+ GLOBAL_SET: 'suppress-warning',
+ COMPONENT_FUNCTIONAL: 'suppress-warning',
+ COMPONENT_V_MODEL: 'suppress-warning',
+ COMPONENT_ASYNC: 'suppress-warning',
+ CUSTOM_DIR: 'suppress-warning',
+ OPTIONS_BEFORE_DESTROY: 'suppress-warning',
+ OPTIONS_DATA_MERGE: 'suppress-warning',
+ OPTIONS_DATA_FN: 'suppress-warning',
+ OPTIONS_DESTROYED: 'suppress-warning',
+ ATTR_FALSE_VALUE: 'suppress-warning',
+
+ COMPILER_V_ON_NATIVE: 'suppress-warning',
+ COMPILER_V_BIND_OBJECT_ORDER: 'suppress-warning',
+
+ CONFIG_WHITESPACE: 'suppress-warning',
+ CONFIG_OPTION_MERGE_STRATS: 'suppress-warning',
+ PRIVATE_APIS: 'suppress-warning',
+ WATCH_ARRAY: 'suppress-warning',
+ };
+
+ let compatH;
+ Vue.config.compilerOptions.whitespace = 'preserve';
+ Vue.createApp({
+ compatConfig: {
+ MODE: 3,
+ RENDER_FUNCTION: 'suppress-warning',
+ },
+ render(h) {
+ compatH = h;
+ },
+ }).mount(document.createElement('div'));
+
+ Vue.configureCompat(compatConfig);
+ installVTUCompat(VTU, fullCompatConfig, compatH);
+ VTU.config.global.renderStubDefaultSlot = true;
+
+ const noop = () => {};
+
+ VTU.config.plugins.createStubs = ({ name, component: rawComponent, registerStub }) => {
+ const component = unwrapLegacyVueExtendComponent(rawComponent);
+ const hyphenatedName = name.replace(/\B([A-Z])/g, '-$1').toLowerCase();
+
+ const stub = Vue.defineComponent({
+ name: getComponentName(component),
+ props: component.props,
+ model: component.model,
+ methods: Object.fromEntries(
+ Object.entries(component.methods ?? {}).map(([key]) => [key, noop]),
+ ),
+ render() {
+ const {
+ $slots: slots = {},
+ $scopedSlots: scopedSlots = {},
+ $parent: parent,
+ $vnode: vnode,
+ } = this;
+
+ const hasStaticDefaultSlot = 'default' in slots && !('default' in scopedSlots);
+ const isTheOnlyChild = parent?.$.subTree === vnode;
+ // this condition should be altered when https://github.com/vuejs/vue-test-utils/pull/2068 is merged
+ // and our codebase will be updated to include it (@vue/test-utils@1.3.6 I assume)
+ const shouldRenderAllSlots = !hasStaticDefaultSlot && isTheOnlyChild;
+
+ const renderSlotByName = (slotName) => {
+ const slot = scopedSlots[slotName] || slots[slotName];
+ let result;
+ if (typeof slot === 'function') {
+ try {
+ result = slot({});
+ } catch {
+ // intentionally blank
+ }
+ } else {
+ result = slot;
+ }
+ return result;
+ };
+
+ const slotContents = shouldRenderAllSlots
+ ? [...new Set([...Object.keys(slots), ...Object.keys(scopedSlots)])]
+ .map(renderSlotByName)
+ .filter(Boolean)
+ : renderSlotByName('default');
+
+ return Vue.h(`${hyphenatedName || 'anonymous'}-stub`, this.$props, slotContents);
+ },
+ });
+
+ if (typeof component === 'function') {
+ component()?.then?.((resolvedComponent) => {
+ registerStub({ source: resolvedComponent.default, stub });
+ });
+ }
+
+ return stub;
+ };
+}
diff --git a/spec/frontend/vue_merge_request_widget/components/action_buttons.js b/spec/frontend/vue_merge_request_widget/components/action_buttons.js
index 6d714aeaf18..7334f061dc9 100644
--- a/spec/frontend/vue_merge_request_widget/components/action_buttons.js
+++ b/spec/frontend/vue_merge_request_widget/components/action_buttons.js
@@ -11,10 +11,6 @@ function factory(propsData = {}) {
}
describe('MR widget extension actions', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('tertiaryButtons', () => {
it('renders buttons', () => {
factory({
diff --git a/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js
index 063425454d7..4164a7df482 100644
--- a/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/added_commit_message_spec.js
@@ -14,10 +14,6 @@ function factory(propsData) {
}
describe('Widget added commit message', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays changes where not merged when state is closed', () => {
factory({ state: 'closed' });
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index bf208f16d18..a07a60438fb 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
@@ -1,26 +1,34 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlButton, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { createAlert } from '~/flash';
+import { createMockSubscription as createMockApolloSubscription } from 'mock-apollo-client';
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { createAlert } from '~/alert';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
import {
- FETCH_LOADING,
- FETCH_ERROR,
APPROVE_ERROR,
UNAPPROVE_ERROR,
} from '~/vue_merge_request_widget/components/approvals/messages';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
+import approvedBySubscription from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql';
+import { createCanApproveResponse } from 'jest/approvals/mock_data';
+
+Vue.use(VueApollo);
const mockAlertDismiss = jest.fn();
-jest.mock('~/flash', () => ({
+jest.mock('~/alert', () => ({
createAlert: jest.fn().mockImplementation(() => ({
dismiss: mockAlertDismiss,
})),
}));
-const RULE_NAME = 'first_rule';
const TEST_HELP_PATH = 'help/path';
const testApprovedBy = () => [1, 7, 10].map((id) => ({ id }));
const testApprovals = () => ({
@@ -34,20 +42,38 @@ const testApprovals = () => ({
require_password_to_approve: false,
invalid_approvers_rules: [],
});
-const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] });
describe('MRWidget approvals', () => {
+ let mockedSubscription;
let wrapper;
let service;
let mr;
- const createComponent = (props = {}) => {
+ const createComponent = (options = {}, responses = { query: approvedByCurrentUser }) => {
+ mockedSubscription = createMockApolloSubscription();
+
+ const requestHandlers = [[approvedByQuery, jest.fn().mockResolvedValue(responses.query)]];
+ const subscriptionHandlers = [[approvedBySubscription, () => mockedSubscription]];
+ const apolloProvider = createMockApollo(requestHandlers);
+ const provide = {
+ ...options.provide,
+ glFeatures: {
+ realtimeApprovals: options.provide?.glFeatures?.realtimeApprovals || false,
+ },
+ };
+
+ subscriptionHandlers.forEach(([document, stream]) => {
+ apolloProvider.defaultClient.setRequestHandler(document, stream);
+ });
+
wrapper = shallowMount(Approvals, {
+ apolloProvider,
propsData: {
mr,
service,
- ...props,
+ ...options.props,
},
+ provide,
stubs: {
GlSprintf,
},
@@ -68,15 +94,10 @@ describe('MRWidget approvals', () => {
};
const findSummary = () => wrapper.findComponent(ApprovalsSummary);
const findOptionalSummary = () => wrapper.findComponent(ApprovalsSummaryOptional);
- const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]');
beforeEach(() => {
service = {
...{
- fetchApprovals: jest.fn().mockReturnValue(Promise.resolve(testApprovals())),
- fetchApprovalSettings: jest
- .fn()
- .mockReturnValue(Promise.resolve(testApprovalRulesResponse())),
approveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())),
unapproveMergeRequest: jest.fn().mockReturnValue(Promise.resolve(testApprovals())),
approveMergeRequestWithAuth: jest.fn().mockReturnValue(Promise.resolve(testApprovals())),
@@ -93,59 +114,26 @@ describe('MRWidget approvals', () => {
isOpen: true,
state: 'open',
targetProjectFullPath: 'gitlab-org/gitlab',
+ id: 1,
iid: '1',
};
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- });
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('when created', () => {
- it('shows loading message', async () => {
- service = {
- fetchApprovals: jest.fn().mockReturnValue(new Promise(() => {})),
- };
-
- createComponent();
- await nextTick();
- expect(wrapper.text()).toContain(FETCH_LOADING);
- });
-
- it('fetches approvals', () => {
- createComponent();
- expect(service.fetchApprovals).toHaveBeenCalled();
- });
- });
-
- describe('when fetch approvals error', () => {
- beforeEach(() => {
- jest.spyOn(service, 'fetchApprovals').mockReturnValue(Promise.reject());
- createComponent();
- return nextTick();
- });
-
- it('still shows loading message', () => {
- expect(wrapper.text()).toContain(FETCH_LOADING);
- });
-
- it('flashes error', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: FETCH_ERROR });
- });
+ gon.current_user_id = getIdFromGraphQLId(
+ approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes[0].id,
+ );
});
describe('action button', () => {
describe('when mr is closed', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ const response = createCanApproveResponse();
+
mr.isOpen = false;
- mr.approvals.user_has_approved = false;
- mr.approvals.user_can_approve = true;
- createComponent();
- return nextTick();
+ createComponent({}, { query: response });
+ await waitForPromises();
});
it('action is not rendered', () => {
@@ -154,12 +142,12 @@ describe('MRWidget approvals', () => {
});
describe('when user cannot approve', () => {
- beforeEach(() => {
- mr.approvals.user_has_approved = false;
- mr.approvals.user_can_approve = false;
+ beforeEach(async () => {
+ const response = JSON.parse(JSON.stringify(approvedByCurrentUser));
+ response.data.project.mergeRequest.approvedBy.nodes = [];
- createComponent();
- return nextTick();
+ createComponent({}, { query: response });
+ await waitForPromises();
});
it('action is not rendered', () => {
@@ -168,15 +156,16 @@ describe('MRWidget approvals', () => {
});
describe('when user can approve', () => {
+ let canApproveResponse;
+
beforeEach(() => {
- mr.approvals.user_has_approved = false;
- mr.approvals.user_can_approve = true;
+ canApproveResponse = createCanApproveResponse();
});
describe('and MR is unapproved', () => {
- beforeEach(() => {
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ createComponent({}, { query: canApproveResponse });
+ await waitForPromises();
});
it('approve action is rendered', () => {
@@ -190,30 +179,33 @@ describe('MRWidget approvals', () => {
describe('and MR is approved', () => {
beforeEach(() => {
- mr.approvals.approved = true;
+ canApproveResponse.data.project.mergeRequest.approved = true;
});
describe('with no approvers', () => {
- beforeEach(() => {
- mr.approvals.approved_by = [];
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes = [];
+ createComponent({}, { query: canApproveResponse });
+ await nextTick();
});
- it('approve action (with inverted style) is rendered', () => {
- expect(findActionData()).toEqual({
+ it('approve action is rendered', () => {
+ expect(findActionData()).toMatchObject({
variant: 'confirm',
text: 'Approve',
- category: 'secondary',
});
});
});
describe('with approvers', () => {
- beforeEach(() => {
- mr.approvals.approved_by = [{ user: { id: 7 } }];
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes =
+ approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes;
+
+ canApproveResponse.data.project.mergeRequest.approvedBy.nodes[0].id = 2;
+
+ createComponent({}, { query: canApproveResponse });
+ await waitForPromises();
});
it('approve additionally action is rendered', () => {
@@ -227,9 +219,9 @@ describe('MRWidget approvals', () => {
});
describe('when approve action is clicked', () => {
- beforeEach(() => {
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ createComponent({}, { query: canApproveResponse });
+ await waitForPromises();
});
it('shows loading icon', () => {
@@ -258,10 +250,6 @@ describe('MRWidget approvals', () => {
it('emits to eventHub', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
});
-
- it('calls store setApprovals', () => {
- expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals());
- });
});
describe('and error', () => {
@@ -286,12 +274,12 @@ describe('MRWidget approvals', () => {
});
describe('when user has approved', () => {
- beforeEach(() => {
- mr.approvals.user_has_approved = true;
- mr.approvals.user_can_approve = false;
+ beforeEach(async () => {
+ const response = JSON.parse(JSON.stringify(approvedByCurrentUser));
- createComponent();
- return nextTick();
+ createComponent({}, { query: response });
+
+ await waitForPromises();
});
it('revoke action is rendered', () => {
@@ -316,10 +304,6 @@ describe('MRWidget approvals', () => {
it('emits to eventHub', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
});
-
- it('calls store setApprovals', () => {
- expect(mr.setApprovals).toHaveBeenCalledWith(testApprovals());
- });
});
describe('and error', () => {
@@ -329,7 +313,7 @@ describe('MRWidget approvals', () => {
return nextTick();
});
- it('flashes error message', () => {
+ it('alerts error message', () => {
expect(createAlert).toHaveBeenCalledWith({ message: UNAPPROVE_ERROR });
});
});
@@ -338,19 +322,24 @@ describe('MRWidget approvals', () => {
});
describe('approvals optional summary', () => {
+ let optionalApprovalsResponse;
+
+ beforeEach(() => {
+ optionalApprovalsResponse = JSON.parse(JSON.stringify(approvedByCurrentUser));
+ });
+
describe('when no approvals required and no approvers', () => {
beforeEach(() => {
- mr.approvals.approved_by = [];
- mr.approvals.approvals_required = 0;
- mr.approvals.user_has_approved = false;
+ optionalApprovalsResponse.data.project.mergeRequest.approvedBy.nodes = [];
+ optionalApprovalsResponse.data.project.mergeRequest.approvalsRequired = 0;
});
describe('and can approve', () => {
- beforeEach(() => {
- mr.approvals.user_can_approve = true;
+ beforeEach(async () => {
+ optionalApprovalsResponse.data.project.mergeRequest.userPermissions.canApprove = true;
- createComponent();
- return nextTick();
+ createComponent({}, { query: optionalApprovalsResponse });
+ await waitForPromises();
});
it('is shown', () => {
@@ -363,11 +352,9 @@ describe('MRWidget approvals', () => {
});
describe('and cannot approve', () => {
- beforeEach(() => {
- mr.approvals.user_can_approve = false;
-
- createComponent();
- return nextTick();
+ beforeEach(async () => {
+ createComponent({}, { query: optionalApprovalsResponse });
+ await nextTick();
});
it('is shown', () => {
@@ -382,9 +369,9 @@ describe('MRWidget approvals', () => {
});
describe('approvals summary', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
- return nextTick();
+ await nextTick();
});
it('is rendered with props', () => {
@@ -393,41 +380,47 @@ describe('MRWidget approvals', () => {
expect(findOptionalSummary().exists()).toBe(false);
expect(summary.exists()).toBe(true);
expect(summary.props()).toMatchObject({
- projectPath: 'gitlab-org/gitlab',
- iid: '1',
- updatedCount: 0,
+ approvalState: approvedByCurrentUser.data.project.mergeRequest,
});
});
});
- describe('invalid rules', () => {
- beforeEach(() => {
- mr.approvals.merge_request_approvers_available = true;
- createComponent();
- });
+ describe('realtime approvals update', () => {
+ describe('realtime_approvals feature disabled', () => {
+ beforeEach(() => {
+ jest.spyOn(console, 'warn').mockImplementation();
+ createComponent();
+ });
- it('does not render related components', () => {
- expect(findInvalidRules().exists()).toBe(false);
+ it('does not subscribe to the approvals update socket', () => {
+ expect(mr.setApprovals).not.toHaveBeenCalled();
+ mockedSubscription.next({});
+ // eslint-disable-next-line no-console
+ expect(console.warn).toHaveBeenCalledWith(
+ expect.stringMatching('Mock subscription has no observer, this will have no effect'),
+ );
+ expect(mr.setApprovals).not.toHaveBeenCalled();
+ });
});
- describe('when invalid rules are present', () => {
+ describe('realtime_approvals feature enabled', () => {
+ const subscriptionApproval = { approved: true };
+ const subscriptionResponse = {
+ data: { mergeRequestApprovalStateUpdated: subscriptionApproval },
+ };
+
beforeEach(() => {
- mr.approvals.invalid_approvers_rules = [{ name: RULE_NAME }];
- createComponent();
+ createComponent({
+ provide: { glFeatures: { realtimeApprovals: true } },
+ });
});
- it('renders related components', () => {
- const invalidRules = findInvalidRules();
+ it('updates approvals when the subscription data is streamed to the Apollo client', () => {
+ expect(mr.setApprovals).not.toHaveBeenCalled();
- expect(invalidRules.exists()).toBe(true);
+ mockedSubscription.next(subscriptionResponse);
- const invalidRulesText = invalidRules.text();
-
- expect(invalidRulesText).toContain(RULE_NAME);
- expect(invalidRulesText).toContain(
- 'GitLab has approved this rule automatically to unblock the merge request.',
- );
- expect(invalidRulesText).toContain('Learn more.');
+ expect(mr.setApprovals).toHaveBeenCalledWith(subscriptionApproval);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
index e6fb0495947..bf3df70d423 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_optional_spec.js
@@ -13,11 +13,6 @@ describe('MRWidget approvals summary optional', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findHelpLink = () => wrapper.findComponent(GlLink);
describe('when can approve', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
index e75ce7c60c9..2c8d8b11b94 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
@@ -1,11 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mount } from '@vue/test-utils';
-import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_multiple_users.json';
-import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_no_approvals.json';
-import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql.json';
+import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql_multiple_users.json';
+import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql_no_approvals.json';
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import waitForPromises from 'helpers/wait_for_promises';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import {
@@ -14,32 +13,22 @@ import {
APPROVED_BY_YOU_AND_OTHERS,
} from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
-import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approved_by.query.graphql';
Vue.use(VueApollo);
describe('MRWidget approvals summary', () => {
- const originalUserId = gon.current_user_id;
let wrapper;
- const createComponent = (response = approvedByCurrentUser) => {
+ const createComponent = (data = approvedByCurrentUser) => {
wrapper = mount(ApprovalsSummary, {
propsData: {
- projectPath: 'gitlab-org/gitlab',
- iid: '1',
+ approvalState: data.data.project.mergeRequest,
},
- apolloProvider: createMockApollo([[approvedByQuery, jest.fn().mockResolvedValue(response)]]),
});
};
const findAvatars = () => wrapper.findComponent(UserAvatarList);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- gon.current_user_id = originalUserId;
- });
-
describe('when approved', () => {
beforeEach(async () => {
createComponent();
@@ -116,4 +105,31 @@ describe('MRWidget approvals summary', () => {
expect(wrapper.findComponent(UserAvatarList).exists()).toBe(false);
});
});
+
+ describe('user avatars list layout', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not add top padding initially', () => {
+ const avatarsList = findAvatars();
+
+ expect(avatarsList.classes()).not.toContain('gl-pt-1');
+ });
+
+ it('adds some top padding when the list is expanded', async () => {
+ const avatarsList = findAvatars();
+ await avatarsList.vm.$emit('expanded');
+
+ expect(avatarsList.classes()).toContain('gl-pt-1');
+ });
+
+ it('removes the top padding when the list collapsed', async () => {
+ const avatarsList = findAvatars();
+ await avatarsList.vm.$emit('expanded');
+ await avatarsList.vm.$emit('collapsed');
+
+ expect(avatarsList.classes()).not.toContain('gl-pt-1');
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
index 52e2393bf05..9516aacea0a 100644
--- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
@@ -26,7 +26,6 @@ describe('Merge Requests Artifacts list app', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
@@ -71,8 +70,8 @@ describe('Merge Requests Artifacts list app', () => {
it('renders disabled buttons', () => {
const buttons = findButtons();
- expect(buttons.at(0).attributes('disabled')).toBe('disabled');
- expect(buttons.at(1).attributes('disabled')).toBe('disabled');
+ expect(buttons.at(0).attributes('disabled')).toBeDefined();
+ expect(buttons.at(1).attributes('disabled')).toBeDefined();
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
index b7bf72cd215..bb049a5d52f 100644
--- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_spec.js
@@ -18,10 +18,6 @@ describe('Artifacts List', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
mountComponent(data);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js
index 198a4c2823a..3a621db7b44 100644
--- a/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/extensions/child_content_spec.js
@@ -20,11 +20,6 @@ function factory(propsData) {
}
describe('MR widget extension child content', () => {
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders child components', () => {
factory({
data: {
diff --git a/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js
index f3aa5bb774f..ffa6b5538d3 100644
--- a/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/extensions/status_icon_spec.js
@@ -11,10 +11,6 @@ function factory(propsData = {}) {
}
describe('MR widget extensions status icon', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders loading icon', () => {
factory({ name: 'test', isLoading: true, iconName: 'failed' });
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
index 81f266d8070..6b22c2e26ac 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
@@ -25,10 +25,6 @@ describe('Merge Request Collapsible Extension', () => {
const findErrorMessage = () => wrapper.find('.js-error-state');
const findIcon = () => wrapper.findComponent(GlIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('while collapsed', () => {
beforeEach(() => {
mountComponent(data);
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js
index 5d923d0383f..01178dab9bb 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_alert_message_spec.js
@@ -11,10 +11,6 @@ function createComponent(propsData = {}) {
}
describe('MrWidgetAlertMessage', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a GlAert', () => {
createComponent({ type: 'danger' });
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js
index 8a42e2e2ce7..7eafccae083 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_spec.js
@@ -29,7 +29,6 @@ describe('MrWidgetAuthor', () => {
});
afterEach(() => {
- wrapper.destroy();
window.gl = oldWindowGl;
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
index 90a29d15488..534b745aed2 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_author_time_spec.js
@@ -23,10 +23,6 @@ describe('MrWidgetAuthorTime', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders provided action text', () => {
expect(wrapper.text()).toContain('Merged by');
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
index 8dadb0c65d0..25de76ba33c 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_container_spec.js
@@ -13,10 +13,6 @@ describe('MrWidgetContainer', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has layout', () => {
factory();
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
index 6a9b019fb4f..090a96d576c 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_icon_spec.js
@@ -15,10 +15,6 @@ describe('MrWidgetIcon', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders icon and container', () => {
expect(wrapper.element.className).toContain('circle-icon-container');
expect(wrapper.findComponent(GlIcon).props('name')).toEqual(TEST_ICON);
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
index 4775a0673b5..33647671853 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
@@ -1,10 +1,12 @@
import axios from 'axios';
+import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
const monitoringUrl = '/root/acets-review-apps/environments/15/metrics';
@@ -35,50 +37,49 @@ const metricsMockData = {
deployment_time: 1493718485,
};
-const createComponent = () => {
- const Component = Vue.extend(MemoryUsage);
-
- return new Component({
- el: document.createElement('div'),
- propsData: {
- metricsUrl: url,
- metricsMonitoringUrl: monitoringUrl,
- memoryMetrics: [],
- deploymentTime: 0,
- hasMetrics: false,
- loadFailed: false,
- loadingMetrics: true,
- backOffRequestCounter: 0,
- },
- });
-};
-
const messages = {
loadingMetrics: 'Loading deployment statistics',
- hasMetrics: 'Memory usage is unchanged at 0MB',
+ hasMetrics: 'Memory usage is unchanged at 0.00MB',
loadFailed: 'Failed to load deployment statistics',
metricsUnavailable: 'Deployment statistics are not available currently',
};
describe('MemoryUsage', () => {
- let vm;
- let el;
+ let wrapper;
let mock;
+ const createComponent = () => {
+ wrapper = shallowMountExtended(MemoryUsage, {
+ propsData: {
+ metricsUrl: url,
+ metricsMonitoringUrl: monitoringUrl,
+ memoryMetrics: [],
+ deploymentTime: 0,
+ hasMetrics: false,
+ loadFailed: false,
+ loadingMetrics: true,
+ backOffRequestCounter: 0,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findUsageInfo = () => wrapper.find('.js-usage-info');
+ const findUsageInfoFailed = () => wrapper.find('.usage-info-failed');
+ const findUsageInfoUnavailable = () => wrapper.find('.usage-info-unavailable');
+ const findMemoryGraph = () => wrapper.findComponent(MemoryGraph);
+
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(`${url}.json`).reply(HTTP_STATUS_OK);
-
- vm = createComponent();
- el = vm.$el;
- });
-
- afterEach(() => {
- mock.restore();
});
describe('data', () => {
it('should have default data', () => {
+ createComponent();
const data = MemoryUsage.data();
expect(Array.isArray(data.memoryMetrics)).toBe(true);
@@ -103,126 +104,182 @@ describe('MemoryUsage', () => {
describe('computed', () => {
describe('memoryChangeMessage', () => {
- it('should contain "increased" if memoryFrom value is less than memoryTo value', () => {
- vm.memoryFrom = 4.28;
- vm.memoryTo = 9.13;
+ it('should contain "increased" if memoryFrom value is less than memoryTo value', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: {
+ ...metricsMockData,
+ metrics: {
+ ...metricsMockData.metrics,
+ memory_after: [
+ {
+ metric: {},
+ value: [1495787020.607, '54858853.130206379'],
+ },
+ ],
+ },
+ },
+ });
- expect(vm.memoryChangeMessage.indexOf('increased')).not.toEqual('-1');
+ createComponent();
+ await waitForPromises();
+
+ expect(findUsageInfo().text().indexOf('increased')).not.toEqual(-1);
});
- it('should contain "decreased" if memoryFrom value is less than memoryTo value', () => {
- vm.memoryFrom = 9.13;
- vm.memoryTo = 4.28;
+ it('should contain "decreased" if memoryFrom value is less than memoryTo value', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: metricsMockData,
+ });
+
+ createComponent();
+ await waitForPromises();
- expect(vm.memoryChangeMessage.indexOf('decreased')).not.toEqual('-1');
+ expect(findUsageInfo().text().indexOf('decreased')).not.toEqual(-1);
});
- it('should contain "unchanged" if memoryFrom value equal to memoryTo value', () => {
- vm.memoryFrom = 1;
- vm.memoryTo = 1;
+ it('should contain "unchanged" if memoryFrom value equal to memoryTo value', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: {
+ ...metricsMockData,
+ metrics: {
+ ...metricsMockData.metrics,
+ memory_after: [
+ {
+ metric: {},
+ value: [1495785220.607, '9572875.906976745'],
+ },
+ ],
+ },
+ },
+ });
+
+ createComponent();
+ await waitForPromises();
- expect(vm.memoryChangeMessage.indexOf('unchanged')).not.toEqual('-1');
+ expect(findUsageInfo().text().indexOf('unchanged')).not.toEqual(-1);
});
});
});
describe('methods', () => {
- const { metrics, deployment_time } = metricsMockData;
+ beforeEach(async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: metricsMockData,
+ });
+
+ createComponent();
+ await waitForPromises();
+ });
describe('getMegabytes', () => {
it('should return Megabytes from provided Bytes value', () => {
- const memoryInBytes = '9572875.906976745';
-
- expect(vm.getMegabytes(memoryInBytes)).toEqual('9.13');
+ expect(findUsageInfo().text()).toContain('9.13MB');
});
});
describe('computeGraphData', () => {
it('should populate sparkline graph', () => {
- // ignore BoostrapVue warnings
- jest.spyOn(console, 'warn').mockImplementation();
-
- vm.computeGraphData(metrics, deployment_time);
- const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
-
- expect(hasMetrics).toBe(true);
- expect(memoryMetrics.length).toBeGreaterThan(0);
- expect(deploymentTime).toEqual(deployment_time);
- expect(memoryFrom).toEqual('9.13');
- expect(memoryTo).toEqual('4.28');
+ expect(findMemoryGraph().exists()).toBe(true);
+ expect(findMemoryGraph().props('metrics')).toHaveLength(1);
+ expect(findUsageInfo().text()).toContain('9.13MB');
+ expect(findUsageInfo().text()).toContain('4.28MB');
});
});
describe('loadMetrics', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
it('should load metrics data using MRWidgetService', async () => {
jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
data: metricsMockData,
});
- jest.spyOn(vm, 'computeGraphData').mockImplementation(() => {});
-
- vm.loadMetrics();
await waitForPromises();
expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
- expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
});
});
});
describe('template', () => {
- it('should render template elements correctly', () => {
- expect(el.classList.contains('mr-memory-usage')).toBe(true);
- expect(el.querySelector('.js-usage-info')).toBeDefined();
- });
+ it('should render template elements correctly', async () => {
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: metricsMockData,
+ });
- it('should show loading metrics message while metrics are being loaded', async () => {
- vm.loadingMetrics = true;
- vm.hasMetrics = false;
- vm.loadFailed = false;
+ createComponent();
+ await waitForPromises();
- await nextTick();
+ expect(wrapper.classes()).toContain('mr-memory-usage');
+ expect(findUsageInfo().exists()).toBe(true);
+ });
+
+ it('should show loading metrics message while metrics are being loaded', () => {
+ createComponent();
- expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
- expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ expect(findUsageInfo().exists()).toBe(true);
+ expect(findUsageInfo().text()).toBe(messages.loadingMetrics);
});
it('should show deployment memory usage when metrics are loaded', async () => {
- // ignore BoostrapVue warnings
- jest.spyOn(console, 'warn').mockImplementation();
-
- vm.loadingMetrics = false;
- vm.hasMetrics = true;
- vm.loadFailed = false;
- vm.memoryMetrics = metricsMockData.metrics.memory_values[0].values;
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: {
+ ...metricsMockData,
+ metrics: {
+ ...metricsMockData.metrics,
+ memory_after: [
+ {
+ metric: {},
+ value: [0, '0'],
+ },
+ ],
+ memory_before: [
+ {
+ metric: {},
+ value: [0, '0'],
+ },
+ ],
+ },
+ },
+ });
- await nextTick();
+ createComponent();
+ await waitForPromises();
- expect(el.querySelector('.memory-graph-container')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
+ expect(findMemoryGraph().exists()).toBe(true);
+ expect(findUsageInfo().text()).toBe(messages.hasMetrics);
});
it('should show failure message when metrics loading failed', async () => {
- vm.loadingMetrics = false;
- vm.hasMetrics = false;
- vm.loadFailed = true;
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockRejectedValue({});
- await nextTick();
+ createComponent();
+ await waitForPromises();
- expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
+ expect(findUsageInfoFailed().exists()).toBe(true);
+ expect(findUsageInfo().text()).toBe(messages.loadFailed);
});
it('should show metrics unavailable message when metrics loading failed', async () => {
- vm.loadingMetrics = false;
- vm.hasMetrics = false;
- vm.loadFailed = false;
+ jest.spyOn(MRWidgetService, 'fetchMetrics').mockResolvedValue({
+ data: {
+ ...metricsMockData,
+ metrics: {
+ ...metricsMockData.metrics,
+ memory_values: [],
+ },
+ },
+ });
- await nextTick();
+ createComponent();
+ await waitForPromises();
- expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
+ expect(findUsageInfoUnavailable().exists()).toBe(true);
+ expect(findUsageInfo().text()).toBe(messages.metricsUnavailable);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
index 13beb43e10b..18842e996de 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
@@ -29,10 +29,6 @@ describe('MrWidgetPipelineContainer', () => {
mock.onGet().reply(HTTP_STATUS_OK, {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDeploymentList = () => wrapper.findComponent(DeploymentList);
const findCIErrorMessage = () => wrapper.findByTestId('ci-error-message');
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 6a899c00b98..820e486c13f 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
@@ -53,13 +53,6 @@ describe('MRWidgetPipeline', () => {
);
};
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
it('should render CI error if there is a pipeline, but no status', () => {
createWrapper({ ciStatus: null }, mount);
expect(findCIErrorMessage().text()).toBe(ciErrorMessage);
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
index ec047fe0714..9bd46267daa 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
@@ -1,54 +1,101 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlModal } from '@gitlab/ui';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
+import rebaseQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
import toast from '~/vue_shared/plugins/global_toast';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
jest.mock('~/vue_shared/plugins/global_toast');
let wrapper;
-
-function createWrapper(propsData) {
- wrapper = mount(WidgetRebase, {
- propsData,
- data() {
- return {
- state: {
- rebaseInProgress: propsData.mr.rebaseInProgress,
- targetBranch: propsData.mr.targetBranch,
+const showMock = jest.fn();
+
+const mockPipelineNodes = [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'user/forked',
+ },
+ },
+];
+
+const mockQueryHandler = ({
+ rebaseInProgress = false,
+ targetBranch = '',
+ pushToSourceBranch = false,
+ nodes = mockPipelineNodes,
+} = {}) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch,
userPermissions: {
- pushToSourceBranch: propsData.mr.canPushToSourceBranch,
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes,
},
},
- };
+ },
+ },
+ });
+
+const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[rebaseQuery, handler]]);
+};
+
+function createWrapper({ propsData = {}, provideData = {}, handler = mockQueryHandler() } = {}) {
+ wrapper = shallowMountExtended(WidgetRebase, {
+ apolloProvider: createMockApolloProvider(handler),
+ provide: {
+ ...provideData,
+ },
+ propsData: {
+ mr: {},
+ service: {},
+ ...propsData,
},
- mocks: {
- $apollo: {
- queries: {
- state: { loading: false },
+ stubs: {
+ StateContainer,
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showMock,
},
- },
+ }),
},
});
}
describe('Merge request widget rebase component', () => {
- const findRebaseMessage = () => wrapper.find('[data-testid="rebase-message"]');
+ const findRebaseMessage = () => wrapper.findByTestId('rebase-message');
+ const findBoldText = () => wrapper.findComponent(BoldText);
const findRebaseMessageText = () => findRebaseMessage().text();
- const findStandardRebaseButton = () => wrapper.find('[data-testid="standard-rebase-button"]');
- const findRebaseWithoutCiButton = () => wrapper.find('[data-testid="rebase-without-ci-button"]');
+ const findStandardRebaseButton = () => wrapper.findByTestId('standard-rebase-button');
+ const findRebaseWithoutCiButton = () => wrapper.findByTestId('rebase-without-ci-button');
+ const findModal = () => wrapper.findComponent(GlModal);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
describe('while rebasing', () => {
- it('should show progress message', () => {
+ it('should show progress message', async () => {
createWrapper({
- mr: { rebaseInProgress: true },
- service: {},
+ handler: mockQueryHandler({ rebaseInProgress: true }),
});
+ await waitForPromises();
+
expect(findRebaseMessageText()).toContain('Rebase in progress');
});
});
@@ -57,95 +104,110 @@ describe('Merge request widget rebase component', () => {
const rebaseMock = jest.fn().mockResolvedValue();
const pollMock = jest.fn().mockResolvedValue({});
- it('renders the warning message', () => {
+ it('renders the warning message', async () => {
createWrapper({
- mr: {
+ handler: mockQueryHandler({
rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
+ pushToSourceBranch: false,
+ }),
});
- const text = findRebaseMessageText();
+ await waitForPromises();
- expect(text).toContain('Merge blocked');
- expect(text.replace(/\s\s+/g, ' ')).toContain(
+ expect(findBoldText().props('message')).toContain('Merge blocked');
+ expect(findBoldText().props('message').replace(/\s\s+/g, ' ')).toContain(
'the source branch must be rebased onto the target branch',
);
});
it('renders an error message when rebasing has failed', async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
+ propsData: {
+ service: {
+ rebase: jest.fn().mockRejectedValue({
+ response: {
+ data: {
+ merge_error: 'Something went wrong!',
+ },
+ },
+ }),
+ },
},
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
});
+ await waitForPromises();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ rebasingError: 'Something went wrong!' });
+ findStandardRebaseButton().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
expect(findRebaseMessageText()).toContain('Something went wrong!');
});
describe('Rebase buttons', () => {
- beforeEach(() => {
+ it('renders both buttons', async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
- },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
});
- });
- it('renders both buttons', () => {
+ await waitForPromises();
+
expect(findRebaseWithoutCiButton().exists()).toBe(true);
expect(findStandardRebaseButton().exists()).toBe(true);
});
it('starts the rebase when clicking', async () => {
- findStandardRebaseButton().vm.$emit('click');
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
- await nextTick();
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
});
it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
- findRebaseWithoutCiButton().vm.$emit('click');
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
- await nextTick();
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
describe('Rebase when pipelines must succeed is enabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- onlyAllowMergeIfPipelineSucceeds: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
},
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
});
+
+ await waitForPromises();
});
it('renders only the rebase button', () => {
@@ -163,19 +225,22 @@ describe('Merge request widget rebase component', () => {
});
describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: true,
- onlyAllowMergeIfPipelineSucceeds: true,
- allowMergeOnSkippedPipeline: true,
- },
- service: {
- rebase: rebaseMock,
- poll: pollMock,
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ allowMergeOnSkippedPipeline: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
},
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
});
+
+ await waitForPromises();
});
it('renders both rebase buttons', () => {
@@ -199,48 +264,99 @@ describe('Merge request widget rebase component', () => {
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
+
+ describe('security modal', () => {
+ it('displays modal and rebases after confirming', async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ provideData: { canCreatePipelineInTargetProject: true },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+ expect(showMock).toHaveBeenCalled();
+
+ findModal().vm.$emit('primary');
+
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+
+ it('does not display modal', async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ provideData: { canCreatePipelineInTargetProject: false },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(showMock).not.toHaveBeenCalled();
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+ });
});
describe('without permissions', () => {
const exampleTargetBranch = 'fake-branch-to-test-with';
describe('UI text', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createWrapper({
- mr: {
- rebaseInProgress: false,
- canPushToSourceBranch: false,
+ handler: mockQueryHandler({
+ pushToSourceBranch: false,
targetBranch: exampleTargetBranch,
- },
- service: {},
+ }),
});
+
+ await waitForPromises();
});
it('renders a message explaining user does not have permissions', () => {
- const text = findRebaseMessageText();
-
- expect(text).toContain('Merge blocked:');
- expect(text).toContain('the source branch must be rebased');
+ expect(findBoldText().props('message')).toContain('Merge blocked');
+ expect(findBoldText().props('message')).toContain('the source branch must be rebased');
});
it('renders the correct target branch name', () => {
- const text = findRebaseMessageText();
-
- expect(text).toContain('Merge blocked:');
- expect(text).toContain('the source branch must be rebased onto the target branch.');
+ expect(findBoldText().props('message')).toContain('Merge blocked:');
+ expect(findBoldText().props('message')).toContain(
+ 'the source branch must be rebased onto the target branch.',
+ );
});
});
- it('does render the "Rebase without pipeline" button', () => {
+ it('does render the "Rebase without pipeline" button', async () => {
createWrapper({
- mr: {
+ handler: mockQueryHandler({
rebaseInProgress: false,
- canPushToSourceBranch: false,
+ pushToSourceBranch: false,
targetBranch: exampleTargetBranch,
- },
- service: {},
+ }),
});
+ await waitForPromises();
+
expect(findRebaseWithoutCiButton().exists()).toBe(true);
});
});
@@ -249,24 +365,27 @@ describe('Merge request widget rebase component', () => {
it('checkRebaseStatus', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
createWrapper({
- mr: {},
- service: {
- rebase() {
- return Promise.resolve();
- },
- poll() {
- return Promise.resolve({
- data: {
- rebase_in_progress: false,
- should_be_rebased: false,
- merge_error: null,
- },
- });
+ propsData: {
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ should_be_rebased: false,
+ merge_error: null,
+ },
+ });
+ },
},
},
});
- wrapper.vm.rebase();
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
// Wait for the rebase request
await nextTick();
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js
index 15522f7ac1d..42a16090510 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_related_links_spec.js
@@ -9,10 +9,6 @@ describe('MRWidgetRelatedLinks', () => {
wrapper = shallowMount(RelatedLinks, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('closesText', () => {
it('returns Closes text for open merge request', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js
index 530549b7b9c..b210327aa31 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_status_icon_spec.js
@@ -17,11 +17,6 @@ describe('MR widget status icon component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('while loading', () => {
it('renders loading icon', () => {
createWrapper({ status: 'loading' });
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
index 73358edee78..70c76687a79 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_suggest_pipeline_spec.js
@@ -18,10 +18,6 @@ describe('MRWidgetSuggestPipeline', () => {
describe('template', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('core functionality', () => {
const findOkBtn = () => wrapper.find('[data-testid="ok"]');
let trackingSpy;
diff --git a/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js
index e393b56034d..aaaa19b16dc 100644
--- a/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/review_app_link_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
@@ -17,10 +18,6 @@ describe('review app link', () => {
wrapper = shallowMount(ReviewAppLink, { propsData: props });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders provided link as href attribute', () => {
expect(wrapper.attributes('href')).toBe(props.link);
});
@@ -37,6 +34,10 @@ describe('review app link', () => {
expect(wrapper.find('svg')).not.toBeNull();
});
+ it('renders unsafe links', () => {
+ expect(wrapper.findComponent(GlButton).props('isUnsafeLink')).toBe(true);
+ });
+
it('tracks an event when clicked', () => {
const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(wrapper.element);
diff --git a/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js
index c0add94e6ed..f520c6a4f78 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/commit_edit_spec.js
@@ -26,10 +26,6 @@ describe('Commits edit component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findTextarea = () => wrapper.find('.form-control');
it('has a correct label', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
index e4448346685..c2ab0e384e8 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
@@ -12,10 +12,6 @@ function factory(propsData = {}) {
}
describe('Merge request widget merge checks failed state component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
mrState | displayText
${{ approvals: true, isApproved: false }} | ${'approvalNeeded'}
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
index c9aca01083d..d321ff6e668 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_failed_pipeline_confirmation_dialog_spec.js
@@ -3,6 +3,8 @@ import MergeFailedPipelineConfirmationDialog from '~/vue_merge_request_widget/co
import { trimText } from 'helpers/text_helper';
describe('MergeFailedPipelineConfirmationDialog', () => {
+ const mockModalHide = jest.fn();
+
let wrapper;
const GlModal = {
@@ -13,7 +15,7 @@ describe('MergeFailedPipelineConfirmationDialog', () => {
</div>
`,
methods: {
- hide: jest.fn(),
+ hide: mockModalHide,
},
};
@@ -38,7 +40,7 @@ describe('MergeFailedPipelineConfirmationDialog', () => {
});
afterEach(() => {
- wrapper.destroy();
+ mockModalHide.mockReset();
});
it('should render informational text explaining why merging immediately can be dangerous', () => {
@@ -54,12 +56,10 @@ describe('MergeFailedPipelineConfirmationDialog', () => {
});
it('when the cancel button is clicked should emit cancel and call hide', () => {
- jest.spyOn(findModal().vm, 'hide');
-
findCancelBtn().vm.$emit('click');
expect(wrapper.emitted('cancel')).toHaveLength(1);
- expect(findModal().vm.hide).toHaveBeenCalled();
+ expect(mockModalHide).toHaveBeenCalled();
});
it('should emit cancel when the hide event is emitted', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
index 08700e834d7..3e18ee75125 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
@@ -10,11 +10,6 @@ describe('MRWidgetArchived', () => {
wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders error icon', () => {
expect(wrapper.findComponent(StateContainer).exists()).toBe(true);
expect(wrapper.findComponent(StateContainer).props().status).toBe('failed');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index fef5fee5f19..65d170cae8b 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -83,8 +83,6 @@ describe('MRWidgetAutoMergeEnabled', () => {
afterEach(() => {
window.gl = oldWindowGl;
- wrapper.destroy();
- wrapper = null;
});
describe('computed', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
index 826f708069c..e65deb2db3d 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -18,10 +18,6 @@ describe('MRWidgetAutoMergeFailed', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
createComponent({
mr: { mergeError },
@@ -48,7 +44,7 @@ describe('MRWidgetAutoMergeFailed', () => {
await nextTick();
- expect(findButton().attributes('disabled')).toBe('disabled');
+ expect(findButton().attributes('disabled')).toBeDefined();
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js
index ac18ccf9e26..6c3b7f76fe6 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_checking_spec.js
@@ -9,11 +9,6 @@ describe('MRWidgetChecking', () => {
wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } });
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders loading icon', () => {
expect(wrapper.findComponent(StateContainer).exists()).toBe(true);
expect(wrapper.findComponent(StateContainer).props().status).toBe('loading');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
index 270a37f87e7..04cc396af40 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_closed_spec.js
@@ -67,12 +67,6 @@ describe('MRWidgetClosed', () => {
wrapper = createComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
it('renders closed icon', () => {
expect(wrapper.findComponent(StateContainer).exists()).toBe(true);
expect(wrapper.findComponent(StateContainer).props().status).toBe('closed');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js
index 5d2d1fdd6f1..e4febda1daa 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js
@@ -36,10 +36,6 @@ describe('Commits message dropdown component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDropdownElements = () => wrapper.findAllComponents(GlDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
index a6d3a6286a7..b3843b066df 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
@@ -21,10 +21,6 @@ describe('Commits header component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findHeaderWrapper = () => wrapper.find('.js-mr-widget-commits-count');
const findCommitToggle = () => wrapper.find('.commit-edit-toggle');
const findTargetBranchMessage = () => wrapper.find('.label-branch');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
index 2ca9dc61745..7f0a171d712 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
@@ -50,10 +50,6 @@ describe('MRWidgetConflicts', () => {
await nextTick();
}
- afterEach(() => {
- wrapper.destroy();
- });
-
// There are two permissions we need to consider:
//
// 1. Is the user allowed to merge to the target branch?
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
index 833fa27d453..38e5422325a 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -28,10 +28,6 @@ describe('MRWidgetFailedToMerge', () => {
jest.spyOn(window, 'clearInterval').mockImplementation();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('interval', () => {
it('sets interval to refresh', () => {
createComponent();
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 a3aa563b516..e44e2834a0e 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
@@ -62,10 +62,6 @@ describe('MRWidgetMerged', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButtonByText = (text) =>
wrapper.findAll('button').wrappers.find((w) => w.text() === text);
const findRemoveSourceBranchButton = () => findButtonByText('Delete source branch');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
index 5408f731b34..85acd5f9a9e 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
@@ -10,6 +10,8 @@ jest.mock('~/lib/utils/simple_poll', () =>
describe('MRWidgetMerging', () => {
let wrapper;
+ const pollMock = jest.fn().mockResolvedValue();
+
const GlEmoji = { template: '<img />' };
beforeEach(() => {
wrapper = shallowMount(MrWidgetMerging, {
@@ -20,7 +22,7 @@ describe('MRWidgetMerging', () => {
transitionStateMachine() {},
},
service: {
- poll: jest.fn().mockResolvedValue(),
+ poll: pollMock,
},
},
stubs: {
@@ -29,10 +31,6 @@ describe('MRWidgetMerging', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders information about merge request being merged', () => {
const message = wrapper.findComponent(BoldText).props('message');
expect(message).toContain('Merging!');
@@ -40,17 +38,11 @@ describe('MRWidgetMerging', () => {
describe('initiateMergePolling', () => {
it('should call simplePoll', () => {
- wrapper.vm.initiateMergePolling();
-
expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 });
});
it('should call handleMergePolling', () => {
- jest.spyOn(wrapper.vm, 'handleMergePolling').mockImplementation(() => {});
-
- wrapper.vm.initiateMergePolling();
-
- expect(wrapper.vm.handleMergePolling).toHaveBeenCalled();
+ expect(pollMock).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
index f29cf55f7ce..fca25b8bb94 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_missing_branch_spec.js
@@ -15,10 +15,6 @@ function factory(sourceBranchRemoved) {
}
describe('MRWidgetMissingBranch', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
sourceBranchRemoved | branchName
${true} | ${'source'}
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
index 42515c597c5..40b053282de 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
@@ -10,11 +10,6 @@ describe('MRWidgetNotAllowed', () => {
wrapper = shallowMount(notAllowedComponent);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders success icon', () => {
expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
expect(wrapper.findComponent(StatusIcon).props().status).toBe('success');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js
index 6de0c06c33d..c8fa1399dcb 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge_spec.js
@@ -1,28 +1,58 @@
-import Vue, { nextTick } from 'vue';
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import NothingToMerge from '~/vue_merge_request_widget/components/states/nothing_to_merge.vue';
describe('NothingToMerge', () => {
- describe('template', () => {
- const Component = Vue.extend(NothingToMerge);
- const newBlobPath = '/foo';
- const vm = new Component({
- el: document.createElement('div'),
+ let wrapper;
+ const newBlobPath = '/foo';
+
+ const defaultProps = {
+ mr: {
+ newBlobPath,
+ },
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = shallowMountExtended(NothingToMerge, {
propsData: {
- mr: { newBlobPath },
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
},
});
+ };
+
+ const findCreateButton = () => wrapper.findByTestId('createFileButton');
+ const findNothingToMergeTextBody = () => wrapper.findByTestId('nothing-to-merge-body');
+
+ describe('With Blob link', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows the component with the correct text and highlights', () => {
+ expect(wrapper.text()).toContain('This merge request contains no changes.');
+ expect(findNothingToMergeTextBody().text()).toContain(
+ 'Use merge requests to propose changes to your project and discuss them with your team. To make changes, push a commit or edit this merge request to use a different branch.',
+ );
+ });
+
+ it('shows the Create file button with the correct attributes', () => {
+ const createButton = findCreateButton();
+
+ expect(createButton.exists()).toBe(true);
+ expect(createButton.attributes('href')).toBe(newBlobPath);
+ });
+ });
- it('should have correct elements', () => {
- expect(vm.$el.classList.contains('mr-widget-body')).toBe(true);
- expect(vm.$el.querySelector('[data-testid="createFileButton"]').href).toContain(newBlobPath);
- expect(vm.$el.innerText).toContain('Use merge requests to propose changes to your project');
+ describe('Without Blob link', () => {
+ beforeEach(() => {
+ createComponent({ mr: { newBlobPath: '' } });
});
- it('should not show new blob link if there is no link available', () => {
- vm.mr.newBlobPath = null;
- nextTick(() => {
- expect(vm.$el.querySelector('[data-testid="createFileButton"]')).toEqual(null);
- });
+ it('does not show the Create file button', () => {
+ expect(findCreateButton().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
index c0197b5e20a..d99106df0a2 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -10,11 +10,6 @@ describe('MRWidgetPipelineBlocked', () => {
wrapper = shallowMount(PipelineBlockedComponent);
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders error icon', () => {
expect(wrapper.findComponent(StatusIcon).exists()).toBe(true);
expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
index 8bae2b62ed1..ea93463f3ab 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -19,11 +19,6 @@ describe('PipelineFailed', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render error status icon', () => {
createComponent();
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 1e4e089e7c1..07fc0be9e51 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -113,6 +113,11 @@ const createComponent = (customConfig = {}, createState = true) => {
GlSprintf,
},
apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]),
+ provide: {
+ glFeatures: {
+ autoMergeLabelsMrWidget: false,
+ },
+ },
});
};
@@ -596,7 +601,7 @@ describe('ReadyToMerge', () => {
describe('commits edit components', () => {
describe('when fast-forward merge is enabled', () => {
- it('should not be rendered if squash is disabled', async () => {
+ it('should not be rendered if squash is disabled', () => {
createComponent({
mr: {
ffOnlyEnabled: true,
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
index aaa4591d67d..02b71ebf183 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -20,10 +20,6 @@ describe('ShaMismatch', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render warning message', () => {
expect(wrapper.text()).toContain('Merge blocked: new changes were just added.');
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
index c839fa17fe5..97f8e695df9 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -14,10 +14,6 @@ describe('Squash before merge component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findCheckbox = () => wrapper.findComponent(GlFormCheckbox);
describe('checkbox', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
index c97b42f61ac..19825318a4f 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -4,12 +4,19 @@ import { removeBreakLine } from 'helpers/text_helper';
import notesEventHub from '~/notes/event_hub';
import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
-function createComponent({ path = '' } = {}) {
+function createComponent({ path = '', propsData = {}, provide = {} } = {}) {
return mount(UnresolvedDiscussions, {
propsData: {
mr: {
createIssueToResolveDiscussionsPath: path,
},
+ ...propsData,
+ },
+ provide: {
+ glFeatures: {
+ hideCreateIssueResolveAll: false,
+ },
+ ...provide,
},
});
}
@@ -21,11 +28,7 @@ describe('UnresolvedDiscussions', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('triggers the correct notes event when the jump to first unresolved discussion button is clicked', () => {
+ it('triggers the correct notes event when the go to first unresolved discussion button is clicked', () => {
jest.spyOn(notesEventHub, '$emit');
wrapper.find('[data-testid="jump-to-first"]').trigger('click');
@@ -38,17 +41,13 @@ describe('UnresolvedDiscussions', () => {
wrapper = createComponent({ path: TEST_HOST });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should have correct elements', () => {
const text = removeBreakLine(wrapper.text()).trim();
expect(text).toContain('Merge blocked:');
expect(text).toContain('all threads must be resolved.');
- expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
- expect(wrapper.element.innerText).toContain('Create issue to resolve all threads');
+ expect(wrapper.element.innerText).toContain('Resolve all with new issue');
+ expect(wrapper.element.innerText).toContain('Go to first unresolved thread');
expect(wrapper.element.querySelector('.js-create-issue').getAttribute('href')).toEqual(
TEST_HOST,
);
@@ -61,9 +60,26 @@ describe('UnresolvedDiscussions', () => {
expect(text).toContain('Merge blocked:');
expect(text).toContain('all threads must be resolved.');
- expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
- expect(wrapper.element.innerText).not.toContain('Create issue to resolve all threads');
+ expect(wrapper.element.innerText).not.toContain('Resolve all with new issue');
+ expect(wrapper.element.innerText).toContain('Go to first unresolved thread');
expect(wrapper.element.querySelector('.js-create-issue')).toEqual(null);
});
});
+
+ describe('when `hideCreateIssueResolveAll` is enabled', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ path: TEST_HOST,
+ provide: {
+ glFeatures: {
+ hideCreateIssueResolveAll: true,
+ },
+ },
+ });
+ });
+
+ it('do not show jump to first button', () => {
+ expect(wrapper.text()).not.toContain('Create issue to resolve all threads');
+ });
+ });
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js
index 5ec9654a4af..20d06a7aaee 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/new_ready_to_merge_spec.js
@@ -15,10 +15,6 @@ function factory({ canMerge }) {
}
describe('New ready to merge state component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
canMerge
${true}
diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
index e610ceb2122..f46829539a8 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import WorkInProgress, {
MSG_SOMETHING_WENT_WRONG,
MSG_MARK_READY,
@@ -22,8 +22,8 @@ const TEST_MR_IID = '23';
const TEST_MR_TITLE = 'Test MR Title';
const TEST_PROJECT_PATH = 'lorem/ipsum';
-jest.mock('~/flash');
-jest.mock('~/merge_request');
+jest.mock('~/alert');
+jest.mock('~/merge_request', () => ({ toggleDraftStatus: jest.fn() }));
describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () => {
let wrapper;
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 e9a34453930..296d7924243 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,35 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue renders given data 1`] = `
-"<content-row-stub level=\\"2\\" statusiconname=\\"success\\" widgetname=\\"MyWidget\\" header=\\"This is a header,This is a subheader\\" helppopover=\\"[object Object]\\" actionbuttons=\\"\\">
- <div class=\\"gl-display-flex gl-flex-direction-column\\">
- <div>
- <p class=\\"gl-mb-0\\">Main text for the row</p>
- <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
- <!---->
- <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
- Badge is optional. Text to be displayed inside badge
- </gl-badge-stub>
- <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
- <p class=\\"gl-m-0 gl-font-sm\\">Optional: Smaller sub-text to be displayed below the main text</p>
+"<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\\" class=\\"\\">
+ <p class=\\"gl-mb-0\\">Widget help popover content</p>
+ <!---->
+ </help-popover-stub>
+ <!---->
+ </div>
</div>
- <ul class=\\"gl-m-0 gl-p-0 gl-list-style-none\\">
- <li>
- <content-row-stub level=\\"3\\" statusiconname=\\"\\" widgetname=\\"MyWidget\\" header=\\"Child row header\\" actionbuttons=\\"\\" data-qa-selector=\\"child_content\\">
- <div class=\\"gl-display-flex gl-flex-direction-column\\">
- <div>
- <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
- <!---->
- <!---->
- <!---->
- <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
+ <div class=\\"gl-display-flex gl-align-items-baseline\\">
+ <status-icon-stub level=\\"2\\" name=\\"MyWidget\\" iconname=\\"success\\"></status-icon-stub>
+ <div class=\\"gl-display-flex gl-flex-direction-column\\">
+ <div>
+ <p class=\\"gl-mb-0\\">Main text for the row</p>
+ <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
+ <!---->
+ <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
+ Badge is optional. Text to be displayed inside badge
+ </gl-badge-stub>
+ <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
+ <p class=\\"gl-m-0 gl-font-sm\\">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\\">
+ <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>
+ <!---->
+ </div>
+ <div class=\\"gl-display-flex gl-align-items-baseline\\">
+ <!---->
+ <div class=\\"gl-display-flex gl-flex-direction-column\\">
+ <div>
+ <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
+ <!---->
+ <!---->
+ <!---->
+ <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
+ <!---->
+ </div>
+ <!---->
+ </div>
+ </div>
+ </div>
</div>
- <!---->
- </div>
- </content-row-stub>
- </li>
- </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
</div>
-</content-row-stub>"
+</div>"
`;
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
index 366ea113162..adefce9060c 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
@@ -11,10 +11,6 @@ function factory(propsData = {}) {
}
describe('~/vue_merge_request_widget/components/widget/action_buttons.vue', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('tertiaryButtons', () => {
it('renders buttons', () => {
factory({
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
index 527e800ddcf..16751bcc0f0 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/dynamic_content_spec.js
@@ -1,6 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
import DynamicContent from '~/vue_merge_request_widget/components/widget/dynamic_content.vue';
+import ContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', () => {
let wrapper;
@@ -13,6 +14,7 @@ describe('~/vue_merge_request_widget/components/widget/dynamic_content.vue', ()
},
stubs: {
DynamicContent,
+ ContentRow,
},
});
};
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index 973866176c2..4972c522733 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -3,6 +3,7 @@ import * as Sentry from '@sentry/browser';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
+import { assertProps } from 'helpers/assert_props';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import ActionButtons from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
@@ -50,10 +51,6 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('on mount', () => {
it('fetches collapsed', async () => {
const fetchCollapsedData = jest
@@ -115,9 +112,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('validates widget name', () => {
expect(() => {
- createComponent({
- propsData: { widgetName: 'InvalidWidgetName' },
- });
+ assertProps(Widget, { widgetName: 'InvalidWidgetName' });
}).toThrow();
});
});
@@ -125,7 +120,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
describe('fetch', () => {
it('sets the data.collapsed property after a successfull call - multiPolling: false', async () => {
const mockData = { headers: {}, status: HTTP_STATUS_OK, data: { vulnerabilities: [] } };
- createComponent({ propsData: { fetchCollapsedData: async () => mockData } });
+ createComponent({ propsData: { fetchCollapsedData: () => Promise.resolve(mockData) } });
await waitForPromises();
expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null });
});
@@ -291,6 +286,21 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(findExpandedSection().text()).toBe('More complex content');
});
+ it('emits a toggle even when button is toggled', () => {
+ createComponent({
+ propsData: {
+ isCollapsible: true,
+ },
+ slots: {
+ content: '<b>More complex content</b>',
+ },
+ });
+
+ expect(findExpandedSection().exists()).toBe(false);
+ findToggleButton().vm.$emit('click');
+ expect(wrapper.emitted('toggle')).toEqual([[{ expanded: true }]]);
+ });
+
it('does not display the toggle button if isCollapsible is false', () => {
createComponent({
propsData: {
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
index 1bad5dacefa..785515ae846 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_action_button_spec.js
@@ -25,10 +25,6 @@ describe('Deployment action button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when passed only icon via props', () => {
beforeEach(() => {
factory({
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 41df485b0de..f2b78dedf3a 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
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+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 {
@@ -21,7 +21,7 @@ import {
retryDetails,
} from './deployment_mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
@@ -30,11 +30,6 @@ describe('DeploymentAction component', () => {
let executeActionSpy;
const factory = (options = {}) => {
- // This destroys any wrappers created before a nested call to factory reassigns it
- if (wrapper && wrapper.destroy) {
- wrapper.destroy();
- }
-
wrapper = mount(DeploymentActions, options);
};
@@ -54,7 +49,6 @@ describe('DeploymentAction component', () => {
});
afterEach(() => {
- wrapper.destroy();
confirmAction.mockReset();
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js
index 948d7ebab5e..77dac4204db 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_list_spec.js
@@ -28,7 +28,6 @@ describe('~/vue_merge_request_widget/components/deployment/deployment_list.vue',
afterEach(() => {
wrapper?.destroy?.();
- wrapper = null;
});
describe('with few deployments', () => {
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 f310f7669a9..234491c531a 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_spec.js
@@ -16,10 +16,6 @@ describe('Deployment component', () => {
let wrapper;
const factory = (options = {}) => {
- // This destroys any wrappers created before a nested call to factory reassigns it
- if (wrapper && wrapper.destroy) {
- wrapper.destroy();
- }
wrapper = mount(DeploymentComponent, options);
};
@@ -32,10 +28,6 @@ describe('Deployment component', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('always renders DeploymentInfo', () => {
expect(wrapper.findComponent(DeploymentInfo).exists()).toBe(true);
});
diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
index 8994fa522d0..72cfd5dd29f 100644
--- a/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
+++ b/spec/frontend/vue_merge_request_widget/deployment/deployment_view_button_spec.js
@@ -2,7 +2,6 @@ import { GlDropdown, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue';
import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { deploymentMockData } from './deployment_mock_data';
const appButtonText = {
@@ -28,16 +27,11 @@ describe('Deployment View App button', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findReviewAppLink = () => wrapper.findComponent(ReviewAppLink);
const findMrWigdetDeploymentDropdown = () => wrapper.findComponent(GlDropdown);
const findMrWigdetDeploymentDropdownIcon = () =>
wrapper.findByTestId('mr-wigdet-deployment-dropdown-icon');
const findDeployUrlMenuItems = () => wrapper.findAllComponents(GlLink);
- const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
describe('text', () => {
it('renders text as passed', () => {
@@ -46,93 +40,39 @@ describe('Deployment View App button', () => {
});
describe('without changes', () => {
- let deployment;
-
beforeEach(() => {
- deployment = { ...deploymentMockData, changes: null };
- });
-
- describe('with safe url', () => {
- beforeEach(() => {
- createComponent({
- propsData: {
- deployment,
- appButtonText,
- },
- });
- });
-
- it('renders the link to the review app without dropdown', () => {
- expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
- expect(findReviewAppLink().attributes('href')).toBe(deployment.external_url);
+ createComponent({
+ propsData: {
+ deployment: { ...deploymentMockData, changes: null },
+ appButtonText,
+ },
});
});
- describe('without safe URL', () => {
- beforeEach(() => {
- deployment = { ...deployment, external_url: 'postgres://example' };
- createComponent({
- propsData: {
- deployment,
- appButtonText,
- },
- });
- });
-
- it('renders the link as a copy button', () => {
- expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
- expect(findCopyButton().props('text')).toBe(deployment.external_url);
- });
+ it('renders the link to the review app without dropdown', () => {
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
});
});
describe('with a single change', () => {
- let deployment;
- let change;
-
beforeEach(() => {
- [change] = deploymentMockData.changes;
- deployment = { ...deploymentMockData, changes: [change] };
- });
-
- describe('with safe URL', () => {
- beforeEach(() => {
- createComponent({
- propsData: {
- deployment,
- appButtonText,
- },
- });
- });
-
- it('renders the link to the review app without dropdown', () => {
- expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
- expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(false);
+ createComponent({
+ propsData: {
+ deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] },
+ appButtonText,
+ },
});
+ });
- it('renders the link to the review app linked to to the first change', () => {
- const expectedUrl = deploymentMockData.changes[0].external_url;
-
- expect(findReviewAppLink().attributes('href')).toBe(expectedUrl);
- });
+ it('renders the link to the review app without dropdown', () => {
+ expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
+ expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(false);
});
- describe('with unsafe URL', () => {
- beforeEach(() => {
- change = { ...change, external_url: 'postgres://example' };
- deployment = { ...deployment, changes: [change] };
- createComponent({
- propsData: {
- deployment,
- appButtonText,
- },
- });
- });
+ it('renders the link to the review app linked to to the first change', () => {
+ const expectedUrl = deploymentMockData.changes[0].external_url;
- it('renders the link as a copy button', () => {
- expect(findMrWigdetDeploymentDropdown().exists()).toBe(false);
- expect(findCopyButton().props('text')).toBe(change.external_url);
- });
+ expect(findReviewAppLink().attributes('href')).toBe(expectedUrl);
});
});
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 548b68bc103..d2d622d0534 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
@@ -73,7 +73,6 @@ describe('Test report extension', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
index 01049e54a7f..9b1e694d9c4 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
@@ -39,7 +39,6 @@ describe('Accessibility extension', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
@@ -102,7 +101,7 @@ describe('Accessibility extension', () => {
await waitForPromises();
});
- it('displays all report list items in viewport', async () => {
+ it('displays all report list items in viewport', () => {
expect(findAllExtensionListItems()).toHaveLength(7);
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
index 67b327217ef..8d3bf3dd3be 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
@@ -61,7 +61,6 @@ describe('Code Quality extension', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
@@ -185,14 +184,14 @@ describe('Code Quality extension', () => {
await waitForPromises();
});
- it('displays all report list items in viewport', async () => {
- expect(findAllExtensionListItems()).toHaveLength(2);
+ it('displays all report list items in viewport', () => {
+ expect(findAllExtensionListItems()).toHaveLength(4);
});
it('displays report list item formatted', () => {
const text = {
newError: trimText(findAllExtensionListItems().at(0).text().replace(/\s+/g, ' ').trim()),
- resolvedError: findAllExtensionListItems().at(1).text().replace(/\s+/g, ' ').trim(),
+ resolvedError: findAllExtensionListItems().at(2).text().replace(/\s+/g, ' ').trim(),
};
expect(text.newError).toContain(
@@ -203,9 +202,23 @@ describe('Code Quality extension', () => {
);
});
+ it('displays report list item formatted with check_name', () => {
+ const text = {
+ newError: trimText(findAllExtensionListItems().at(1).text().replace(/\s+/g, ' ').trim()),
+ resolvedError: findAllExtensionListItems().at(3).text().replace(/\s+/g, ' ').trim(),
+ };
+
+ expect(text.newError).toContain(
+ 'Minor - Rubocop/Metrics/ParameterLists - Avoid parameter lists longer than 5 parameters. [12/5] in main.rb:3',
+ );
+ expect(text.resolvedError).toContain(
+ 'Minor - Rubocop/Metrics/ParameterLists - Avoid parameter lists longer than 5 parameters. [12/5] Fixed in main.rb:3',
+ );
+ });
+
it('adds fixed indicator (badge) when error is resolved', () => {
- expect(findAllExtensionListItems().at(1).findComponent(GlBadge).exists()).toBe(true);
- expect(findAllExtensionListItems().at(1).findComponent(GlBadge).text()).toEqual(i18n.fixed);
+ expect(findAllExtensionListItems().at(3).findComponent(GlBadge).exists()).toBe(true);
+ expect(findAllExtensionListItems().at(3).findComponent(GlBadge).text()).toEqual(i18n.fixed);
});
it('should not add fixed indicator (badge) when error is new', () => {
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
index cb23b730a93..e66c1521ff5 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
@@ -57,6 +57,13 @@ export const codeQualityResponseResolvedAndNewErrors = {
file_path: 'index.js',
line: 12,
},
+ {
+ description: 'Avoid parameter lists longer than 5 parameters. [12/5]',
+ check_name: 'Rubocop/Metrics/ParameterLists',
+ severity: 'minor',
+ file_path: 'main.rb',
+ line: 3,
+ },
],
resolved_errors: [
{
@@ -65,6 +72,13 @@ export const codeQualityResponseResolvedAndNewErrors = {
file_path: 'index.js',
line: 12,
},
+ {
+ description: 'Avoid parameter lists longer than 5 parameters. [12/5]',
+ check_name: 'Rubocop/Metrics/ParameterLists',
+ severity: 'minor',
+ file_path: 'main.rb',
+ line: 3,
+ },
],
existing_errors: [],
summary: {
diff --git a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
index 13384e1efca..5baed8ff211 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
@@ -48,7 +48,6 @@ describe('Terraform extension', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
});
@@ -83,7 +82,7 @@ describe('Terraform extension', () => {
${'2 valid reports'} | ${{ 0: validPlanWithName, 1: validPlanWithName }} | ${'2 Terraform reports were generated in your pipelines'} | ${''}
${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'}
`('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => {
- beforeEach(async () => {
+ beforeEach(() => {
mockPollingApi(HTTP_STATUS_OK, response, {});
return createComponent();
});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
index 015d394312a..20f1796008a 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
@@ -15,11 +15,6 @@ describe('MRWidgetHowToMerge', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
beforeEach(() => {
mountComponent();
});
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 f37276ad594..64fb2806447 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
@@ -1,9 +1,10 @@
import { GlBadge, GlLink, GlIcon, GlButton, GlDropdown } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import * as Sentry from '@sentry/browser';
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approvals.query.graphql.json';
import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -20,6 +21,7 @@ import {
registerExtension,
registeredExtensions,
} from '~/vue_merge_request_widget/components/extensions';
+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';
@@ -28,6 +30,7 @@ import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
+import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
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';
@@ -60,6 +63,8 @@ jest.mock('@sentry/browser', () => ({
Vue.use(VueApollo);
describe('MrWidgetOptions', () => {
+ let stateQueryHandler;
+ let queryResponse;
let wrapper;
let mock;
@@ -83,37 +88,41 @@ describe('MrWidgetOptions', () => {
afterEach(() => {
mock.restore();
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
-
gl.mrWidgetData = {};
- gon.features = {};
});
- const createComponent = (mrData = mockData, options = {}) => {
- wrapper = mount(MrWidgetOptions, {
+ const createComponent = (mrData = mockData, options = {}, data = {}, fullMount = true) => {
+ const mounting = fullMount ? mount : shallowMount;
+
+ queryResponse = {
+ data: {
+ project: {
+ ...getStateQueryResponse.data.project,
+ mergeRequest: {
+ ...getStateQueryResponse.data.project.mergeRequest,
+ mergeError: mrData.mergeError || null,
+ },
+ },
+ },
+ };
+ stateQueryHandler = jest.fn().mockResolvedValue(queryResponse);
+ wrapper = mounting(MrWidgetOptions, {
propsData: {
mrData: { ...mrData },
},
data() {
- return { loading: false };
+ return {
+ loading: false,
+ ...data,
+ };
},
...options,
apolloProvider: createMockApollo([
- [
- getStateQuery,
- jest.fn().mockResolvedValue({
- data: {
- project: {
- ...getStateQueryResponse.data.project,
- mergeRequest: {
- ...getStateQueryResponse.data.project.mergeRequest,
- mergeError: mrData.mergeError || null,
- },
- },
- },
- }),
- ],
+ [approvalsQuery, jest.fn().mockResolvedValue(approvedByCurrentUser)],
+ [getStateQuery, stateQueryHandler],
[readyToMergeQuery, jest.fn().mockResolvedValue(readyToMergeResponse)],
[
userPermissionsQuery,
@@ -142,7 +151,9 @@ describe('MrWidgetOptions', () => {
return createComponent();
});
- describe('data', () => {
+ // 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();
@@ -151,9 +162,17 @@ describe('MrWidgetOptions', () => {
describe('computed', () => {
describe('componentName', () => {
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip.each`
+ ${'merged'} | ${'mr-widget-merged'}
+ `('should translate $state into $componentName', ({ state, componentName }) => {
+ wrapper.vm.mr.state = state;
+
+ expect(wrapper.vm.componentName).toEqual(componentName);
+ });
+
it.each`
state | componentName
- ${'merged'} | ${'mr-widget-merged'}
${'conflicts'} | ${'mr-widget-conflicts'}
${'shaMismatch'} | ${'sha-mismatch'}
`('should translate $state into $componentName', ({ state, componentName }) => {
@@ -351,18 +370,6 @@ describe('MrWidgetOptions', () => {
});
});
- describe('initPolling', () => {
- it('should call SmartInterval', () => {
- wrapper.vm.initPolling();
-
- expect(SmartInterval).toHaveBeenCalledWith(
- expect.objectContaining({
- callback: wrapper.vm.checkStatus,
- }),
- );
- });
- });
-
describe('initDeploymentsPolling', () => {
it('should call SmartInterval', () => {
wrapper.vm.initDeploymentsPolling();
@@ -473,15 +480,15 @@ describe('MrWidgetOptions', () => {
});
it('should call setFavicon method', async () => {
- wrapper.vm.mr.ciStatusFaviconPath = overlayDataUrl;
+ wrapper.vm.mr.faviconOverlayPath = overlayDataUrl;
await wrapper.vm.setFaviconHelper();
expect(setFaviconOverlay).toHaveBeenCalledWith(overlayDataUrl);
});
- it('should not call setFavicon when there is no ciStatusFaviconPath', async () => {
- wrapper.vm.mr.ciStatusFaviconPath = null;
+ it('should not call setFavicon when there is no faviconOverlayPath', async () => {
+ wrapper.vm.mr.faviconOverlayPath = null;
await wrapper.vm.setFaviconHelper();
expect(faviconElement.getAttribute('href')).toEqual(null);
});
@@ -529,23 +536,64 @@ describe('MrWidgetOptions', () => {
});
});
- describe('resumePolling', () => {
- it('should call stopTimer on pollingInterval', () => {
- jest.spyOn(wrapper.vm.pollingInterval, 'resume').mockImplementation(() => {});
+ describe('Apollo query', () => {
+ const interval = 5;
+ const data = 'foo';
+ const mockCheckStatus = jest.fn().mockResolvedValue({ data });
+ const mockSetGraphqlData = jest.fn();
+ const mockSetData = jest.fn();
- wrapper.vm.resumePolling();
+ beforeEach(() => {
+ wrapper.destroy();
+
+ return createComponent(
+ mockData,
+ {},
+ {
+ pollInterval: interval,
+ startingPollInterval: interval,
+ mr: {
+ setData: mockSetData,
+ setGraphqlData: mockSetGraphqlData,
+ },
+ service: {
+ checkStatus: mockCheckStatus,
+ },
+ },
+ false,
+ );
+ });
- expect(wrapper.vm.pollingInterval.resume).toHaveBeenCalled();
+ describe('normal polling behavior', () => {
+ it('responds to the GraphQL query finishing', () => {
+ expect(mockSetGraphqlData).toHaveBeenCalledWith(queryResponse.data.project);
+ expect(mockCheckStatus).toHaveBeenCalled();
+ expect(mockSetData).toHaveBeenCalledWith(data, undefined);
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1);
+ });
});
- });
- describe('stopPolling', () => {
- it('should call stopTimer on pollingInterval', () => {
- jest.spyOn(wrapper.vm.pollingInterval, 'stopTimer').mockImplementation(() => {});
+ describe('external event control', () => {
+ describe('enablePolling', () => {
+ it('enables the Apollo query polling using the event hub', () => {
+ eventHub.$emit('EnablePolling');
+
+ expect(stateQueryHandler).toHaveBeenCalled();
+ jest.advanceTimersByTime(interval * STATE_QUERY_POLLING_INTERVAL_BACKOFF);
+ expect(stateQueryHandler).toHaveBeenCalledTimes(2);
+ });
+ });
- wrapper.vm.stopPolling();
+ describe('disablePolling', () => {
+ it('disables the Apollo query polling using the event hub', () => {
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.pollingInterval.stopTimer).toHaveBeenCalled();
+ eventHub.$emit('DisablePolling');
+ jest.advanceTimersByTime(interval * STATE_QUERY_POLLING_INTERVAL_BACKOFF);
+
+ expect(stateQueryHandler).toHaveBeenCalledTimes(1); // no additional polling after a real interval timeout
+ });
+ });
});
});
});
@@ -800,7 +848,7 @@ describe('MrWidgetOptions', () => {
});
describe('security widget', () => {
- const setup = async (hasPipeline) => {
+ const setup = (hasPipeline) => {
const mrData = {
...mockData,
...(hasPipeline ? {} : { pipeline: null }),
@@ -815,7 +863,9 @@ describe('MrWidgetOptions', () => {
apolloMock: [
[
securityReportMergeRequestDownloadPathsQuery,
- async () => ({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
+ jest
+ .fn()
+ .mockResolvedValue({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
],
],
});
@@ -890,11 +940,7 @@ describe('MrWidgetOptions', () => {
});
describe('mock extension', () => {
- let pollRequest;
-
beforeEach(() => {
- pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
-
registerExtension(workingExtension());
createComponent();
@@ -945,10 +991,6 @@ describe('MrWidgetOptions', () => {
expect(collapsedSection.findComponent(GlButton).exists()).toBe(true);
expect(collapsedSection.findComponent(GlButton).text()).toBe('Full report');
});
-
- it('extension polling is not called if enablePolling flag is not passed', () => {
- expect(pollRequest).toHaveBeenCalledTimes(0);
- });
});
describe('expansion', () => {
@@ -1167,33 +1209,6 @@ describe('MrWidgetOptions', () => {
'i_code_review_merge_request_widget_test_extension_count_expand_warning',
);
});
-
- it.each`
- widgetName | nonStandardEvent
- ${'WidgetCodeQuality'} | ${'i_testing_code_quality_widget_total'}
- ${'WidgetTerraform'} | ${'i_testing_terraform_widget_total'}
- ${'WidgetIssues'} | ${'i_testing_issues_widget_total'}
- ${'WidgetTestSummary'} | ${'i_testing_summary_widget_total'}
- `(
- "sends non-standard events for the '$widgetName' widget",
- async ({ widgetName, nonStandardEvent }) => {
- const definition = {
- ...workingExtension(),
- name: widgetName,
- };
-
- registerExtension(definition);
- createComponent();
-
- await waitForPromises();
-
- api.trackRedisHllUserEvent.mockClear();
-
- findExtensionToggleButton().trigger('click');
-
- expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(nonStandardEvent);
- },
- );
});
it('triggers the "full report clicked" events when the appropriate button is clicked', () => {
@@ -1235,10 +1250,6 @@ describe('MrWidgetOptions', () => {
});
describe('widget container', () => {
- afterEach(() => {
- delete window.gon.features.refactorSecurityExtension;
- });
-
it('should not be displayed when the refactor_security_extension feature flag is turned off', () => {
createComponent();
expect(findWidgetContainer().exists()).toBe(false);
diff --git a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
index 88d9d0b4cff..a6288b9c725 100644
--- a/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/get_state_key_spec.js
@@ -20,7 +20,7 @@ describe('getStateKey', () => {
};
const bound = getStateKey.bind(context);
- expect(bound()).toEqual(null);
+ expect(bound()).toEqual('checking');
context.detailedMergeStatus = 'MERGEABLE';
diff --git a/spec/frontend/vue_shared/alert_details/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 3bc191d988f..6c2b21053f0 100644
--- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -86,12 +86,10 @@ describe('AlertDetails', () => {
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
mock.restore();
});
+ const findTabs = () => wrapper.findByTestId('alertDetailsTabs');
const findCreateIncidentBtn = () => wrapper.findByTestId('createIncidentBtn');
const findViewIncidentBtn = () => wrapper.findByTestId('viewIncidentBtn');
const findIncidentCreationAlert = () => wrapper.findByTestId('incidentCreationError');
@@ -107,7 +105,7 @@ describe('AlertDetails', () => {
});
it('shows an empty state', () => {
- expect(wrapper.findByTestId('alertDetailsTabs').exists()).toBe(false);
+ expect(findTabs().exists()).toBe(false);
});
});
@@ -349,9 +347,7 @@ describe('AlertDetails', () => {
${1} | ${'metrics'}
${2} | ${'activity'}
`('will navigate to the correct tab via $tabId', ({ index, tabId }) => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ currentTabIndex: index });
+ findTabs().vm.$emit('input', index);
expect($router.push).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
});
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
index 12c5c190e26..217103ab25c 100644
--- a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
@@ -32,10 +32,6 @@ describe('Alert Details Sidebar To Do', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findToDoButton = () => wrapper.find('[data-testid="alert-todo-button"]');
describe('updating the alert to do', () => {
diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
deleted file mode 100644
index 9d84a535d67..00000000000
--- a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
-import AlertMetrics from '~/vue_shared/alert_details/components/alert_metrics.vue';
-
-jest.mock('~/monitoring/stores', () => ({
- monitoringDashboard: {},
-}));
-
-jest.mock('~/monitoring/components/embeds/metric_embed.vue', () => ({
- render(h) {
- return h('div');
- },
-}));
-
-describe('Alert Metrics', () => {
- let wrapper;
- const mock = new MockAdapter(axios);
-
- function mountComponent({ props } = {}) {
- wrapper = shallowMount(AlertMetrics, {
- propsData: {
- ...props,
- },
- });
- }
-
- const findChart = () => wrapper.findComponent(MetricEmbed);
- const findEmptyState = () => wrapper.findComponent({ ref: 'emptyState' });
-
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
- afterAll(() => {
- mock.restore();
- });
-
- describe('Empty state', () => {
- it('should display a message when metrics dashboard url is not provided', () => {
- mountComponent();
- expect(findChart().exists()).toBe(false);
- expect(findEmptyState().text()).toBe("Metrics weren't available in the alerts payload.");
- });
- });
-
- describe('Chart', () => {
- it('should be rendered when dashboard url is provided', async () => {
- mountComponent({ props: { dashboardUrl: 'metrics.url' } });
-
- await waitForPromises();
- await nextTick();
-
- expect(findEmptyState().exists()).toBe(false);
- expect(findChart().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index 2a37ff2b784..98cb2f5cb0b 100644
--- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -45,12 +45,6 @@ describe('AlertManagementStatus', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('sidebar', () => {
it('displays the dropdown status header', () => {
mountComponent({ props: { isSidebar: true } });
diff --git a/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js b/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js
index a2981478954..0ecca0a69b9 100644
--- a/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js
@@ -16,13 +16,6 @@ describe('AlertSummaryRow', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
describe('Alert Summary Row', () => {
beforeEach(() => {
mountComponent({
diff --git a/spec/frontend/vue_shared/alert_details/router_spec.js b/spec/frontend/vue_shared/alert_details/router_spec.js
deleted file mode 100644
index e3efc104862..00000000000
--- a/spec/frontend/vue_shared/alert_details/router_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import createRouter from '~/vue_shared/alert_details/router';
-import setWindowLocation from 'helpers/set_window_location_helper';
-
-const BASE_PATH = '/-/alert_management/1/details';
-const EMPTY_HASH = '';
-const NOOP = () => {};
-
-describe('AlertDetails router', () => {
- const originalLocation = window.location.href;
- let router;
-
- beforeEach(() => {
- setWindowLocation(originalLocation);
- router = createRouter(BASE_PATH);
- });
-
- describe('redirects hash route mode URLs to history route mode', () => {
- it.each`
- hashPath | historyPath
- ${'/#/overview'} | ${'/overview'}
- ${'#/overview'} | ${'/overview'}
- ${'/#/'} | ${'/'}
- ${'#/'} | ${'/'}
- ${'/#'} | ${'/'}
- ${'#'} | ${'/'}
- ${'/'} | ${'/'}
- ${'/overview'} | ${'/overview'}
- `('should redirect "$hashPath" to "$historyPath"', ({ hashPath, historyPath }) => {
- router.push(hashPath, NOOP);
-
- expect(window.location.hash).toBe(EMPTY_HASH);
- expect(window.location.pathname).toBe(BASE_PATH + historyPath);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
index 98a357bac2b..bf4435fae45 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
@@ -1,21 +1,28 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlDropdownItem } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
import AlertSetAssignees from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar Assignees', () => {
let wrapper;
+ let requestHandlers;
let mock;
const mockPath = '/-/autocomplete/users.json';
+ const mockUrlRoot = '/gitlab';
+ const expectedUrl = `${mockUrlRoot}${mockPath}`;
+
const mockUsers = [
{
avatar_url:
@@ -40,81 +47,64 @@ describe('Alert Details Sidebar Assignees', () => {
const findSidebarIcon = () => wrapper.findByTestId('assignees-icon');
const findUnassigned = () => wrapper.findByTestId('unassigned-users');
+ const mockDefaultHandler = (errors = []) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ issuableSetAssignees: {
+ errors,
+ issuable: {
+ id: 'id',
+ iid: 'iid',
+ assignees: {
+ nodes: [],
+ },
+ notes: {
+ nodes: [],
+ },
+ },
+ },
+ },
+ });
+ const createMockApolloProvider = (handlers) => {
+ Vue.use(VueApollo);
+ requestHandlers = handlers;
+
+ return createMockApollo([[AlertSetAssignees, handlers]]);
+ };
+
function mountComponent({
- data,
- users = [],
- isDropdownSearching = false,
+ props,
sidebarCollapsed = true,
- loading = false,
- stubs = {},
+ handlers = mockDefaultHandler(),
} = {}) {
wrapper = shallowMountExtended(SidebarAssignees, {
- data() {
- return {
- users,
- isDropdownSearching,
- };
- },
+ apolloProvider: createMockApolloProvider(handlers),
propsData: {
alert: { ...mockAlert },
- ...data,
+ ...props,
sidebarCollapsed,
projectPath: 'projectPath',
projectId: '1',
},
- mocks: {
- $apollo: {
- mutate: jest.fn(),
- queries: {
- alert: {
- loading,
- },
- },
- },
- },
- stubs,
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- mock.restore();
- });
-
describe('sidebar expanded', () => {
- const mockUpdatedMutationResult = {
- data: {
- alertSetAssignees: {
- errors: [],
- alert: {
- assigneeUsernames: ['root'],
- },
- },
- },
- };
-
beforeEach(() => {
mock = new MockAdapter(axios);
+ window.gon = {
+ relative_url_root: mockUrlRoot,
+ };
- mock.onGet(mockPath).replyOnce(HTTP_STATUS_OK, mockUsers);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, mockUsers);
mountComponent({
- data: { alert: mockAlert },
+ props: { alert: mockAlert },
sidebarCollapsed: false,
- loading: false,
- users: mockUsers,
- stubs: {
- SidebarAssignee,
- },
});
});
it('renders a unassigned option', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isDropdownSearching: false });
- await nextTick();
+ await waitForPromises();
expect(findDropdown().text()).toBe('Unassigned');
});
@@ -122,60 +112,38 @@ describe('Alert Details Sidebar Assignees', () => {
expect(findSidebarIcon().exists()).toBe(false);
});
- it('calls `$apollo.mutate` with `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isDropdownSearching: false });
-
- await nextTick();
+ it('calls `AlertSetAssignees` mutation and variables containing `iid`, `assigneeUsernames`, & `projectPath`', async () => {
+ await waitForPromises();
wrapper.findComponent(SidebarAssignee).vm.$emit('update-alert-assignees', 'root');
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: AlertSetAssignees,
- variables: {
- iid: '1527542',
- assigneeUsernames: ['root'],
- fullPath: 'projectPath',
- },
+ expect(requestHandlers).toHaveBeenCalledWith({
+ iid: '1527542',
+ assigneeUsernames: ['root'],
+ fullPath: 'projectPath',
});
});
it('emits an error when request contains error messages', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isDropdownSearching: false });
- const errorMutationResult = {
- data: {
- issuableSetAssignees: {
- errors: ['There was a problem for sure.'],
- alert: {},
- },
- },
- };
-
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(errorMutationResult);
+ mountComponent({
+ sidebarCollapsed: false,
+ handlers: mockDefaultHandler(['There was a problem for sure.']),
+ });
+ await waitForPromises();
- await nextTick();
const SideBarAssigneeItem = wrapper.findAllComponents(SidebarAssignee).at(0);
await SideBarAssigneeItem.vm.$emit('update-alert-assignees');
- expect(wrapper.emitted('alert-error')).toBeDefined();
+
+ await waitForPromises();
+ expect(wrapper.emitted('alert-error')).toHaveLength(1);
});
it('stops updating and cancels loading when the request fails', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
- wrapper.vm.updateAlertAssignees('root');
expect(findUnassigned().text()).toBe('assign yourself');
});
it('shows a user avatar, username and full name when a user is set', () => {
mountComponent({
- data: { alert: mockAlerts[1] },
- sidebarCollapsed: false,
- loading: false,
- stubs: {
- SidebarAssignee,
- },
+ props: { alert: mockAlerts[1] },
});
expect(findAssigned().find('img').attributes('src')).toBe('/url');
@@ -188,15 +156,10 @@ describe('Alert Details Sidebar Assignees', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockPath).replyOnce(HTTP_STATUS_OK, mockUsers);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, mockUsers);
mountComponent({
- data: { alert: mockAlert },
- loading: false,
- users: mockUsers,
- stubs: {
- SidebarAssignee,
- },
+ props: { alert: mockAlert },
});
});
it('does not display the status dropdown', () => {
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
index 3b38349622f..89d02cc9de8 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
@@ -44,9 +44,6 @@ describe('Alert Details Sidebar', () => {
}
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
mock.restore();
});
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
index a3adbcf8d3a..7df744cd11d 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
@@ -45,12 +45,6 @@ describe('Alert Details Sidebar Status', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
describe('sidebar expanded', () => {
beforeEach(() => {
mountComponent({
diff --git a/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
index 6a750bb99c0..72c16e8ff22 100644
--- a/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
+++ b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
@@ -17,13 +17,6 @@ describe('Alert Details System Note', () => {
});
}
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
describe('System notes', () => {
beforeEach(() => {
mountComponent({});
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 45d34bcdd3f..b93c64efbcb 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
@@ -17,7 +17,7 @@ exports[`Expand button on click when short text is provided renders button after
role="img"
>
<use
- href="#ellipsis_h"
+ href="file-mock#ellipsis_h"
/>
</svg>
@@ -47,7 +47,7 @@ exports[`Expand button on click when short text is provided renders button after
role="img"
>
<use
- href="#ellipsis_h"
+ href="file-mock#ellipsis_h"
/>
</svg>
@@ -72,7 +72,7 @@ exports[`Expand button when short text is provided renders button before text 1`
role="img"
>
<use
- href="#ellipsis_h"
+ href="file-mock#ellipsis_h"
/>
</svg>
@@ -102,7 +102,7 @@ exports[`Expand button when short text is provided renders button before text 1`
role="img"
>
<use
- href="#ellipsis_h"
+ href="file-mock#ellipsis_h"
/>
</svg>
diff --git a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
deleted file mode 100644
index ca9d4488870..00000000000
--- a/spec/frontend/vue_shared/components/__snapshots__/file_row_header_spec.js.snap
+++ /dev/null
@@ -1,40 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`File row header component adds multiple ellipsises after 40 characters 1`] = `
-<div
- class="file-row-header bg-white sticky-top p-2 js-file-row-header"
- title="app/assets/javascripts/merge_requests/widget/diffs/notes"
->
- <gl-truncate-stub
- class="bold"
- position="middle"
- text="app/assets/javascripts/merge_requests/widget/diffs/notes"
- />
-</div>
-`;
-
-exports[`File row header component renders file path 1`] = `
-<div
- class="file-row-header bg-white sticky-top p-2 js-file-row-header"
- title="app/assets"
->
- <gl-truncate-stub
- class="bold"
- position="middle"
- text="app/assets"
- />
-</div>
-`;
-
-exports[`File row header component trucates path after 40 characters 1`] = `
-<div
- class="file-row-header bg-white sticky-top p-2 js-file-row-header"
- title="app/assets/javascripts/merge_requests"
->
- <gl-truncate-stub
- class="bold"
- position="middle"
- text="app/assets/javascripts/merge_requests"
- />
-</div>
-`;
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
index f3fb840b270..8c2f2b52f8e 100644
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -34,10 +34,6 @@ describe('Actions button component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButton = () => wrapper.findComponent(GlButton);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findDropdown = () => wrapper.findComponent(GlDropdown);
diff --git a/spec/frontend/vue_shared/components/alert_details_table_spec.js b/spec/frontend/vue_shared/components/alert_details_table_spec.js
index 8a9ee4699bd..8e7a10c4d77 100644
--- a/spec/frontend/vue_shared/components/alert_details_table_spec.js
+++ b/spec/frontend/vue_shared/components/alert_details_table_spec.js
@@ -41,11 +41,6 @@ describe('AlertDetails', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTableComponent = () => wrapper.findComponent(GlTable);
const findTableKeys = () => findTableComponent().findAll('tbody td:first-child');
const findTableFieldValueByKey = (fieldKey) =>
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index c7f9d8fd8d5..da5516f8db1 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -64,16 +64,7 @@ const REACTION_CONTROL_CLASSES = [
describe('vue_shared/components/awards_list', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createComponent = (props = {}) => {
- if (wrapper) {
- throw new Error('There should only be one wrapper created per test');
- }
-
wrapper = mount(AwardsList, { propsData: props });
};
const matchingEmojiTag = (name) => expect.stringMatching(`gl-emoji data-name="${name}"`);
@@ -98,7 +89,6 @@ describe('vue_shared/components/awards_list', () => {
addButtonClass: TEST_ADD_BUTTON_CLASS,
});
});
-
it('shows awards in correct order', () => {
expect(findAwardsData()).toEqual([
{
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 ce7fd40937f..6acd1f51a86 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
@@ -24,10 +24,6 @@ describe('Blob Rich Viewer component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the passed content without transformations', () => {
expect(wrapper.html()).toContain(content);
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
index 4b44311b253..a480e0869e8 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js
@@ -23,10 +23,6 @@ describe('Blob Simple Viewer component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('does not fail if content is empty', () => {
const spy = jest.spyOn(window.console, 'error');
createComponent('');
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index ea708b6f3fe..d1b1e58f5d7 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -21,10 +21,6 @@ describe('Changed file icon', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findIcon = () => wrapper.findComponent(GlIcon);
const findIconName = () => findIcon().props('name');
const findIconClasses = () => findIcon().classes();
diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
index 6932a812287..2a40511affb 100644
--- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
+++ b/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
@@ -10,8 +10,6 @@ describe('vue_shared/components/chronic_duration_input', () => {
let hiddenElement;
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
textElement = null;
hiddenElement = null;
});
@@ -22,10 +20,6 @@ describe('vue_shared/components/chronic_duration_input', () => {
};
const createComponent = (props = {}) => {
- if (wrapper) {
- throw new Error('There should only be one wrapper created per test');
- }
-
wrapper = mount(ChronicDurationInput, { propsData: props });
findComponents();
};
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 4f24ec2d015..afb509b9fe6 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -82,10 +82,6 @@ describe('CI Badge Link Component', () => {
wrapper = shallowMount(CiBadgeLink, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each(Object.keys(statuses))('should render badge for status: %s', (status) => {
createComponent({ status: statuses[status] });
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index 2064bee9673..31d63654168 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -7,11 +7,6 @@ describe('CI Icon component', () => {
const findIconWrapper = () => wrapper.find('[data-testid="ci-icon-wrapper"]');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render a span element with an svg', () => {
wrapper = shallowMount(ciIcon, {
propsData: {
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index b18b00e70bb..08a9c2a42d8 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -59,11 +59,6 @@ describe('clipboard button', () => {
expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith('bv::hide::tooltip', 'clipboard-button-1');
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('without gfm', () => {
beforeEach(() => {
createWrapper({
diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
index 31c08260dd0..584e29d94c4 100644
--- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
@@ -21,11 +21,6 @@ describe('Clone Dropdown Button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
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 181692e61b5..25283eb1211 100644
--- a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
@@ -11,10 +11,6 @@ describe('Code Block Highlighted', () => {
wrapper = shallowMount(CodeBlock, { propsData });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders highlighted code if language is supported', async () => {
createComponent({ code, language: 'javascript' });
diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js
index 9a4dbcc47ff..0fdfb96cb23 100644
--- a/spec/frontend/vue_shared/components/code_block_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_spec.js
@@ -13,10 +13,6 @@ describe('Code Block', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('overwrites the default slot', () => {
createComponent({}, { default: 'DEFAULT SLOT' });
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index 060048c4bbd..174e27af948 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -34,10 +34,6 @@ describe('ColorPicker', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('label', () => {
it('hides the label if the label is not passed', () => {
createComponent(shallowMount);
@@ -100,7 +96,7 @@ describe('ColorPicker', () => {
expect(colorTextInput().attributes('class')).not.toContain('is-invalid');
});
- it('shows invalid feedback when the state is marked as invalid', async () => {
+ it('shows invalid feedback when the state is marked as invalid', () => {
createComponent(mount, { invalidFeedback: invalidText, state: false });
expect(invalidFeedback().text()).toBe(invalidText);
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
index fe614f03119..0c9bdc1848d 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
@@ -1,5 +1,5 @@
+import { rgbFromHex } from '@gitlab/ui/dist/utils/utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { hexToRgb } from '~/lib/utils/color_utils';
import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
import { color } from './mock_data';
@@ -20,16 +20,14 @@ describe('ColorItem', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the correct title', () => {
expect(wrapper.text()).toBe(propsData.title);
});
it('renders the correct background color for the color item', () => {
- const convertedColor = hexToRgb(propsData.color).join(', ');
- expect(findColorItem().attributes('style')).toBe(`background-color: rgb(${convertedColor});`);
+ const colorAsRGB = rgbFromHex(propsData.color);
+ expect(findColorItem().attributes('style')).toBe(
+ `background-color: rgb(${colorAsRGB.join(', ')});`,
+ );
});
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
index 5b0772f6e34..f262b03414c 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
@@ -13,7 +13,7 @@ import ColorSelectRoot from '~/vue_shared/components/color_select_dropdown/color
import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
import { colorQueryResponse, updateColorMutationResponse, color } from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
@@ -60,10 +60,6 @@ describe('LabelsSelectRoot', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
const defaultClasses = ['labels-select-wrapper', 'gl-relative'];
@@ -73,7 +69,7 @@ describe('LabelsSelectRoot', () => {
${'embedded'} | ${[...defaultClasses, 'is-embedded']}
`(
'renders component root element with CSS class `$cssClass` when variant is "$variant"',
- async ({ variant, cssClass }) => {
+ ({ variant, cssClass }) => {
createComponent({
propsData: { variant },
});
@@ -145,7 +141,7 @@ describe('LabelsSelectRoot', () => {
await waitForPromises();
});
- it('creates flash with error message', () => {
+ it('creates alert with error message', () => {
expect(createAlert).toHaveBeenCalledWith({
captureError: true,
message: 'Error fetching epic color.',
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
index 303824c77b3..bdb9e8763e2 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
@@ -22,14 +22,10 @@ describe('DropdownContentsColorView', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findColors = () => wrapper.findAllComponents(ColorItem);
const findColorList = () => wrapper.findComponent(GlDropdownForm);
- it('renders color list', async () => {
+ it('renders color list', () => {
expect(findColorList().exists()).toBe(true);
expect(findColors()).toHaveLength(ISSUABLE_COLORS.length);
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
index ee4d3a2630a..2e3a8550e97 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
@@ -2,6 +2,7 @@ import { nextTick } from 'vue';
import { GlDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
+import { stubComponent } from 'helpers/stub_component';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
import DropdownHeader from '~/vue_shared/components/color_select_dropdown/dropdown_header.vue';
@@ -19,31 +20,37 @@ const defaultProps = {
describe('DropdownContent', () => {
let wrapper;
- const createComponent = ({ propsData = {} } = {}) => {
+ const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
wrapper = mountExtended(DropdownContents, {
propsData: {
...defaultProps,
...propsData,
},
+ stubs,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findColorView = () => wrapper.findComponent(DropdownContentsColorView);
const findDropdownHeader = () => wrapper.findComponent(DropdownHeader);
const findDropdown = () => wrapper.findComponent(GlDropdown);
it('calls dropdown `show` method on `isVisible` prop change', async () => {
- createComponent();
- const spy = jest.spyOn(wrapper.vm.$refs.dropdown, 'show');
+ const showDropdown = jest.fn();
+ const hideDropdown = jest.fn();
+ const dropdownStub = {
+ GlDropdown: stubComponent(GlDropdown, {
+ methods: {
+ show: showDropdown,
+ hide: hideDropdown,
+ },
+ }),
+ };
+ createComponent({ stubs: dropdownStub });
await wrapper.setProps({
isVisible: true,
});
- expect(spy).toHaveBeenCalledTimes(1);
+ expect(showDropdown).toHaveBeenCalledTimes(1);
});
it('does not emit `setColor` event on dropdown hide if color did not change', () => {
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
index d203d78477f..6c8aabe1c7f 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
@@ -15,10 +15,6 @@ describe('DropdownHeader', () => {
const findButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
index 5bbdb136353..01d3fde279b 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
@@ -22,10 +22,6 @@ describe('DropdownValue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there is a color set', () => {
it('renders the color', () => {
expect(findColorItems()).toHaveLength(2);
@@ -35,12 +31,9 @@ describe('DropdownValue', () => {
index | cssClass
${0} | ${[]}
${1} | ${['hide-collapsed']}
- `(
- 'passes correct props to the ColorItem with CSS class `$cssClass`',
- async ({ index, cssClass }) => {
- expect(findColorItems().at(index).props()).toMatchObject(propsData.selectedColor);
- expect(findColorItems().at(index).classes()).toEqual(cssClass);
- },
- );
+ `('passes correct props to the ColorItem with CSS class `$cssClass`', ({ index, cssClass }) => {
+ expect(findColorItems().at(index).props()).toMatchObject(propsData.selectedColor);
+ expect(findColorItems().at(index).classes()).toEqual(cssClass);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js
index 1893e127f6f..62a2738d8df 100644
--- a/spec/frontend/vue_shared/components/commit_spec.js
+++ b/spec/frontend/vue_shared/components/commit_spec.js
@@ -24,10 +24,6 @@ describe('Commit component', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a fork icon if it does not represent a tag', () => {
createComponent({
tag: false,
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
index 3f7ec156c19..92cd7597637 100644
--- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -1,14 +1,11 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { WorkspaceType, TYPE_ISSUE, TYPE_EPIC } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-const createComponent = ({
- workspaceType = WorkspaceType.project,
- issuableType = TYPE_ISSUE,
-} = {}) =>
+const createComponent = ({ workspaceType = WORKSPACE_PROJECT, issuableType = TYPE_ISSUE } = {}) =>
shallowMount(ConfidentialityBadge, {
propsData: {
workspaceType,
@@ -23,14 +20,10 @@ describe('ConfidentialityBadge', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
- workspaceType | issuableType | expectedTooltip
- ${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
- ${WorkspaceType.group} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
+ 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.'}
+ ${WORKSPACE_GROUP} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
`(
'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
({ workspaceType, issuableType, expectedTooltip }) => {
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 a660643d74f..d7f94c00d09 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
@@ -24,7 +24,7 @@ describe('Confirm Danger Modal', () => {
const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message');
const findPrimaryAction = () => findModal().props('actionPrimary');
const findCancelAction = () => findModal().props('actionCancel');
- const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
+ const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[attr];
const createComponent = ({ provide = {} } = {}) =>
shallowMountExtended(ConfirmDangerModal, {
@@ -42,10 +42,6 @@ describe('Confirm Danger Modal', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the default warning message', () => {
expect(findDefaultWarning().text()).toBe(CONFIRM_DANGER_WARNING);
});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
index a179afccae0..e082fa4085f 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_spec.js
@@ -32,10 +32,6 @@ describe('Confirm Danger Modal', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the button', () => {
expect(wrapper.html()).toContain(buttonText);
});
@@ -52,7 +48,7 @@ describe('Confirm Danger Modal', () => {
wrapper = createComponent({ disabled: true });
- expect(findBtn().attributes('disabled')).toBe('true');
+ expect(findBtn().attributes('disabled')).toBeDefined();
});
it('passes `buttonClass` prop to button', () => {
diff --git a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
index 1cde92cf522..fbfef5cbe46 100644
--- a/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_fork_modal_spec.js
@@ -21,10 +21,6 @@ describe('vue_shared/components/confirm_fork_modal', () => {
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('visible = false', () => {
beforeEach(() => {
wrapper = createComponent();
diff --git a/spec/frontend/vue_shared/components/confirm_modal_spec.js b/spec/frontend/vue_shared/components/confirm_modal_spec.js
index c1e682a1aae..283ef52cee7 100644
--- a/spec/frontend/vue_shared/components/confirm_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_modal_spec.js
@@ -47,10 +47,6 @@ describe('vue_shared/components/confirm_modal', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findModal = () => wrapper.findComponent(GlModalStub);
const findForm = () => wrapper.find('form');
const findFormData = () =>
diff --git a/spec/frontend/vue_shared/components/content_transition_spec.js b/spec/frontend/vue_shared/components/content_transition_spec.js
index 8bb6d31cce7..5f2b1f096f3 100644
--- a/spec/frontend/vue_shared/components/content_transition_spec.js
+++ b/spec/frontend/vue_shared/components/content_transition_spec.js
@@ -13,11 +13,6 @@ const TEST_SLOTS = [
describe('~/vue_shared/components/content_transition.vue', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createComponent = (props = {}, slots = {}) => {
wrapper = shallowMount(ContentTransition, {
propsData: {
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
index c1495e8264a..a3e5f187f9b 100644
--- 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
@@ -18,10 +18,6 @@ describe('DateTimePickerInput', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders label above the input', () => {
createComponent({
label: inputLabel,
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index aa41df438d2..5620b569409 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -26,10 +26,6 @@ describe('DateTimePicker', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders dropdown toggle button with selected text', async () => {
createComponent();
await nextTick();
diff --git a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
index 79001b9282f..dde2540e121 100644
--- a/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
+++ b/spec/frontend/vue_shared/components/deployment_instance/deployment_instance_spec.js
@@ -17,10 +17,6 @@ describe('Deploy Board Instance', () => {
});
describe('as a non-canary deployment', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a div with the correct css status and tooltip data', () => {
wrapper = createComponent({
tooltipText: 'This is a pod',
@@ -43,10 +39,6 @@ describe('Deploy Board Instance', () => {
});
describe('as a canary deployment', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a div with canary class when stable prop is provided as false', async () => {
wrapper = createComponent({
stable: false,
@@ -58,10 +50,6 @@ describe('Deploy Board Instance', () => {
});
describe('as a legend item', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should not have a tooltip', () => {
wrapper = createComponent();
diff --git a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
index 353d493add9..ca9c2b7d381 100644
--- a/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
+++ b/spec/frontend/vue_shared/components/design_management/design_note_pin_spec.js
@@ -16,10 +16,6 @@ describe('Design note pin component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should match the snapshot of note without index', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
index 99c973bdd26..2a4037d76b7 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -58,7 +58,6 @@ describe('Diff Stats Dropdown', () => {
const findChangedFiles = () => findChanged().findAllComponents(GlDropdownItem);
const findNoFilesText = () => findChanged().findComponent(GlDropdownText);
const findCollapsed = () => wrapper.findByTestId('diff-stats-additions-deletions-expanded');
- const findExpanded = () => wrapper.findByTestId('diff-stats-additions-deletions-collapsed');
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
describe('file item', () => {
@@ -88,40 +87,25 @@ describe('Diff Stats Dropdown', () => {
});
describe.each`
- changed | added | deleted | expectedDropdownHeader | expectedAddedDeletedExpanded | expectedAddedDeletedCollapsed
- ${0} | ${0} | ${0} | ${'0 changed files'} | ${'+0 -0'} | ${'with 0 additions and 0 deletions'}
- ${2} | ${0} | ${2} | ${'2 changed files'} | ${'+0 -2'} | ${'with 0 additions and 2 deletions'}
- ${2} | ${2} | ${0} | ${'2 changed files'} | ${'+2 -0'} | ${'with 2 additions and 0 deletions'}
- ${2} | ${1} | ${1} | ${'2 changed files'} | ${'+1 -1'} | ${'with 1 addition and 1 deletion'}
- ${1} | ${0} | ${1} | ${'1 changed file'} | ${'+0 -1'} | ${'with 0 additions and 1 deletion'}
- ${1} | ${1} | ${0} | ${'1 changed file'} | ${'+1 -0'} | ${'with 1 addition and 0 deletions'}
- ${4} | ${2} | ${2} | ${'4 changed files'} | ${'+2 -2'} | ${'with 2 additions and 2 deletions'}
+ changed | added | deleted | expectedDropdownHeader | expectedAddedDeletedCollapsed
+ ${0} | ${0} | ${0} | ${'0 changed files'} | ${'with 0 additions and 0 deletions'}
+ ${2} | ${0} | ${2} | ${'2 changed files'} | ${'with 0 additions and 2 deletions'}
+ ${2} | ${2} | ${0} | ${'2 changed files'} | ${'with 2 additions and 0 deletions'}
+ ${2} | ${1} | ${1} | ${'2 changed files'} | ${'with 1 addition and 1 deletion'}
+ ${1} | ${0} | ${1} | ${'1 changed file'} | ${'with 0 additions and 1 deletion'}
+ ${1} | ${1} | ${0} | ${'1 changed file'} | ${'with 1 addition and 0 deletions'}
+ ${4} | ${2} | ${2} | ${'4 changed files'} | ${'with 2 additions and 2 deletions'}
`(
'when there are $changed changed file(s), $added added and $deleted deleted file(s)',
- ({
- changed,
- added,
- deleted,
- expectedDropdownHeader,
- expectedAddedDeletedExpanded,
- expectedAddedDeletedCollapsed,
- }) => {
+ ({ changed, added, deleted, expectedDropdownHeader, expectedAddedDeletedCollapsed }) => {
beforeEach(() => {
createComponent({ changed, added, deleted });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it(`dropdown header should be '${expectedDropdownHeader}'`, () => {
expect(findChanged().props('text')).toBe(expectedDropdownHeader);
});
- it(`added and deleted count in expanded section should be '${expectedAddedDeletedExpanded}'`, () => {
- expect(findExpanded().text()).toBe(expectedAddedDeletedExpanded);
- });
-
it(`added and deleted count in collapsed section should be '${expectedAddedDeletedCollapsed}'`, () => {
expect(findCollapsed().text()).toBe(expectedAddedDeletedCollapsed);
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
index 6e0717c29d7..694c69fbe9f 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -18,10 +18,6 @@ describe('DiffViewer', () => {
wrapper = mount(DiffViewer, { propsData });
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders image diff', () => {
window.gon = {
relative_url_root: '',
diff --git a/spec/frontend/vue_shared/components/diff_viewer/utils_spec.js b/spec/frontend/vue_shared/components/diff_viewer/utils_spec.js
new file mode 100644
index 00000000000..b95e1ee283e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/diff_viewer/utils_spec.js
@@ -0,0 +1,33 @@
+import { transition } from '~/vue_shared/components/diff_viewer/utils';
+import {
+ TRANSITION_LOAD_START,
+ TRANSITION_LOAD_ERROR,
+ TRANSITION_LOAD_SUCCEED,
+ TRANSITION_ACKNOWLEDGE_ERROR,
+ STATE_IDLING,
+ STATE_LOADING,
+ STATE_ERRORED,
+} from '~/diffs/constants';
+
+describe('transition', () => {
+ it.each`
+ state | transitionEvent | result
+ ${'idle'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
+ ${'idle'} | ${TRANSITION_LOAD_ERROR} | ${STATE_IDLING}
+ ${'idle'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING}
+ ${'idle'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING}
+ ${'loading'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
+ ${'loading'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
+ ${'loading'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING}
+ ${'loading'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_LOADING}
+ ${'errored'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
+ ${'errored'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
+ ${'errored'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_ERRORED}
+ ${'errored'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING}
+ `(
+ 'correctly updates the state to "$result" when it starts as "$state" and the transition is "$transitionEvent"',
+ ({ state, transitionEvent, result }) => {
+ expect(transition(state, transitionEvent)).toBe(result);
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index 16f924b44d8..7863ef45817 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -42,10 +42,6 @@ describe('ImageDiffViewer component', () => {
triggerEvent('mouseup', doc.body);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders image diff for replaced', () => {
createComponent(allProps);
const metaInfoElements = wrapper.findAll('.image-info');
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
index c4358f0d9cb..661db19ff0e 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
@@ -13,10 +13,6 @@ describe('Diff viewer mode changed component', () => {
});
});
- afterEach(() => {
- vm.destroy();
- });
-
it('renders aMode & bMode', () => {
expect(vm.text()).toContain('File mode changed from 123 to 321');
});
diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
index 549388c1a5c..0d536b23c45 100644
--- a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js
@@ -1,173 +1,119 @@
import { shallowMount, mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
+import { GlAlert, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as transitionModule from '~/vue_shared/components/diff_viewer/utils';
import {
+ TRANSITION_ACKNOWLEDGE_ERROR,
TRANSITION_LOAD_START,
TRANSITION_LOAD_ERROR,
TRANSITION_LOAD_SUCCEED,
- TRANSITION_ACKNOWLEDGE_ERROR,
STATE_IDLING,
STATE_LOADING,
- STATE_ERRORED,
} from '~/diffs/constants';
import Renamed from '~/vue_shared/components/diff_viewer/viewers/renamed.vue';
Vue.use(Vuex);
-function createRenamedComponent({ props = {}, store = new Vuex.Store({}), deep = false }) {
+let wrapper;
+let store;
+let event;
+
+const DIFF_FILE_COMMIT_SHA = 'commitsha';
+const DIFF_FILE_SHORT_SHA = 'commitsh';
+const DIFF_FILE_VIEW_PATH = `blob/${DIFF_FILE_COMMIT_SHA}/filename.ext`;
+
+const defaultStore = {
+ modules: {
+ diffs: {
+ namespaced: true,
+ actions: { switchToFullDiffFromRenamedFile: jest.fn().mockResolvedValue() },
+ },
+ },
+};
+const diffFile = {
+ content_sha: DIFF_FILE_COMMIT_SHA,
+ view_path: DIFF_FILE_VIEW_PATH,
+ alternate_viewer: {
+ name: 'text',
+ },
+};
+const defaultProps = { diffFile };
+
+function createRenamedComponent({ props = {}, storeArg = defaultStore, deep = false } = {}) {
+ store = new Vuex.Store(storeArg);
const mnt = deep ? mount : shallowMount;
- return mnt(Renamed, {
- propsData: { ...props },
+ wrapper = mnt(Renamed, {
+ propsData: { ...defaultProps, ...props },
store,
});
}
-describe('Renamed Diff Viewer', () => {
- const DIFF_FILE_COMMIT_SHA = 'commitsha';
- const DIFF_FILE_SHORT_SHA = 'commitsh';
- const DIFF_FILE_VIEW_PATH = `blob/${DIFF_FILE_COMMIT_SHA}/filename.ext`;
- let diffFile;
- let wrapper;
+const findErrorAlert = () => wrapper.findComponent(GlAlert);
+const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+const findShowFullDiffBtn = () => wrapper.findComponent(GlLink);
+const findPlainText = () => wrapper.find('[test-id="plaintext"]');
+describe('Renamed Diff Viewer', () => {
beforeEach(() => {
- diffFile = {
- content_sha: DIFF_FILE_COMMIT_SHA,
- view_path: DIFF_FILE_VIEW_PATH,
- alternate_viewer: {
- name: 'text',
- },
+ event = {
+ preventDefault: jest.fn(),
};
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
- describe('is', () => {
+ describe('when clicking to load full diff', () => {
beforeEach(() => {
- wrapper = createRenamedComponent({ props: { diffFile } });
+ createRenamedComponent();
});
- it.each`
- state | request | result
- ${'idle'} | ${'idle'} | ${true}
- ${'idle'} | ${'loading'} | ${false}
- ${'idle'} | ${'errored'} | ${false}
- ${'loading'} | ${'loading'} | ${true}
- ${'loading'} | ${'idle'} | ${false}
- ${'loading'} | ${'errored'} | ${false}
- ${'errored'} | ${'errored'} | ${true}
- ${'errored'} | ${'idle'} | ${false}
- ${'errored'} | ${'loading'} | ${false}
- `(
- 'returns the $result for "$request" when the state is "$state"',
- ({ request, result, state }) => {
- wrapper.vm.state = state;
+ it('shows a loading state', async () => {
+ expect(findLoadingIcon().exists()).toBe(false);
- expect(wrapper.vm.is(request)).toEqual(result);
- },
- );
- });
+ await findShowFullDiffBtn().vm.$emit('click', event);
- describe('transition', () => {
- beforeEach(() => {
- wrapper = createRenamedComponent({ props: { diffFile } });
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it.each`
- state | transition | result
- ${'idle'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
- ${'idle'} | ${TRANSITION_LOAD_ERROR} | ${STATE_IDLING}
- ${'idle'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING}
- ${'idle'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING}
- ${'loading'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
- ${'loading'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
- ${'loading'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING}
- ${'loading'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_LOADING}
- ${'errored'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING}
- ${'errored'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED}
- ${'errored'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_ERRORED}
- ${'errored'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING}
- `(
- 'correctly updates the state to "$result" when it starts as "$state" and the transition is "$transition"',
- ({ state, transition, result }) => {
- wrapper.vm.state = state;
-
- wrapper.vm.transition(transition);
-
- expect(wrapper.vm.state).toEqual(result);
- },
- );
- });
-
- describe('switchToFull', () => {
- let store;
-
- beforeEach(() => {
- store = new Vuex.Store({
- modules: {
- diffs: {
- namespaced: true,
- actions: { switchToFullDiffFromRenamedFile: () => {} },
- },
- },
- });
-
+ it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', () => {
jest.spyOn(store, 'dispatch');
- wrapper = createRenamedComponent({ props: { diffFile }, store });
- });
-
- afterEach(() => {
- store = null;
- });
-
- it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', async () => {
- store.dispatch.mockResolvedValue();
-
- wrapper.vm.switchToFull();
+ findShowFullDiffBtn().vm.$emit('click', event);
- await nextTick();
expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', {
diffFile,
});
});
it.each`
- after | resolvePromise | resolution
- ${STATE_IDLING} | ${'mockResolvedValue'} | ${'successful'}
- ${STATE_ERRORED} | ${'mockRejectedValue'} | ${'rejected'}
+ after | resolvePromise | resolution
+ ${TRANSITION_LOAD_SUCCEED} | ${'mockResolvedValue'} | ${'successful'}
+ ${TRANSITION_LOAD_ERROR} | ${'mockRejectedValue'} | ${'rejected'}
`(
'moves through the correct states during a $resolution request',
async ({ after, resolvePromise }) => {
- store.dispatch[resolvePromise]();
+ jest.spyOn(transitionModule, 'transition');
+ store.dispatch = jest.fn()[resolvePromise]();
- expect(wrapper.vm.state).toEqual(STATE_IDLING);
+ expect(transitionModule.transition).not.toHaveBeenCalled();
- wrapper.vm.switchToFull();
+ findShowFullDiffBtn().vm.$emit('click', event);
- expect(wrapper.vm.state).toEqual(STATE_LOADING);
+ expect(transitionModule.transition).toHaveBeenCalledWith(
+ STATE_IDLING,
+ TRANSITION_LOAD_START,
+ );
+
+ await waitForPromises();
- await nextTick(); // This tick is needed for when the action (promise) finishes
- await nextTick(); // This tick waits for the state change in the promise .then/.catch to bubble into the component
- expect(wrapper.vm.state).toEqual(after);
+ expect(transitionModule.transition).toHaveBeenCalledTimes(2);
+ expect(transitionModule.transition.mock.calls[1]).toEqual([STATE_LOADING, after]);
},
);
});
describe('clickLink', () => {
- let event;
-
- beforeEach(() => {
- event = {
- preventDefault: jest.fn(),
- };
- });
-
it.each`
alternateViewer | stops | handled
${'text'} | ${true} | ${'should'}
@@ -175,42 +121,51 @@ describe('Renamed Diff Viewer', () => {
`(
'given { alternate_viewer: { name: "$alternateViewer" } }, the click event $handled be handled in the component',
({ alternateViewer, stops }) => {
- wrapper = createRenamedComponent({
- props: {
- diffFile: {
- ...diffFile,
- alternate_viewer: { name: alternateViewer },
- },
+ const props = {
+ diffFile: {
+ ...diffFile,
+ alternate_viewer: { name: alternateViewer },
},
+ };
+
+ createRenamedComponent({
+ props,
});
- jest.spyOn(wrapper.vm, 'switchToFull').mockImplementation(() => {});
+ store.dispatch = jest.fn().mockResolvedValue();
- wrapper.vm.clickLink(event);
+ findShowFullDiffBtn().vm.$emit('click', event);
if (stops) {
expect(event.preventDefault).toHaveBeenCalled();
- expect(wrapper.vm.switchToFull).toHaveBeenCalled();
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'diffs/switchToFullDiffFromRenamedFile',
+ props,
+ );
} else {
expect(event.preventDefault).not.toHaveBeenCalled();
- expect(wrapper.vm.switchToFull).not.toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalled();
}
},
);
});
describe('dismissError', () => {
- let transitionSpy;
-
beforeEach(() => {
- wrapper = createRenamedComponent({ props: { diffFile } });
- transitionSpy = jest.spyOn(wrapper.vm, 'transition');
+ createRenamedComponent({ props: { diffFile } });
});
it(`transitions the component with "${TRANSITION_ACKNOWLEDGE_ERROR}"`, () => {
- wrapper.vm.dismissError();
+ jest.spyOn(transitionModule, 'transition');
+
+ expect(transitionModule.transition).not.toHaveBeenCalled();
+
+ findErrorAlert().vm.$emit('dismiss');
- expect(transitionSpy).toHaveBeenCalledWith(TRANSITION_ACKNOWLEDGE_ERROR);
+ expect(transitionModule.transition).toHaveBeenCalledWith(
+ expect.stringContaining(''),
+ TRANSITION_ACKNOWLEDGE_ERROR,
+ );
});
});
@@ -224,14 +179,19 @@ describe('Renamed Diff Viewer', () => {
`(
'with { alternate_viewer: { name: $nameDisplay } }, renders the component',
({ altViewer }) => {
- const file = { ...diffFile };
-
- file.alternate_viewer.name = altViewer;
- wrapper = createRenamedComponent({ props: { diffFile: file } });
+ createRenamedComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ alternate_viewer: {
+ ...diffFile.alternate_viewer,
+ name: altViewer,
+ },
+ },
+ },
+ });
- expect(wrapper.find('[test-id="plaintext"]').text()).toEqual(
- 'File renamed with no changes.',
- );
+ expect(findPlainText().text()).toBe('File renamed with no changes.');
},
);
@@ -245,15 +205,15 @@ describe('Renamed Diff Viewer', () => {
const file = { ...diffFile };
file.alternate_viewer.name = altType;
- wrapper = createRenamedComponent({
+ createRenamedComponent({
deep: true,
props: { diffFile: file },
});
- const link = wrapper.find('a');
+ const link = findShowFullDiffBtn();
- expect(link.text()).toEqual(linkText);
- expect(link.attributes('href')).toEqual(DIFF_FILE_VIEW_PATH);
+ expect(link.text()).toBe(linkText);
+ expect(link.attributes('href')).toBe(DIFF_FILE_VIEW_PATH);
},
);
});
diff --git a/spec/frontend/vue_shared/components/dismissible_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
index 8b1189f25d5..53e7d9fc7fc 100644
--- a/spec/frontend/vue_shared/components/dismissible_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_alert_spec.js
@@ -16,10 +16,6 @@ describe('vue_shared/components/dismissible_alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAlert = () => wrapper.findComponent(GlAlert);
describe('default', () => {
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
index 7d8581e11e9..6d179434d1d 100644
--- a/spec/frontend/vue_shared/components/dismissible_container_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -11,10 +11,6 @@ describe('DismissibleContainer', () => {
featureId: 'some-feature-id',
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
const findBtn = () => wrapper.find('[data-testid="close"]');
let mockAxios;
diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
index 4b32fbffebe..463fd74f582 100644
--- a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
@@ -12,10 +12,11 @@ describe('Dismissible Feedback Alert', () => {
const featureName = 'Dependency List';
const STORAGE_DISMISSAL_KEY = 'dependency_list_feedback_dismissed';
- const createComponent = ({ mountFn = shallowMount } = {}) => {
+ const createComponent = ({ props, mountFn = shallowMount } = {}) => {
wrapper = mountFn(Component, {
propsData: {
featureName,
+ ...props,
},
stubs: {
GlSprintf,
@@ -23,11 +24,6 @@ describe('Dismissible Feedback Alert', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createFullComponent = () => createComponent({ mountFn: mount });
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -45,6 +41,27 @@ describe('Dismissible Feedback Alert', () => {
});
});
+ describe('with other attributes', () => {
+ const mockTitle = 'My title';
+ const mockVariant = 'warning';
+
+ beforeEach(() => {
+ createComponent({
+ props: {
+ title: mockTitle,
+ variant: mockVariant,
+ },
+ });
+ });
+
+ it('passes props to alert', () => {
+ expect(findAlert().props()).toMatchObject({
+ title: mockTitle,
+ variant: mockVariant,
+ });
+ });
+ });
+
describe('dismissible', () => {
describe('after dismissal', () => {
beforeEach(() => {
diff --git a/spec/frontend/vue_shared/components/dom_element_listener_spec.js b/spec/frontend/vue_shared/components/dom_element_listener_spec.js
index a848c34b7ce..d31e9b867e4 100644
--- a/spec/frontend/vue_shared/components/dom_element_listener_spec.js
+++ b/spec/frontend/vue_shared/components/dom_element_listener_spec.js
@@ -42,10 +42,6 @@ describe('~/vue_shared/components/dom_element_listener.vue', () => {
};
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
index e34ed31b4bf..82130500458 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -11,10 +11,6 @@ describe('DropdownButton component', () => {
wrapper = mount(DropdownButton, { propsData: props, slots });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
describe('dropdownToggleText', () => {
it('returns default toggle text', () => {
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
index dd3e55c82bb..dd5a05a40c6 100644
--- a/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_widget_spec.js
@@ -31,10 +31,6 @@ describe('DropdownWidget component', () => {
},
});
- // We need to mock out `showDropdown` which
- // invokes `show` method of BDropdown used inside GlDropdown.
- // Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
- jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
};
@@ -42,11 +38,6 @@ describe('DropdownWidget component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('passes default selectText prop to dropdown', () => {
expect(findDropdown().props('text')).toBe('Select');
});
@@ -64,7 +55,7 @@ describe('DropdownWidget component', () => {
expect(wrapper.emitted('set-search')).toEqual([[searchTerm]]);
});
- it('renders one selectable item per passed option', async () => {
+ it('renders one selectable item per passed option', () => {
expect(findDropdownItems()).toHaveLength(2);
});
diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
index 119d6448507..4708a5555f8 100644
--- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -39,16 +39,12 @@ describe('DropdownKeyboardNavigation', () => {
},
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('onInit', () => {
beforeEach(() => {
createComponent();
});
- it('should $emit @change with the default index', async () => {
+ it('should $emit @change with the default index', () => {
expect(wrapper.emitted('change')[0]).toStrictEqual([MOCK_DEFAULT_INDEX]);
});
@@ -104,6 +100,25 @@ describe('DropdownKeyboardNavigation', () => {
describe.each`
keyboardAction | direction | index | max | min
+ ${helpers.arrowDown} | ${1} | ${10} | ${10} | ${0}
+ ${helpers.arrowUp} | ${-1} | ${0} | ${10} | ${0}
+ `(
+ 'moving out of bounds with cycle enabled',
+ ({ keyboardAction, direction, index, max, min }) => {
+ beforeEach(() => {
+ createComponent({ index, max, min, enableCycle: true });
+ keyboardAction();
+ });
+
+ it(`in ${direction} direction does $emit correct @change event`, () => {
+ // The first @change`call happens on created() so we test that we only have 1 call
+ expect(wrapper.emitted('change')[1]).toStrictEqual([direction === 1 ? min : max]);
+ });
+ },
+ );
+
+ describe.each`
+ keyboardAction | direction | index | max | min
${helpers.arrowDown} | ${1} | ${0} | ${10} | ${0}
${helpers.arrowUp} | ${-1} | ${10} | ${10} | ${0}
`('moving in bounds', ({ keyboardAction, direction, index, max, min }) => {
diff --git a/spec/frontend/vue_shared/components/ensure_data_spec.js b/spec/frontend/vue_shared/components/ensure_data_spec.js
index eef8b452f5f..217e795bc64 100644
--- a/spec/frontend/vue_shared/components/ensure_data_spec.js
+++ b/spec/frontend/vue_shared/components/ensure_data_spec.js
@@ -59,7 +59,6 @@ describe('EnsureData', () => {
});
afterEach(() => {
- wrapper.destroy();
Sentry.captureException.mockClear();
});
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
index 6b98f6c5e89..6e2e854adae 100644
--- a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -122,6 +122,12 @@ describe('EntitySelect', () => {
});
describe('once a group is selected', () => {
+ it('emits `input` event with the select value', async () => {
+ createComponent();
+ await selectGroup();
+ expect(wrapper.emitted('input')[0]).toEqual(['1']);
+ });
+
it(`uses the selected group's name as the toggle text`, async () => {
createComponent();
await selectGroup();
@@ -146,6 +152,16 @@ describe('EntitySelect', () => {
expect(findListbox().props('toggleText')).toBe(defaultToggleText);
});
+
+ it('emits `input` event with `null` on reset', async () => {
+ createComponent();
+ await selectGroup();
+
+ findListbox().vm.$emit('reset');
+ await nextTick();
+
+ expect(wrapper.emitted('input')[2]).toEqual([null]);
+ });
});
});
@@ -201,7 +217,7 @@ describe('EntitySelect', () => {
describe('pagination', () => {
const searchString = 'searchString';
- beforeEach(async () => {
+ beforeEach(() => {
let requestCount = 0;
fetchItemsMock.mockImplementation((searchQuery, page) => {
requestCount += 1;
diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
index 57dce032d30..0a174c98efb 100644
--- a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
@@ -63,11 +63,8 @@ describe('ProjectSelect', () => {
};
const openListbox = () => findListbox().vm.$emit('shown');
- beforeAll(() => {
- gon.api_version = apiVersion;
- });
-
beforeEach(() => {
+ gon.api_version = apiVersion;
mock = new MockAdapter(axios);
});
@@ -100,6 +97,7 @@ describe('ProjectSelect', () => {
${'defaultToggleText'} | ${PROJECT_TOGGLE_TEXT}
${'headerText'} | ${PROJECT_HEADER_TEXT}
${'clearable'} | ${true}
+ ${'block'} | ${false}
`('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
expect(findEntitySelect().props(prop)).toBe(expectedValue);
});
@@ -139,6 +137,18 @@ describe('ProjectSelect', () => {
expect(mock.history.get[0].params.include_subgroups).toBe(true);
});
+ it('does not include shared projects if withShared prop is false', async () => {
+ createComponent({
+ props: {
+ withShared: false,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.with_shared).toBe(false);
+ });
+
it('fetches projects globally if no group ID is provided', async () => {
createComponent({
props: {
diff --git a/spec/frontend/vue_shared/components/expand_button_spec.js b/spec/frontend/vue_shared/components/expand_button_spec.js
index 170c947e520..ad2a57d90eb 100644
--- a/spec/frontend/vue_shared/components/expand_button_spec.js
+++ b/spec/frontend/vue_shared/components/expand_button_spec.js
@@ -27,10 +27,6 @@ describe('Expand button', () => {
});
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the prepended collapse button', () => {
expect(expanderPrependEl().isVisible()).toBe(true);
expect(expanderAppendEl().isVisible()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/file_finder/index_spec.js b/spec/frontend/vue_shared/components/file_finder/index_spec.js
index 5cf891a2e52..bb0b12d205a 100644
--- a/spec/frontend/vue_shared/components/file_finder/index_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/index_spec.js
@@ -1,244 +1,215 @@
-import Mousetrap from 'mousetrap';
-import Vue, { nextTick } from 'vue';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import VirtualList from 'vue-virtual-scroll-list';
+import { Mousetrap } from '~/lib/mousetrap';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { file } from 'jest/ide/helpers';
-import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import FindFileComponent from '~/vue_shared/components/file_finder/index.vue';
+import FileFinderItem from '~/vue_shared/components/file_finder/item.vue';
+import { setHTMLFixture } from 'helpers/fixtures';
describe('File finder item spec', () => {
- const Component = Vue.extend(FindFileComponent);
- let vm;
+ let wrapper;
+
+ const TEST_FILES = [
+ {
+ ...file('index.js'),
+ path: 'index.js',
+ type: 'blob',
+ url: '/index.jsurl',
+ },
+ {
+ ...file('component.js'),
+ path: 'component.js',
+ type: 'blob',
+ },
+ ];
function createComponent(props) {
- vm = new Component({
+ wrapper = mountExtended(FindFileComponent, {
+ attachTo: document.body,
propsData: {
- files: [],
+ files: TEST_FILES,
visible: true,
loading: false,
...props,
},
});
-
- vm.$mount('#app');
}
- beforeEach(() => {
- setHTMLFixture('<div id="app"></div>');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
+ const findAllFileFinderItems = () => wrapper.findAllComponents(FileFinderItem);
+ const findSearchInput = () => wrapper.findByTestId('search-input');
+ const enterSearchText = (text) => findSearchInput().setValue(text);
+ const clearSearch = () => wrapper.findByTestId('clear-search-input').vm.$emit('click');
describe('with entries', () => {
beforeEach(() => {
createComponent({
- files: [
- {
- ...file('index.js'),
- path: 'index.js',
- type: 'blob',
- url: '/index.jsurl',
- },
- {
- ...file('component.js'),
- path: 'component.js',
- type: 'blob',
- },
- ],
+ files: TEST_FILES,
});
return nextTick();
});
it('renders list of blobs', () => {
- expect(vm.$el.textContent).toContain('index.js');
- expect(vm.$el.textContent).toContain('component.js');
- expect(vm.$el.textContent).not.toContain('folder');
+ expect(wrapper.text()).toContain('index.js');
+ expect(wrapper.text()).toContain('component.js');
+ expect(wrapper.text()).not.toContain('folder');
});
it('filters entries', async () => {
- vm.searchText = 'index';
-
- await nextTick();
+ await enterSearchText('index');
- expect(vm.$el.textContent).toContain('index.js');
- expect(vm.$el.textContent).not.toContain('component.js');
+ expect(wrapper.text()).toContain('index.js');
+ expect(wrapper.text()).not.toContain('component.js');
});
it('shows clear button when searchText is not empty', async () => {
- vm.searchText = 'index';
-
- await nextTick();
+ await enterSearchText('index');
- expect(vm.$el.querySelector('.dropdown-input').classList).toContain('has-value');
- expect(vm.$el.querySelector('.dropdown-input-search').classList).toContain('hidden');
+ expect(wrapper.find('.dropdown-input').classes()).toContain('has-value');
+ expect(wrapper.find('.dropdown-input-search').classes()).toContain('hidden');
});
it('clear button resets searchText', async () => {
- vm.searchText = 'index';
+ await enterSearchText('index');
+ expect(findSearchInput().element.value).toBe('index');
- vm.clearSearchInput();
+ await clearSearch();
- expect(vm.searchText).toBe('');
+ expect(findSearchInput().element.value).toBe('');
});
it('clear button focuses search input', async () => {
- jest.spyOn(vm.$refs.searchInput, 'focus').mockImplementation(() => {});
- vm.searchText = 'index';
+ expect(findSearchInput().element).not.toBe(document.activeElement);
- vm.clearSearchInput();
+ await enterSearchText('index');
+ await clearSearch();
- await nextTick();
-
- expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
+ expect(findSearchInput().element).toBe(document.activeElement);
});
describe('listShowCount', () => {
- it('returns 1 when no filtered entries exist', () => {
- vm.searchText = 'testing 123';
+ it('returns 1 when no filtered entries exist', async () => {
+ await enterSearchText('testing 123');
- expect(vm.listShowCount).toBe(1);
+ expect(wrapper.findComponent(VirtualList).props('remain')).toBe(1);
});
it('returns entries length when not filtered', () => {
- expect(vm.listShowCount).toBe(2);
+ expect(wrapper.findComponent(VirtualList).props('remain')).toBe(2);
});
});
- describe('filteredBlobsLength', () => {
- it('returns length of filtered blobs', () => {
- vm.searchText = 'index';
+ describe('filtering', () => {
+ it('renders only items that match the filter', async () => {
+ await enterSearchText('index');
- expect(vm.filteredBlobsLength).toBe(1);
+ expect(findAllFileFinderItems()).toHaveLength(1);
});
});
describe('DOM Performance', () => {
it('renders less DOM nodes if not visible by utilizing v-if', async () => {
- vm.visible = false;
+ createComponent({ visible: false });
await nextTick();
- expect(vm.$el).toBeInstanceOf(Comment);
+ expect(wrapper.findByTestId('overlay').exists()).toBe(false);
});
});
describe('watches', () => {
describe('searchText', () => {
it('resets focusedIndex when updated', async () => {
- vm.focusedIndex = 1;
- vm.searchText = 'test';
-
+ await enterSearchText('index');
await nextTick();
- expect(vm.focusedIndex).toBe(0);
+ expect(findAllFileFinderItems().at(0).props('focused')).toBe(true);
});
});
describe('visible', () => {
it('resets searchText when changed to false', async () => {
- vm.searchText = 'test';
- vm.visible = false;
-
- await nextTick();
+ await enterSearchText('test');
+ await wrapper.setProps({ visible: false });
+ // need to set it back to true, so the component's content renders
+ await wrapper.setProps({ visible: true });
- expect(vm.searchText).toBe('');
+ expect(findSearchInput().element.value).toBe('');
});
});
});
describe('openFile', () => {
- beforeEach(() => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- });
-
it('closes file finder', () => {
- vm.openFile(vm.files[0]);
+ expect(wrapper.emitted('toggle')).toBeUndefined();
- expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
+ findSearchInput().trigger('keyup.enter');
+
+ expect(wrapper.emitted('toggle')).toHaveLength(1);
});
it('pushes to router', () => {
- vm.openFile(vm.files[0]);
+ expect(wrapper.emitted('click')).toBeUndefined();
+
+ findSearchInput().trigger('keyup.enter');
- expect(vm.$emit).toHaveBeenCalledWith('click', vm.files[0]);
+ expect(wrapper.emitted('click')).toHaveLength(1);
});
});
describe('onKeyup', () => {
it('opens file on enter key', async () => {
- const event = new CustomEvent('keyup');
- event.keyCode = ENTER_KEY_CODE;
+ expect(wrapper.emitted('click')).toBeUndefined();
- jest.spyOn(vm, 'openFile').mockImplementation(() => {});
+ await findSearchInput().trigger('keyup.enter');
- vm.$refs.searchInput.dispatchEvent(event);
-
- await nextTick();
-
- expect(vm.openFile).toHaveBeenCalledWith(vm.files[0]);
+ expect(wrapper.emitted('click')[0][0]).toBe(TEST_FILES[0]);
});
it('closes file finder on esc key', async () => {
- const event = new CustomEvent('keyup');
- event.keyCode = ESC_KEY_CODE;
-
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
-
- vm.$refs.searchInput.dispatchEvent(event);
+ expect(wrapper.emitted('toggle')).toBeUndefined();
- await nextTick();
+ await findSearchInput().trigger('keyup.esc');
- expect(vm.$emit).toHaveBeenCalledWith('toggle', false);
+ expect(wrapper.emitted('toggle')[0][0]).toBe(false);
});
});
describe('onKeyDown', () => {
- let el;
-
- beforeEach(() => {
- el = vm.$refs.searchInput;
- });
-
describe('up key', () => {
- const event = new CustomEvent('keydown');
- event.keyCode = UP_KEY_CODE;
+ it('resets to last index when at top', async () => {
+ expect(findAllFileFinderItems().at(0).props('focused')).toBe(true);
- it('resets to last index when at top', () => {
- el.dispatchEvent(event);
+ await findSearchInput().trigger('keydown.up');
- expect(vm.focusedIndex).toBe(1);
+ expect(findAllFileFinderItems().at(-1).props('focused')).toBe(true);
});
- it('minus 1 from focusedIndex', () => {
- vm.focusedIndex = 1;
-
- el.dispatchEvent(event);
+ it('minus 1 from focusedIndex', async () => {
+ await findSearchInput().trigger('keydown.up');
+ await findSearchInput().trigger('keydown.up');
- expect(vm.focusedIndex).toBe(0);
+ expect(findAllFileFinderItems().at(0).props('focused')).toBe(true);
});
});
describe('down key', () => {
- const event = new CustomEvent('keydown');
- event.keyCode = DOWN_KEY_CODE;
+ it('resets to first index when at bottom', async () => {
+ await findSearchInput().trigger('keydown.down');
+ expect(findAllFileFinderItems().at(-1).props('focused')).toBe(true);
- it('resets to first index when at bottom', () => {
- vm.focusedIndex = 1;
- el.dispatchEvent(event);
-
- expect(vm.focusedIndex).toBe(0);
+ await findSearchInput().trigger('keydown.down');
+ expect(findAllFileFinderItems().at(0).props('focused')).toBe(true);
});
- it('adds 1 to focusedIndex', () => {
- el.dispatchEvent(event);
+ it('adds 1 to focusedIndex', async () => {
+ expect(findAllFileFinderItems().at(0).props('focused')).toBe(true);
+
+ await findSearchInput().trigger('keydown.down');
- expect(vm.focusedIndex).toBe(1);
+ expect(findAllFileFinderItems().at(1).props('focused')).toBe(true);
});
});
});
@@ -246,46 +217,45 @@ describe('File finder item spec', () => {
describe('without entries', () => {
it('renders loading text when loading', () => {
- createComponent({ loading: true });
+ createComponent({ loading: true, files: [] });
- expect(vm.$el.querySelector('.gl-spinner')).not.toBe(null);
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
it('renders no files text', () => {
- createComponent();
+ createComponent({ files: [] });
- expect(vm.$el.textContent).toContain('No files found.');
+ expect(wrapper.text()).toContain('No files found.');
});
});
describe('keyboard shortcuts', () => {
beforeEach(async () => {
createComponent();
-
- jest.spyOn(vm, 'toggle').mockImplementation(() => {});
-
await nextTick();
});
- it('calls toggle on `t` key press', async () => {
+ it('calls toggle on `t` key press', () => {
+ expect(wrapper.emitted('toggle')).toBeUndefined();
+
Mousetrap.trigger('t');
- await nextTick();
- expect(vm.toggle).toHaveBeenCalled();
+ expect(wrapper.emitted('toggle')).not.toBeUndefined();
});
- it('calls toggle on `mod+p` key press', async () => {
+ it('calls toggle on `mod+p` key press', () => {
+ expect(wrapper.emitted('toggle')).toBeUndefined();
+
Mousetrap.trigger('mod+p');
- await nextTick();
- expect(vm.toggle).toHaveBeenCalled();
+ expect(wrapper.emitted('toggle')).not.toBeUndefined();
});
it('always allows `mod+p` to trigger toggle', () => {
expect(
Mousetrap.prototype.stopCallback(
null,
- vm.$el.querySelector('.dropdown-input-field'),
+ wrapper.find('.dropdown-input-field').element,
'mod+p',
),
).toBe(false);
@@ -293,7 +263,7 @@ describe('File finder item spec', () => {
it('onlys handles `t` when focused in input-field', () => {
expect(
- Mousetrap.prototype.stopCallback(null, vm.$el.querySelector('.dropdown-input-field'), 't'),
+ Mousetrap.prototype.stopCallback(null, wrapper.find('.dropdown-input-field').element, 't'),
).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index f0998b1b5c6..c73f14e9c6e 100644
--- a/spec/frontend/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -22,10 +22,6 @@ describe('File finder item spec', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders file name & path', () => {
createComponent();
@@ -40,7 +36,7 @@ describe('File finder item spec', () => {
expect(wrapper.classes()).toContain('is-focused');
});
- it('does not have is-focused class when not focused', async () => {
+ it('does not have is-focused class when not focused', () => {
createComponent({ focused: false });
expect(wrapper.classes()).not.toContain('is-focused');
@@ -54,13 +50,13 @@ describe('File finder item spec', () => {
expect(wrapper.find('.diff-changed-stats').exists()).toBe(false);
});
- it('renders when a changed file', async () => {
+ it('renders when a changed file', () => {
createComponent({ file: { changed: true } });
expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
});
- it('renders when a temp file', async () => {
+ it('renders when a temp file', () => {
createComponent({ file: { tempFile: true } });
expect(wrapper.find('.diff-changed-stats').exists()).toBe(true);
@@ -84,7 +80,7 @@ describe('File finder item spec', () => {
expect(findChangedFilePath().findAll('.highlighted')).toHaveLength(4);
});
- it('adds ellipsis to long text', async () => {
+ it('adds ellipsis to long text', () => {
const path = new Array(70)
.fill()
.map((_, i) => `${i}-`)
@@ -105,7 +101,7 @@ describe('File finder item spec', () => {
expect(findChangedFileName().findAll('.highlighted')).toHaveLength(4);
});
- it('does not add ellipsis to long text', async () => {
+ it('does not add ellipsis to long text', () => {
const name = new Array(70)
.fill()
.map((_, i) => `${i}-`)
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index 0fcc0678c13..d95773f2218 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -16,10 +16,6 @@ describe('File Icon component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a span element and an icon', () => {
createComponent({
fileName: 'test.js',
diff --git a/spec/frontend/vue_shared/components/file_row_header_spec.js b/spec/frontend/vue_shared/components/file_row_header_spec.js
index 80f4c275dcc..885a80f73b5 100644
--- a/spec/frontend/vue_shared/components/file_row_header_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_header_spec.js
@@ -1,36 +1,24 @@
import { shallowMount } from '@vue/test-utils';
+import { GlTruncate } from '@gitlab/ui';
import FileRowHeader from '~/vue_shared/components/file_row_header.vue';
describe('File row header component', () => {
- let vm;
+ let wrapper;
function createComponent(path) {
- vm = shallowMount(FileRowHeader, {
+ wrapper = shallowMount(FileRowHeader, {
propsData: {
path,
},
});
}
- afterEach(() => {
- vm.destroy();
- });
-
it('renders file path', () => {
- createComponent('app/assets');
-
- expect(vm.element).toMatchSnapshot();
- });
-
- it('trucates path after 40 characters', () => {
- createComponent('app/assets/javascripts/merge_requests');
-
- expect(vm.element).toMatchSnapshot();
- });
-
- it('adds multiple ellipsises after 40 characters', () => {
- createComponent('app/assets/javascripts/merge_requests/widget/diffs/notes');
+ const path = 'app/assets';
+ createComponent(path);
- expect(vm.element).toMatchSnapshot();
+ const truncate = wrapper.findComponent(GlTruncate);
+ expect(truncate.exists()).toBe(true);
+ expect(truncate.props('text')).toBe(path);
});
});
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index b70d4565f56..976866af27c 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -6,6 +6,9 @@ import FileIcon from '~/vue_shared/components/file_icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue';
+const scrollIntoViewMock = jest.fn();
+HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+
describe('File row component', () => {
let wrapper;
@@ -18,10 +21,6 @@ describe('File row component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders name', () => {
const fileName = 't4';
createComponent({
@@ -72,11 +71,10 @@ describe('File row component', () => {
},
level: 0,
});
- jest.spyOn(wrapper.vm, '$emit');
wrapper.element.click();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', fileName);
+ expect(wrapper.emitted('toggleTreeOpen')[0][0]).toEqual(fileName);
});
it('calls scrollIntoView if made active', () => {
@@ -89,14 +87,12 @@ describe('File row component', () => {
level: 0,
});
- jest.spyOn(wrapper.vm, 'scrollIntoView');
-
wrapper.setProps({
file: { ...wrapper.props('file'), active: true },
});
return nextTick().then(() => {
- expect(wrapper.vm.scrollIntoView).toHaveBeenCalled();
+ expect(scrollIntoViewMock).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/vue_shared/components/file_tree_spec.js b/spec/frontend/vue_shared/components/file_tree_spec.js
index e8818e09dc0..9d2fa369910 100644
--- a/spec/frontend/vue_shared/components/file_tree_spec.js
+++ b/spec/frontend/vue_shared/components/file_tree_spec.js
@@ -33,10 +33,6 @@ describe('File Tree component', () => {
...pick(x.attributes(), Object.keys(TEST_EXTA_ARGS)),
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('file row component', () => {
beforeEach(() => {
createComponent({ file: {} });
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index b0e393bbf5e..f576121fc18 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -82,10 +82,6 @@ describe('FilteredSearchBarRoot', () => {
wrapper = createComponent({ sortOptions: mockSortOptions });
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('data', () => {
it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => {
expect(wrapper.vm.filterValue).toEqual([]);
@@ -402,7 +398,7 @@ describe('FilteredSearchBarRoot', () => {
expect(glFilteredSearchEl.props('historyItems')).toEqual(mockHistoryItems);
});
- it('renders checkbox when `showCheckbox` prop is true', async () => {
+ it('renders checkbox when `showCheckbox` prop is true', () => {
let wrapperWithCheckbox = createComponent({
showCheckbox: true,
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
index 63c22aff3d5..dd0ec65c871 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
@@ -15,7 +15,7 @@ const labelsEndpoint = 'fake_labels_endpoint';
const groupEndpoint = 'fake_group_endpoint';
const projectEndpoint = 'fake_project_endpoint';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Filters actions', () => {
let state;
@@ -165,16 +165,10 @@ describe('Filters actions', () => {
});
describe('fetchAuthors', () => {
- let restoreVersion;
beforeEach(() => {
- restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
- afterEach(() => {
- gon.api_version = restoreVersion;
- });
-
describe('success', () => {
beforeEach(() => {
mock.onAny().replyOnce(HTTP_STATUS_OK, filterUsers);
@@ -305,17 +299,11 @@ describe('Filters actions', () => {
describe('fetchAssignees', () => {
describe('success', () => {
- let restoreVersion;
beforeEach(() => {
mock.onAny().replyOnce(HTTP_STATUS_OK, filterUsers);
- restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
- afterEach(() => {
- gon.api_version = restoreVersion;
- });
-
it('dispatches RECEIVE_ASSIGNEES_SUCCESS with received data and groupEndpoint set', () => {
return testAction(
actions.fetchAssignees,
@@ -350,17 +338,11 @@ describe('Filters actions', () => {
});
describe('error', () => {
- let restoreVersion;
beforeEach(() => {
mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
- restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
- afterEach(() => {
- gon.api_version = restoreVersion;
- });
-
it('dispatches RECEIVE_ASSIGNEES_ERROR and groupEndpoint set', () => {
return testAction(
actions.fetchAssignees,
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index 164235e4bb9..d87aa3194d2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -18,6 +18,7 @@ import {
OPTIONS_NONE_ANY,
OPERATOR_IS,
OPERATOR_NOT,
+ OPERATOR_OR,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
getRecentlyUsedSuggestions,
@@ -98,6 +99,7 @@ function createComponent({
portalName: 'fake target',
alignSuggestions: jest.fn(),
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
filteredSearchSuggestionListInstance: {
register: jest.fn(),
unregister: jest.fn(),
@@ -120,10 +122,6 @@ describe('BaseToken', () => {
const getMockSuggestionListSuggestions = () =>
JSON.parse(findMockSuggestionList().attributes('data-suggestions'));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('data', () => {
it('calls `getRecentlyUsedSuggestions` to populate `recentSuggestions` when `recentSuggestionsStorageKey` is defined', () => {
wrapper = createComponent();
@@ -304,6 +302,7 @@ describe('BaseToken', () => {
operator | shouldRenderFilteredSearchSuggestion
${OPERATOR_IS} | ${true}
${OPERATOR_NOT} | ${false}
+ ${OPERATOR_OR} | ${false}
`('when operator is $operator', ({ shouldRenderFilteredSearchSuggestion, operator }) => {
beforeEach(() => {
const props = {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 311d5a13280..6bbbfd838a0 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -9,14 +9,15 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { mockBranches, mockBranchToken } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
@@ -45,6 +46,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
});
@@ -54,58 +56,83 @@ describe('BranchToken', () => {
let mock;
let wrapper;
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const triggerFetchBranches = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
describe('fetchBranches', () => {
- it('calls `config.fetchBranches` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchBranches');
-
- wrapper.vm.fetchBranches('foo');
+ it('sets loading state', async () => {
+ wrapper = createComponent({
+ config: {
+ fetchBranches: jest.fn().mockResolvedValue(new Promise(() => {})),
+ },
+ });
+ await nextTick();
- expect(wrapper.vm.config.fetchBranches).toHaveBeenCalledWith('foo');
+ expect(findBaseToken().props('suggestionsLoading')).toBe(true);
});
- it('sets response to `branches` when request is succesful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchBranches').mockResolvedValue({ data: mockBranches });
+ describe('when request is successful', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchBranches: jest.fn().mockResolvedValue({ data: mockBranches }),
+ },
+ });
+ });
+
+ it('calls `config.fetchBranches` with provided searchTerm param', async () => {
+ const searchTerm = 'foo';
+ await triggerFetchBranches(searchTerm);
- wrapper.vm.fetchBranches('foo');
+ expect(findBaseToken().props('config').fetchBranches).toHaveBeenCalledWith(searchTerm);
+ });
+
+ it('sets response to `branches`', async () => {
+ await triggerFetchBranches();
- return waitForPromises().then(() => {
- expect(wrapper.vm.branches).toEqual(mockBranches);
+ expect(findBaseToken().props('suggestions')).toEqual(mockBranches);
+ });
+
+ it('sets `loading` to false when request completes', async () => {
+ await triggerFetchBranches();
+
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
+ describe('when request fails', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchBranches: jest.fn().mockRejectedValue({}),
+ },
+ });
+ });
- wrapper.vm.fetchBranches('foo');
+ it('calls `createAlert` with alert error message when request fails', async () => {
+ await triggerFetchBranches();
- return waitForPromises().then(() => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching branches.',
});
});
- });
-
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
- wrapper.vm.fetchBranches('foo');
+ it('sets `loading` to false when request completes', async () => {
+ await triggerFetchBranches();
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
@@ -120,16 +147,13 @@ describe('BranchToken', () => {
await nextTick();
}
- beforeEach(async () => {
- wrapper = createComponent({ value: { data: mockBranches[0].name } });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- branches: mockBranches,
+ beforeEach(() => {
+ wrapper = createComponent({
+ value: { data: mockBranches[0].name },
+ config: {
+ initialBranches: mockBranches,
+ },
});
-
- await nextTick();
});
it('renders gl-filtered-search-token component', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index 7be7035a0f2..fb8cea09a9b 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -8,7 +8,7 @@ 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 '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -22,7 +22,7 @@ import {
mockProjectCrmContactsQueryResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
@@ -71,6 +71,7 @@ describe('CrmContactToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
listeners,
@@ -79,7 +80,6 @@ describe('CrmContactToken', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -159,7 +159,7 @@ describe('CrmContactToken', () => {
});
});
- it('calls `createAlert` with flash error message when request fails', async () => {
+ it('calls `createAlert` with alert error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index ecd3e8a04f1..20369342220 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -8,7 +8,7 @@ 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 '~/flash';
+import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -22,7 +22,7 @@ import {
mockProjectCrmOrganizationsQueryResponse,
} from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
@@ -70,6 +70,7 @@ describe('CrmOrganizationToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
listeners,
@@ -78,7 +79,6 @@ describe('CrmOrganizationToken', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -158,7 +158,7 @@ describe('CrmOrganizationToken', () => {
});
});
- it('calls `createAlert` with flash error message when request fails', async () => {
+ it('calls `createAlert` when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index 773df01ada7..5e675c10038 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
@@ -17,10 +17,11 @@ import {
OPTIONS_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { mockReactionEmojiToken, mockEmojis } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const GlEmoji = { template: '<img/>' };
const defaultStubs = {
Portal: true,
@@ -51,6 +52,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
});
@@ -60,58 +62,72 @@ describe('EmojiToken', () => {
let mock;
let wrapper;
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const triggerFetchEmojis = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
describe('fetchEmojis', () => {
- it('calls `config.fetchEmojis` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEmojis');
-
- wrapper.vm.fetchEmojis('foo');
+ it('sets loading state', async () => {
+ wrapper = createComponent({
+ config: {
+ fetchEmojis: jest.fn().mockResolvedValue(new Promise(() => {})),
+ },
+ });
+ await nextTick();
- expect(wrapper.vm.config.fetchEmojis).toHaveBeenCalledWith('foo');
+ expect(findBaseToken().props('suggestionsLoading')).toBe(true);
});
- it('sets response to `emojis` when request is successful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockResolvedValue(mockEmojis);
+ describe('when request is successful', () => {
+ const searchTerm = 'foo';
- wrapper.vm.fetchEmojis('foo');
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchEmojis: jest.fn().mockResolvedValue({ data: mockEmojis }),
+ },
+ });
+ return triggerFetchEmojis(searchTerm);
+ });
- return waitForPromises().then(() => {
- expect(wrapper.vm.emojis).toEqual(mockEmojis);
+ it('calls `config.fetchEmojis` with provided searchTerm param', () => {
+ expect(findBaseToken().props('config').fetchEmojis).toHaveBeenCalledWith(searchTerm);
});
- });
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({});
+ it('sets response to `emojis`', () => {
+ expect(findBaseToken().props('suggestions')).toEqual(mockEmojis);
+ });
+ });
- wrapper.vm.fetchEmojis('foo');
+ describe('when request fails', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchEmojis: jest.fn().mockRejectedValue({}),
+ },
+ });
+ return triggerFetchEmojis();
+ });
- return waitForPromises().then(() => {
+ it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching emojis.',
});
});
- });
-
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({});
-
- wrapper.vm.fetchEmojis('foo');
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
@@ -120,18 +136,13 @@ describe('EmojiToken', () => {
describe('template', () => {
const defaultEmojis = OPTIONS_NONE_ANY;
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = createComponent({
value: { data: `"${mockEmojis[0].name}"` },
+ config: {
+ initialEmojis: mockEmojis,
+ },
});
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- emojis: mockEmojis,
- });
-
- await nextTick();
});
it('renders gl-filtered-search-token component', () => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 9d96123c17f..c55721fe032 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -11,7 +11,7 @@ import {
mockRegularLabel,
mockLabels,
} from 'jest/sidebar/components/labels/labels_select_vue/mock_data';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -20,7 +20,7 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
import { mockLabelToken } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
BaseToken,
@@ -51,6 +51,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
listeners,
@@ -60,103 +61,125 @@ function createComponent(options = {}) {
describe('LabelToken', () => {
let mock;
let wrapper;
+ const defaultLabels = OPTIONS_NONE_ANY;
beforeEach(() => {
mock = new MockAdapter(axios);
});
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const findSuggestions = () => wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const findTokenSegments = () => wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const triggerFetchLabels = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
- beforeEach(() => {
- wrapper = createComponent();
- });
-
describe('getActiveLabel', () => {
it('returns label object from labels array based on provided `currentValue` param', () => {
- expect(wrapper.vm.getActiveLabel(mockLabels, 'Foo Label')).toEqual(mockRegularLabel);
+ wrapper = createComponent();
+
+ expect(findBaseToken().props('getActiveTokenValue')(mockLabels, 'Foo Label')).toEqual(
+ mockRegularLabel,
+ );
});
});
describe('getLabelName', () => {
- it('returns value of `name` or `title` property present in provided label param', () => {
- let mockLabel = {
- title: 'foo',
- };
+ it('returns value of `name` or `title` property present in provided label param', async () => {
+ const customMockLabels = [
+ { title: 'Title with no name label' },
+ { name: 'Name Label', title: 'Title with name label' },
+ ];
+
+ wrapper = createComponent({
+ active: true,
+ config: {
+ ...mockLabelToken,
+ fetchLabels: jest.fn().mockResolvedValue({ data: customMockLabels }),
+ },
+ stubs: { Portal: true },
+ });
- expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.title);
+ await waitForPromises();
- mockLabel = {
- name: 'foo',
- };
+ const suggestions = findSuggestions();
+ const indexWithTitle = defaultLabels.length;
+ const indexWithName = defaultLabels.length + 1;
- expect(wrapper.vm.getLabelName(mockLabel)).toBe(mockLabel.name);
+ expect(suggestions.at(indexWithTitle).text()).toBe(customMockLabels[0].title);
+ expect(suggestions.at(indexWithName).text()).toBe(customMockLabels[1].name);
});
});
describe('fetchLabels', () => {
- it('calls `config.fetchLabels` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchLabels');
-
- wrapper.vm.fetchLabels('foo');
-
- expect(wrapper.vm.config.fetchLabels).toHaveBeenCalledWith('foo');
- });
+ describe('when request is successful', () => {
+ const searchTerm = 'foo';
+
+ beforeEach(async () => {
+ wrapper = createComponent({
+ config: {
+ fetchLabels: jest.fn().mockResolvedValue({ data: mockLabels }),
+ },
+ });
+ await triggerFetchLabels(searchTerm);
+ });
- it('sets response to `labels` when request is succesful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchLabels').mockResolvedValue(mockLabels);
+ it('calls `config.fetchLabels` with provided searchTerm param', () => {
+ expect(findBaseToken().props('config').fetchLabels).toHaveBeenCalledWith(searchTerm);
+ });
- wrapper.vm.fetchLabels('foo');
+ it('sets response to `labels`', () => {
+ expect(findBaseToken().props('suggestions')).toEqual(mockLabels);
+ });
- return waitForPromises().then(() => {
- expect(wrapper.vm.labels).toEqual(mockLabels);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
-
- wrapper.vm.fetchLabels('foo');
+ describe('when request fails', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({
+ config: {
+ fetchLabels: jest.fn().mockRejectedValue({}),
+ },
+ });
+ await triggerFetchLabels();
+ });
- return waitForPromises().then(() => {
+ it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching labels.',
});
});
- });
-
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
-
- wrapper.vm.fetchLabels('foo');
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
});
describe('template', () => {
- const defaultLabels = OPTIONS_NONE_ANY;
-
beforeEach(async () => {
- wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- labels: mockLabels,
+ wrapper = createComponent({
+ value: { data: `"${mockRegularLabel.title}"` },
+ config: {
+ initialLabels: mockLabels,
+ },
});
await nextTick();
});
it('renders base-token component', () => {
- const baseTokenEl = wrapper.findComponent(BaseToken);
+ const baseTokenEl = findBaseToken();
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
@@ -166,7 +189,7 @@ describe('LabelToken', () => {
});
it('renders token item when value is selected', () => {
- const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const tokenSegments = findTokenSegments();
expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label"
expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label"
@@ -181,12 +204,12 @@ describe('LabelToken', () => {
config: { ...mockLabelToken, defaultLabels },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const tokenSegments = findTokenSegments();
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
- const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const suggestions = findSuggestions();
expect(suggestions).toHaveLength(defaultLabels.length);
defaultLabels.forEach((label, index) => {
@@ -200,7 +223,7 @@ describe('LabelToken', () => {
config: { ...mockLabelToken, defaultLabels: [] },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const tokenSegments = findTokenSegments();
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await nextTick();
@@ -215,11 +238,10 @@ describe('LabelToken', () => {
config: { ...mockLabelToken },
stubs: { Portal: true },
});
- const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+ const tokenSegments = findTokenSegments();
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
-
- const suggestions = wrapper.findAllComponents(GlFilteredSearchSuggestion);
+ const suggestions = findSuggestions();
expect(suggestions).toHaveLength(OPTIONS_NONE_ANY.length);
OPTIONS_NONE_ANY.forEach((label, index) => {
@@ -234,7 +256,7 @@ describe('LabelToken', () => {
input: mockInput,
},
});
- wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
+ findBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});
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 589697fe542..db51b4a05b1 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
@@ -8,16 +8,17 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { sortMilestonesByDueDate } from '~/milestones/utils';
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';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/milestones/utils');
const defaultStubs = {
@@ -48,6 +49,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
stubs,
});
@@ -57,6 +59,12 @@ describe('MilestoneToken', () => {
let mock;
let wrapper;
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
+ const triggerFetchMilestones = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createComponent();
@@ -64,73 +72,77 @@ describe('MilestoneToken', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
describe('fetchMilestones', () => {
- describe('when config.shouldSkipSort is true', () => {
- beforeEach(() => {
- wrapper.vm.config.shouldSkipSort = true;
+ it('sets loading state', async () => {
+ wrapper = createComponent({
+ config: {
+ fetchMilestones: jest.fn().mockResolvedValue(new Promise(() => {})),
+ },
});
+ await nextTick();
- afterEach(() => {
- wrapper.vm.config.shouldSkipSort = false;
- });
+ expect(findBaseToken().props('suggestionsLoading')).toBe(true);
+ });
+
+ describe('when config.shouldSkipSort is true', () => {
it('does not call sortMilestonesByDueDate', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({
- data: mockMilestones,
+ wrapper = createComponent({
+ config: {
+ shouldSkipSort: true,
+ fetchMilestones: jest.fn().mockResolvedValue({ data: mockMilestones }),
+ },
});
- wrapper.vm.fetchMilestones();
-
- await waitForPromises();
+ await triggerFetchMilestones();
expect(sortMilestonesByDueDate).toHaveBeenCalledTimes(0);
});
});
- it('calls `config.fetchMilestones` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchMilestones');
-
- wrapper.vm.fetchMilestones('foo');
+ describe('when request is successful', () => {
+ const searchTerm = 'foo';
- expect(wrapper.vm.config.fetchMilestones).toHaveBeenCalledWith('foo');
- });
-
- it('sets response to `milestones` when request is successful', () => {
- wrapper.vm.config.shouldSkipSort = false;
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ shouldSkipSort: false,
+ fetchMilestones: jest.fn().mockResolvedValue({ data: mockMilestones }),
+ },
+ });
+ return triggerFetchMilestones(searchTerm);
+ });
- jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({
- data: mockMilestones,
+ it('calls `config.fetchMilestones` with provided searchTerm param', () => {
+ expect(findBaseToken().props('config').fetchMilestones).toHaveBeenCalledWith(searchTerm);
});
- wrapper.vm.fetchMilestones();
- return waitForPromises().then(() => {
- expect(wrapper.vm.milestones).toEqual(mockMilestones);
+ it('sets response to `milestones`', () => {
expect(sortMilestonesByDueDate).toHaveBeenCalled();
+ expect(findBaseToken().props('suggestions')).toEqual(mockMilestones);
});
});
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
-
- wrapper.vm.fetchMilestones('foo');
+ describe('when request fails', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchMilestones: jest.fn().mockRejectedValue({}),
+ },
+ });
+ return triggerFetchMilestones();
+ });
- return waitForPromises().then(() => {
+ it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching milestones.',
});
});
- });
- it('sets `loading` to false when request completes', () => {
- jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
-
- wrapper.vm.fetchMilestones('foo');
-
- return waitForPromises().then(() => {
- expect(wrapper.vm.loading).toBe(false);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
});
@@ -142,16 +154,13 @@ describe('MilestoneToken', () => {
{ text: 'bar', value: 'baz' },
];
- beforeEach(async () => {
- wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- milestones: mockMilestones,
+ beforeEach(() => {
+ wrapper = createComponent({
+ value: { data: `"${mockRegularMilestone.title}"` },
+ config: {
+ initialMilestones: mockMilestones,
+ },
});
-
- await nextTick();
});
it('renders gl-filtered-search-token component', () => {
@@ -228,7 +237,7 @@ describe('MilestoneToken', () => {
it('finds the correct value from the activeToken', () => {
DEFAULT_MILESTONES.forEach(({ value, title }) => {
- const activeToken = wrapper.vm.getActiveMilestone([], value);
+ const activeToken = findBaseToken().props('getActiveTokenValue')([], value);
expect(activeToken.title).toEqual(title);
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index 0e5fa0f66d4..79fd527cbe3 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -2,11 +2,11 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import { mockReleaseToken } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('ReleaseToken', () => {
const id = '123';
@@ -24,13 +24,10 @@ describe('ReleaseToken', () => {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders release value', async () => {
wrapper = createComponent({ value: { data: id } });
await nextTick();
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
index 32cb74d5f80..e4ca7dcb19a 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -17,7 +17,7 @@ import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_t
import { mockAuthorToken, mockUsers } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
@@ -57,6 +57,7 @@ function createComponent(options = {}) {
portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {},
suggestionsListClass: () => 'custom-class',
+ termsAsTokens: () => false,
},
data() {
return { ...data };
@@ -67,99 +68,82 @@ function createComponent(options = {}) {
}
describe('UserToken', () => {
- const originalGon = window.gon;
const currentUserLength = 1;
let mock;
let wrapper;
- const getBaseToken = () => wrapper.findComponent(BaseToken);
+ const findBaseToken = () => wrapper.findComponent(BaseToken);
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
- window.gon = originalGon;
mock.restore();
- wrapper.destroy();
});
describe('methods', () => {
describe('fetchUsers', () => {
+ const triggerFetchUsers = (searchTerm = null) => {
+ findBaseToken().vm.$emit('fetch-suggestions', searchTerm);
+ return waitForPromises();
+ };
+
beforeEach(() => {
wrapper = createComponent();
});
- it('calls `config.fetchUsers` with provided searchTerm param', () => {
- jest.spyOn(wrapper.vm.config, 'fetchUsers');
-
- getBaseToken().vm.$emit('fetch-suggestions', mockUsers[0].username);
-
- expect(wrapper.vm.config.fetchUsers).toHaveBeenCalledWith(
- mockAuthorToken.fetchPath,
- mockUsers[0].username,
- );
- });
-
- it('sets response to `users` when request is successful', () => {
- jest.spyOn(wrapper.vm.config, 'fetchUsers').mockResolvedValue(mockUsers);
-
- getBaseToken().vm.$emit('fetch-suggestions', 'root');
-
- return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
+ it('sets loading state', async () => {
+ wrapper = createComponent({
+ config: {
+ fetchUsers: jest.fn().mockResolvedValue(new Promise(() => {})),
+ },
});
+ await nextTick();
+
+ expect(findBaseToken().props('suggestionsLoading')).toBe(true);
});
- // TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
- describe('when there are null users presents', () => {
- const mockUsersWithNullUser = mockUsers.concat([null]);
+ describe('when request is successful', () => {
+ const searchTerm = 'foo';
beforeEach(() => {
- jest
- .spyOn(wrapper.vm.config, 'fetchUsers')
- .mockResolvedValue({ data: mockUsersWithNullUser });
-
- getBaseToken().vm.$emit('fetch-suggestions', 'root');
+ wrapper = createComponent({
+ config: {
+ fetchUsers: jest.fn().mockResolvedValue({ data: mockUsers }),
+ },
+ });
+ return triggerFetchUsers(searchTerm);
});
- describe('when res.data is present', () => {
- it('filters the successful response when null values are present', () => {
- return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
- });
- });
+ it('calls `config.fetchUsers` with provided searchTerm param', () => {
+ expect(findBaseToken().props('config').fetchUsers).toHaveBeenCalledWith(searchTerm);
});
- describe('when response is an array', () => {
- it('filters the successful response when null values are present', () => {
- return waitForPromises().then(() => {
- expect(getBaseToken().props('suggestions')).toEqual(mockUsers);
- });
- });
+ it('sets response to `users` when request is successful', () => {
+ expect(findBaseToken().props('suggestions')).toEqual(mockUsers);
});
});
- it('calls `createAlert` with flash error message when request fails', () => {
- jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({});
-
- getBaseToken().vm.$emit('fetch-suggestions', 'root');
+ describe('when request fails', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ config: {
+ fetchUsers: jest.fn().mockRejectedValue({}),
+ },
+ });
+ return triggerFetchUsers();
+ });
- return waitForPromises().then(() => {
+ it('calls `createAlert`', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching users.',
});
});
- });
-
- it('sets `loading` to false when request completes', async () => {
- jest.spyOn(wrapper.vm.config, 'fetchUsers').mockRejectedValue({});
-
- getBaseToken().vm.$emit('fetch-suggestions', 'root');
- await waitForPromises();
-
- expect(getBaseToken().props('suggestionsLoading')).toBe(false);
+ it('sets `loading` to false when request completes', () => {
+ expect(findBaseToken().props('suggestionsLoading')).toBe(false);
+ });
});
});
});
@@ -178,12 +162,12 @@ describe('UserToken', () => {
data: { users: mockUsers },
});
- const baseTokenEl = getBaseToken();
+ const baseTokenEl = findBaseToken();
expect(baseTokenEl.exists()).toBe(true);
expect(baseTokenEl.props()).toMatchObject({
suggestions: mockUsers,
- getActiveTokenValue: wrapper.vm.getActiveUser,
+ getActiveTokenValue: baseTokenEl.props('getActiveTokenValue'),
});
});
@@ -191,7 +175,6 @@ describe('UserToken', () => {
wrapper = createComponent({
value: { data: mockUsers[0].username },
data: { users: mockUsers },
- stubs: { Portal: true },
});
await nextTick();
@@ -205,7 +188,7 @@ describe('UserToken', () => {
expect(tokenValue.text()).toBe(mockUsers[0].name); // "Administrator"
});
- it('renders token value with correct avatarUrl from user object', async () => {
+ it('renders token value with correct avatarUrl from user object', () => {
const getAvatarEl = () =>
wrapper.findAllComponents(GlFilteredSearchTokenSegment).at(2).findComponent(GlAvatar);
@@ -215,30 +198,13 @@ describe('UserToken', () => {
users: [
{
...mockUsers[0],
+ avatarUrl: mockUsers[0].avatar_url,
+ avatar_url: undefined,
},
],
},
- stubs: { Portal: true },
});
- await nextTick();
-
- expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url);
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- users: [
- {
- ...mockUsers[0],
- avatarUrl: mockUsers[0].avatar_url,
- avatar_url: undefined,
- },
- ],
- });
-
- await nextTick();
-
expect(getAvatarEl().props('src')).toBe(mockUsers[0].avatar_url);
});
@@ -264,7 +230,6 @@ describe('UserToken', () => {
wrapper = createComponent({
active: true,
config: { ...mockAuthorToken, defaultUsers: [] },
- stubs: { Portal: true },
});
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
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 2189d6ac3cc..6f98a74a82f 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,18 +2,8 @@
exports[`Form Footer Actions renders content properly 1`] = `
<footer
- class="form-actions d-flex justify-content-between"
+ class="gl-mt-5 footer-block"
>
- <div>
- Bar
- </div>
-
- <div>
- Foo
- </div>
-
- <div>
- Abrakadabra
- </div>
+ Bar Foo Abrakadabra
</footer>
`;
diff --git a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
index 361b162b6a0..eee8a0c4532 100644
--- a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
+++ b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
@@ -10,10 +10,6 @@ describe('Form Footer Actions', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders content properly', () => {
const defaultSlot = 'Foo';
const prepend = 'Bar';
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index e1da8b690af..4f1603f93ba 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -10,10 +10,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
describe('InputCopyToggleVisibility', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
const valueProp = 'hR8x1fuJbzwu5uFKLf9e';
const createComponent = (options = {}) => {
@@ -21,7 +17,7 @@ describe('InputCopyToggleVisibility', () => {
InputCopyToggleVisibility,
merge({}, options, {
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
}),
);
diff --git a/spec/frontend/vue_shared/components/form/title_spec.js b/spec/frontend/vue_shared/components/form/title_spec.js
index 452f3723e76..d499f847c72 100644
--- a/spec/frontend/vue_shared/components/form/title_spec.js
+++ b/spec/frontend/vue_shared/components/form/title_spec.js
@@ -12,10 +12,6 @@ describe('Title edit field', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index af53d256236..38d54eff872 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -10,12 +10,8 @@ describe('GlCountdown', () => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date(now).getTime());
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when there is time remaining', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mount(GlCountdown, {
propsData: {
endDateString: '2000-01-01T01:02:03Z',
@@ -37,7 +33,7 @@ describe('GlCountdown', () => {
});
describe('when there is no time remaining', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mount(GlCountdown, {
propsData: {
endDateString: '1900-01-01T00:00:00Z',
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index 458f2cc5374..da9bc0f8a2f 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -48,11 +48,6 @@ describe('Header CI Component', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('render', () => {
beforeEach(() => {
createComponent({ itemName: 'Pipeline' });
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
index 77c03dc0c3c..76e66d07fa0 100644
--- a/spec/frontend/vue_shared/components/help_popover_spec.js
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -23,10 +23,6 @@ describe('HelpPopover', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('with title and content', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/integration_help_text_spec.js b/spec/frontend/vue_shared/components/integration_help_text_spec.js
index c63e46313b3..dd20b09f176 100644
--- a/spec/frontend/vue_shared/components/integration_help_text_spec.js
+++ b/spec/frontend/vue_shared/components/integration_help_text_spec.js
@@ -22,11 +22,6 @@ describe('IntegrationHelpText component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should use the gl components', () => {
wrapper = createComponent();
diff --git a/spec/frontend/vue_shared/components/keep_alive_slots_spec.js b/spec/frontend/vue_shared/components/keep_alive_slots_spec.js
index 10c6cbe6d94..f69a883ee4d 100644
--- a/spec/frontend/vue_shared/components/keep_alive_slots_spec.js
+++ b/spec/frontend/vue_shared/components/keep_alive_slots_spec.js
@@ -37,10 +37,6 @@ describe('~/vue_shared/components/keep_alive_slots.vue', () => {
isVisible: x.isVisible(),
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
index 7ed6a59c844..397fd270344 100644
--- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlFormGroup, GlListbox } from '@gitlab/ui';
+import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
describe('ListboxInput', () => {
@@ -27,7 +27,7 @@ describe('ListboxInput', () => {
// Finders
const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
- const findGlListbox = () => wrapper.findComponent(GlListbox);
+ const findGlListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findInput = () => wrapper.find('input');
const createComponent = (propsData) => {
@@ -153,7 +153,7 @@ describe('ListboxInput', () => {
expect(findGlListbox().props('searchable')).toBe(true);
});
- it('passes all items to GlListbox by default', () => {
+ it('passes all items to GlCollapsibleListbox by default', () => {
createComponent();
expect(findGlListbox().props('items')).toStrictEqual(items);
@@ -165,7 +165,7 @@ describe('ListboxInput', () => {
findGlListbox().vm.$emit('search', '1');
});
- it('passes only the items that match the search string', async () => {
+ it('passes only the items that match the search string', () => {
expect(findGlListbox().props('items')).toStrictEqual([
{
text: 'Group 1',
@@ -183,7 +183,7 @@ describe('ListboxInput', () => {
findGlListbox().vm.$emit('search', '1');
});
- it('passes only the items that match the search string', async () => {
+ it('passes only the items that match the search string', () => {
expect(findGlListbox().props('items')).toStrictEqual([{ text: 'Item 1', value: '1' }]);
});
});
diff --git a/spec/frontend/vue_shared/components/local_storage_sync_spec.js b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
index a80717a1aea..1c7f419b118 100644
--- a/spec/frontend/vue_shared/components/local_storage_sync_spec.js
+++ b/spec/frontend/vue_shared/components/local_storage_sync_spec.js
@@ -17,7 +17,6 @@ describe('Local Storage Sync', () => {
const getStorageValue = (value) => localStorage.getItem(STORAGE_KEY, value);
afterEach(() => {
- wrapper.destroy();
localStorage.clear();
});
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 ecb2b37c3a5..8aab867f32a 100644
--- a/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/apply_suggestion_spec.js
@@ -17,11 +17,6 @@ describe('Apply Suggestion component', () => {
beforeEach(() => createWrapper());
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('initial template', () => {
it('renders a dropdown with the correct props', () => {
const dropdown = findDropdown();
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
new file mode 100644
index 00000000000..aea25abb324
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
@@ -0,0 +1,76 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_replies.query.graphql.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { updateText } from '~/lib/utils/text_markdown';
+import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
+import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql';
+
+jest.mock('~/lib/utils/text_markdown');
+
+let wrapper;
+let savedRepliesResp;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ savedRepliesResp = jest.fn().mockResolvedValue(response);
+
+ const requestHandlers = [[savedRepliesQuery, savedRepliesResp]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(options = {}) {
+ const { mockApollo } = options;
+
+ return mountExtended(CommentTemplatesDropdown, {
+ attachTo: '#root',
+ propsData: {
+ newCommentTemplatePath: '/new',
+ },
+ apolloProvider: mockApollo,
+ });
+}
+
+describe('Comment templates dropdown', () => {
+ beforeEach(() => {
+ setHTMLFixture('<div class="md-area"><textarea></textarea><div id="root"></div></div>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('fetches data when dropdown gets opened', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ wrapper.find('.js-comment-template-toggle').trigger('click');
+
+ await waitForPromises();
+
+ expect(savedRepliesResp).toHaveBeenCalled();
+ });
+
+ it('adds content to textarea', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ wrapper.find('.js-comment-template-toggle').trigger('click');
+
+ await waitForPromises();
+
+ wrapper.find('.gl-new-dropdown-item').trigger('click');
+
+ expect(updateText).toHaveBeenCalledWith({
+ textArea: document.querySelector('textarea'),
+ tag: savedRepliesResponse.data.currentUser.savedReplies.nodes[0].content,
+ cursorOffset: 0,
+ wrap: false,
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js
new file mode 100644
index 00000000000..67f296b1bf0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/drawio_toolbar_button_spec.js
@@ -0,0 +1,66 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue';
+import { launchDrawioEditor } from '~/drawio/drawio_editor';
+import { create } from '~/drawio/markdown_field_editor_facade';
+
+jest.mock('~/drawio/drawio_editor');
+jest.mock('~/drawio/markdown_field_editor_facade');
+
+describe('vue_shared/components/markdown/drawio_toolbar_button', () => {
+ let wrapper;
+ let textArea;
+ const uploadsPath = '/uploads';
+ const markdownPreviewPath = '/markdown/preview';
+
+ const buildWrapper = (props = { uploadsPath, markdownPreviewPath }) => {
+ wrapper = shallowMount(DrawioToolbarButton, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ textArea = document.createElement('textarea');
+ textArea.classList.add('js-gfm-input');
+
+ document.body.appendChild(textArea);
+ });
+
+ afterEach(() => {
+ textArea.remove();
+ });
+
+ describe('default', () => {
+ it('renders button that launches draw.io editor', () => {
+ buildWrapper();
+
+ expect(wrapper.findComponent(GlButton).props()).toMatchObject({
+ icon: 'diagram',
+ category: 'tertiary',
+ });
+ });
+ });
+
+ describe('when clicking button', () => {
+ it('launches draw.io editor', async () => {
+ const editorFacadeStub = {};
+
+ create.mockReturnValueOnce(editorFacadeStub);
+
+ buildWrapper();
+
+ await wrapper.findComponent(GlButton).vm.$emit('click');
+
+ expect(create).toHaveBeenCalledWith({
+ markdownPreviewPath,
+ textArea,
+ uploadsPath,
+ });
+ expect(launchDrawioEditor).toHaveBeenCalledWith({
+ editorFacade: editorFacadeStub,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
deleted file mode 100644
index 34071775b9c..00000000000
--- a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
-
-describe('vue_shared/component/markdown/editor_mode_dropdown', () => {
- let wrapper;
-
- const createComponent = ({ value, size } = {}) => {
- wrapper = shallowMount(EditorModeDropdown, {
- propsData: {
- value,
- size,
- },
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItem = (text) =>
- wrapper
- .findAllComponents(GlDropdownItem)
- .filter((item) => item.text().startsWith(text))
- .at(0);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe.each`
- modeText | value | dropdownText | otherMode
- ${'Rich text'} | ${'richText'} | ${'View markdown'} | ${'Markdown'}
- ${'Markdown'} | ${'markdown'} | ${'View rich text'} | ${'Rich text'}
- `('$modeText', ({ modeText, value, dropdownText, otherMode }) => {
- beforeEach(() => {
- createComponent({ value });
- });
-
- it('shows correct dropdown label', () => {
- expect(findDropdown().props('text')).toEqual(dropdownText);
- });
-
- it('checks correct checked dropdown item', () => {
- expect(findDropdownItem(modeText).props().isChecked).toBe(true);
- expect(findDropdownItem(otherMode).props().isChecked).toBe(false);
- });
-
- it('emits event on click', () => {
- findDropdownItem(modeText).vm.$emit('click');
-
- expect(wrapper.emitted().input).toEqual([[value]]);
- });
- });
-
- it('passes size to dropdown', () => {
- createComponent({ size: 'small', value: 'markdown' });
-
- expect(findDropdown().props('size')).toEqual('small');
- });
-});
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
new file mode 100644
index 00000000000..693353ed604
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
@@ -0,0 +1,37 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+
+describe('vue_shared/component/markdown/editor_mode_switcher', () => {
+ let wrapper;
+
+ const createComponent = ({ value } = {}) => {
+ wrapper = shallowMount(EditorModeSwitcher, {
+ propsData: {
+ value,
+ },
+ });
+ };
+
+ const findSwitcherButton = () => wrapper.findComponent(GlButton);
+
+ describe.each`
+ modeText | value | buttonText
+ ${'Rich text'} | ${'richText'} | ${'Switch to Markdown'}
+ ${'Markdown'} | ${'markdown'} | ${'Switch to rich text'}
+ `('when $modeText', ({ modeText, value, buttonText }) => {
+ beforeEach(() => {
+ createComponent({ value });
+ });
+
+ it('shows correct button label', () => {
+ expect(findSwitcherButton().text()).toEqual(buttonText);
+ });
+
+ it('emits event on click', () => {
+ findSwitcherButton(modeText).vm.$emit('click');
+
+ expect(wrapper.emitted().input).toEqual([[]]);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 68ce07f86b9..b29f0d58d77 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -18,12 +18,6 @@ const textareaValue = 'testing\n123';
const uploadsPath = 'test/uploads';
const restrictedToolBarItems = ['quote'];
-function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) {
- expect(writeLink.element.children[0].classList.contains('active')).toBe(isWrite);
- expect(previewLink.element.children[0].classList.contains('active')).toBe(!isWrite);
- expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : '');
-}
-
describe('Markdown field component', () => {
let axiosMock;
let subject;
@@ -92,8 +86,7 @@ describe('Markdown field component', () => {
});
}
- const getPreviewLink = () => subject.findByTestId('preview-tab');
- const getWriteLink = () => subject.findByTestId('write-tab');
+ const getPreviewToggle = () => subject.findByTestId('preview-toggle');
const getMarkdownButton = () => subject.find('.js-md');
const getListBulletedButton = () => subject.findAll('.js-md[title="Add a bullet list"]');
const getVideo = () => subject.find('video');
@@ -109,8 +102,7 @@ describe('Markdown field component', () => {
<p>markdown preview</p>
<video src="${FIXTURES_PATH}/static/mock-video.mp4"></video>
`;
- let previewLink;
- let writeLink;
+ let previewToggle;
let dropzoneSpy;
beforeEach(() => {
@@ -140,8 +132,8 @@ describe('Markdown field component', () => {
.onPost(markdownPreviewPath)
.reply(HTTP_STATUS_OK, { references: { users: [], commands: 'test command' } });
- previewLink = getPreviewLink();
- previewLink.vm.$emit('click', { target: {} });
+ previewToggle = getPreviewToggle();
+ previewToggle.vm.$emit('click', true);
await axios.waitFor(markdownPreviewPath);
const referencedCommands = subject.find('[data-testid="referenced-commands"]');
@@ -155,26 +147,29 @@ describe('Markdown field component', () => {
axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { body: previewHTML });
});
- it('sets preview link as active', async () => {
- previewLink = getPreviewLink();
- previewLink.vm.$emit('click', { target: {} });
+ it('sets preview toggle as active', async () => {
+ previewToggle = getPreviewToggle();
+
+ expect(previewToggle.text()).toBe('Preview');
+
+ previewToggle.vm.$emit('click', true);
await nextTick();
- expect(previewLink.element.children[0].classList.contains('active')).toBe(true);
+ expect(previewToggle.text()).toBe('Continue editing');
});
it('shows preview loading text', async () => {
- previewLink = getPreviewLink();
- previewLink.vm.$emit('click', { target: {} });
+ previewToggle = getPreviewToggle();
+ previewToggle.vm.$emit('click', true);
await nextTick();
expect(subject.find('.md-preview-holder').element.textContent.trim()).toContain('Loading…');
});
it('renders markdown preview and GFM', async () => {
- previewLink = getPreviewLink();
+ previewToggle = getPreviewToggle();
- previewLink.vm.$emit('click', { target: {} });
+ previewToggle.vm.$emit('click', true);
await axios.waitFor(markdownPreviewPath);
expect(subject.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
@@ -182,8 +177,8 @@ describe('Markdown field component', () => {
});
it('calls video.pause() on comment input when isSubmitting is changed to true', async () => {
- previewLink = getPreviewLink();
- previewLink.vm.$emit('click', { target: {} });
+ previewToggle = getPreviewToggle();
+ previewToggle.vm.$emit('click', true);
await axios.waitFor(markdownPreviewPath);
const video = getVideo();
@@ -195,34 +190,27 @@ describe('Markdown field component', () => {
expect(callPause).toHaveBeenCalled();
});
- it('clicking already active write or preview link does nothing', async () => {
- writeLink = getWriteLink();
- previewLink = getPreviewLink();
-
- writeLink.vm.$emit('click', { target: {} });
- await nextTick();
-
- assertMarkdownTabs(true, writeLink, previewLink, subject);
- writeLink.vm.$emit('click', { target: {} });
- await nextTick();
+ it('switches between preview/write on toggle', async () => {
+ previewToggle = getPreviewToggle();
- assertMarkdownTabs(true, writeLink, previewLink, subject);
- previewLink.vm.$emit('click', { target: {} });
+ previewToggle.vm.$emit('click', true);
await nextTick();
+ expect(subject.find('.md-preview-holder').element.style.display).toBe(''); // visible
- assertMarkdownTabs(false, writeLink, previewLink, subject);
- previewLink.vm.$emit('click', { target: {} });
+ previewToggle.vm.$emit('click', false);
await nextTick();
-
- assertMarkdownTabs(false, writeLink, previewLink, subject);
+ expect(subject.find('.md-preview-holder').element.style.display).toBe('none');
});
- it('passes correct props to MarkdownToolbar', () => {
+ it('passes correct props to MarkdownHeader and MarkdownToolbar', () => {
expect(findMarkdownToolbar().props()).toEqual({
canAttachFile: true,
markdownDocsPath,
quickActionsDocsPath: '',
showCommentToolBar: true,
+ });
+
+ expect(findMarkdownHeader().props()).toMatchObject({
showContentEditorSwitcher: false,
});
});
@@ -380,13 +368,13 @@ describe('Markdown field component', () => {
it('defaults to false', () => {
createSubject();
- expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(false);
+ expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(false);
});
it('passes showContentEditorSwitcher', () => {
createSubject({ showContentEditorSwitcher: true });
- expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(true);
+ expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(true);
});
});
});
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 176ccfc5a69..1bbbe0896f2 100644
--- a/spec/frontend/vue_shared/components/markdown/field_view_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
@@ -6,20 +6,14 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
describe('Markdown Field View component', () => {
- let wrapper;
-
function createComponent() {
- wrapper = shallowMount(MarkdownFieldView);
+ shallowMount(MarkdownFieldView);
}
beforeEach(() => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('processes rendering with GFM', () => {
expect(renderGFM).toHaveBeenCalledTimes(1);
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index ed417097e1e..48fe5452e74 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -1,9 +1,11 @@
import $ from 'jquery';
import { nextTick } from 'vue';
-import { GlTabs } from '@gitlab/ui';
+import { GlToggle } from '@gitlab/ui';
import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
+import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
describe('Markdown field header component', () => {
let wrapper;
@@ -14,18 +16,18 @@ describe('Markdown field header component', () => {
previewMarkdown: false,
...props,
},
- stubs: { GlTabs },
+ stubs: { GlToggle },
});
};
- const findWriteTab = () => wrapper.findByTestId('write-tab');
- const findPreviewTab = () => wrapper.findByTestId('preview-tab');
+ const findPreviewToggle = () => wrapper.findByTestId('preview-toggle');
const findToolbar = () => wrapper.findByTestId('md-header-toolbar');
const findToolbarButtons = () => wrapper.findAllComponents(ToolbarButton);
const findToolbarButtonByProp = (prop, value) =>
findToolbarButtons()
.filter((button) => button.props(prop) === value)
.at(0);
+ const findDrawioToolbarButton = () => wrapper.findComponent(DrawioToolbarButton);
beforeEach(() => {
window.gl = {
@@ -37,10 +39,6 @@ describe('Markdown field header component', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('markdown header buttons', () => {
it('renders the buttons with the correct title', () => {
const buttons = [
@@ -89,16 +87,14 @@ describe('Markdown field header component', () => {
});
});
- it('activates `write` tab when previewMarkdown is false', () => {
- expect(findWriteTab().attributes('active')).toBe('true');
- expect(findPreviewTab().attributes('active')).toBeUndefined();
+ it('hides markdown preview when previewMarkdown is false', () => {
+ expect(findPreviewToggle().text()).toBe('Preview');
});
- it('activates `preview` tab when previewMarkdown is true', () => {
+ it('shows markdown preview when previewMarkdown is true', () => {
createWrapper({ previewMarkdown: true });
- expect(findWriteTab().attributes('active')).toBeUndefined();
- expect(findPreviewTab().attributes('active')).toBe('true');
+ expect(findPreviewToggle().text()).toBe('Continue editing');
});
it('hides toolbar in preview mode', () => {
@@ -107,17 +103,16 @@ describe('Markdown field header component', () => {
expect(findToolbar().classes().includes('gl-display-none!')).toBe(true);
});
- it('emits toggle markdown event when clicking preview tab', async () => {
- const eventData = { target: {} };
- findPreviewTab().vm.$emit('click', eventData);
+ it('emits toggle markdown event when clicking preview toggle', async () => {
+ findPreviewToggle().vm.$emit('click', true);
await nextTick();
- expect(wrapper.emitted('preview-markdown').length).toEqual(1);
+ expect(wrapper.emitted('showPreview').length).toEqual(1);
- findWriteTab().vm.$emit('click', eventData);
+ findPreviewToggle().vm.$emit('click', false);
await nextTick();
- expect(wrapper.emitted('write-markdown').length).toEqual(1);
+ expect(wrapper.emitted('showPreview').length).toEqual(2);
});
it('does not emit toggle markdown event when triggered from another form', () => {
@@ -127,15 +122,8 @@ describe('Markdown field header component', () => {
),
]);
- expect(wrapper.emitted('preview-markdown')).toBeUndefined();
- expect(wrapper.emitted('write-markdown')).toBeUndefined();
- });
-
- it('blurs preview link after click', () => {
- const target = { blur: jest.fn() };
- findPreviewTab().vm.$emit('click', { target });
-
- expect(target.blur).toHaveBeenCalled();
+ expect(wrapper.emitted('showPreview')).toBeUndefined();
+ expect(wrapper.emitted('hidePreview')).toBeUndefined();
});
it('renders markdown table template', () => {
@@ -168,12 +156,12 @@ describe('Markdown field header component', () => {
expect(wrapper.find('.js-suggestion-btn').exists()).toBe(false);
});
- it('hides preview tab when previewMarkdown property is false', () => {
+ it('hides markdown preview when previewMarkdown property is false', () => {
createWrapper({
enablePreview: false,
});
- expect(wrapper.findByTestId('preview-tab').exists()).toBe(false);
+ expect(wrapper.findByTestId('preview-toggle').exists()).toBe(false);
});
describe('restricted tool bar items', () => {
@@ -197,4 +185,38 @@ describe('Markdown field header component', () => {
expect(findToolbarButtons().length).toBe(defaultCount);
});
});
+
+ describe('when drawIOEnabled is true', () => {
+ const uploadsPath = '/uploads';
+ const markdownPreviewPath = '/preview';
+
+ beforeEach(() => {
+ createWrapper({
+ drawioEnabled: true,
+ uploadsPath,
+ markdownPreviewPath,
+ });
+ });
+
+ it('renders drawio toolbar button', () => {
+ expect(findDrawioToolbarButton().props()).toEqual({
+ uploadsPath,
+ markdownPreviewPath,
+ });
+ });
+ });
+
+ describe('with content editor switcher', () => {
+ beforeEach(() => {
+ createWrapper({
+ showContentEditorSwitcher: true,
+ });
+ });
+
+ it('re-emits event from switcher', () => {
+ wrapper.findComponent(EditorModeSwitcher).vm.$emit('input', 'richText');
+
+ expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
+ });
+ });
});
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 26b536984ff..26a74036b10 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -1,18 +1,30 @@
import axios from 'axios';
+import Autosize from 'autosize';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '~/vue_shared/constants';
+import {
+ EDITING_MODE_MARKDOWN_FIELD,
+ EDITING_MODE_CONTENT_EDITOR,
+ CLEAR_AUTOSAVE_ENTRY_EVENT,
+} from '~/vue_shared/constants';
+import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import ContentEditor from '~/content_editor/components/content_editor.vue';
import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { assertProps } from 'helpers/assert_props';
import { stubComponent } from 'helpers/stub_component';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/emoji');
+jest.mock('autosize');
describe('vue_shared/component/markdown/markdown_editor', () => {
+ useLocalStorageSpy();
+
let wrapper;
const value = 'test markdown';
const renderMarkdownPath = '/api/markdown';
@@ -27,23 +39,26 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const autocompleteDataSources = { commands: '/foobar/-/autcomplete_sources' };
let mock;
+ const defaultProps = {
+ value,
+ renderMarkdownPath,
+ markdownDocsPath,
+ quickActionsDocsPath,
+ enableAutocomplete,
+ autocompleteDataSources,
+ enablePreview,
+ formFieldProps: {
+ id: formFieldId,
+ name: formFieldName,
+ placeholder: formFieldPlaceholder,
+ 'aria-label': formFieldAriaLabel,
+ },
+ };
const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
wrapper = mountExtended(MarkdownEditor, {
attachTo,
propsData: {
- value,
- renderMarkdownPath,
- markdownDocsPath,
- quickActionsDocsPath,
- enableAutocomplete,
- autocompleteDataSources,
- enablePreview,
- formFieldProps: {
- id: formFieldId,
- name: formFieldName,
- placeholder: formFieldPlaceholder,
- 'aria-label': formFieldAriaLabel,
- },
+ ...defaultProps,
...propsData,
},
stubs: {
@@ -52,10 +67,31 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
},
});
};
+
+ const ContentEditorStub = stubComponent(ContentEditor);
+
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
const findTextarea = () => wrapper.find('textarea');
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const findContentEditor = () => wrapper.findComponent(ContentEditor);
+ const findContentEditor = () => {
+ const result = wrapper.findComponent(ContentEditor);
+
+ // In Vue.js 3 there are nuances stubbing component with custom stub on mount
+ // So we try to search for stub also
+ return result.exists() ? result : wrapper.findComponent(ContentEditorStub);
+ };
+
+ const enableContentEditor = async () => {
+ findMarkdownField().vm.$emit('enableContentEditor');
+ await nextTick();
+ await waitForPromises();
+ };
+
+ const enableMarkdownEditor = async () => {
+ findContentEditor().vm.$emit('enableMarkdownEditor');
+ await nextTick();
+ await waitForPromises();
+ };
beforeEach(() => {
window.uploads_path = 'uploads';
@@ -63,8 +99,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
afterEach(() => {
- wrapper.destroy();
mock.restore();
+
+ localStorage.clear();
});
it('displays markdown field by default', () => {
@@ -83,8 +120,178 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
+ it.each`
+ desc | supportsQuickActions
+ ${'passes render_quick_actions param to renderMarkdownPath if quick actions are enabled'} | ${true}
+ ${'does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled'} | ${false}
+ `('$desc', async ({ supportsQuickActions }) => {
+ buildWrapper({ propsData: { supportsQuickActions } });
+
+ await enableContentEditor();
+
+ expect(mock.history.post).toHaveLength(1);
+ expect(mock.history.post[0].url).toContain(`render_quick_actions=${supportsQuickActions}`);
+ });
+
+ it('enables content editor switcher when contentEditorEnabled prop is true', () => {
+ buildWrapper({ propsData: { enableContentEditor: true } });
+
+ expect(findMarkdownField().text()).toContain('Switch to rich text');
+ });
+
+ it('hides content editor switcher when contentEditorEnabled prop is false', () => {
+ buildWrapper({ propsData: { enableContentEditor: false } });
+
+ expect(findMarkdownField().text()).not.toContain('Switch to rich text');
+ });
+
+ it('passes down any additional props to markdown field component', () => {
+ const propsData = {
+ line: { text: 'hello world', richText: 'hello world' },
+ lines: [{ text: 'hello world', richText: 'hello world' }],
+ canSuggest: true,
+ };
+
+ buildWrapper({
+ propsData: { ...propsData, myCustomProp: 'myCustomValue', 'data-testid': 'custom id' },
+ });
+
+ expect(findMarkdownField().props()).toMatchObject(propsData);
+ expect(findMarkdownField().vm.$attrs).toMatchObject({
+ myCustomProp: 'myCustomValue',
+
+ // data-testid isn't copied over
+ 'data-testid': 'markdown-field',
+ });
+ });
+
+ describe('disabled', () => {
+ it('disables markdown field when disabled prop is true', () => {
+ buildWrapper({ propsData: { disabled: true } });
+
+ expect(findMarkdownField().find('textarea').attributes('disabled')).toBeDefined();
+ });
+
+ it('enables markdown field when disabled prop is false', () => {
+ buildWrapper({ propsData: { disabled: false } });
+
+ expect(findMarkdownField().find('textarea').attributes('disabled')).toBe(undefined);
+ });
+
+ it('disables content editor when disabled prop is true', async () => {
+ buildWrapper({ propsData: { disabled: true } });
+
+ await enableContentEditor();
+
+ expect(findContentEditor().props('editable')).toBe(false);
+ });
+
+ it('enables content editor when disabled prop is false', async () => {
+ buildWrapper({ propsData: { disabled: false } });
+
+ await enableContentEditor();
+
+ expect(findContentEditor().props('editable')).toBe(true);
+ });
+ });
+
+ describe('autosize', () => {
+ it('autosizes the textarea when the value changes', async () => {
+ buildWrapper();
+ await findTextarea().setValue('Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines');
+ await nextTick();
+ expect(Autosize.update).toHaveBeenCalled();
+ });
+
+ it('autosizes the textarea when the value changes from outside the component', async () => {
+ buildWrapper();
+ wrapper.setProps({ value: 'Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines' });
+
+ await nextTick();
+ await waitForPromises();
+ expect(Autosize.update).toHaveBeenCalled();
+ });
+
+ it('does not autosize the textarea if markdown editor is disabled', async () => {
+ buildWrapper();
+ await enableContentEditor();
+
+ wrapper.setProps({ value: 'Lots of newlines\n\n\n\n\n\n\nMore content\n\n\nand newlines' });
+
+ expect(Autosize.update).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('autosave', () => {
+ it('automatically saves the textarea value to local storage if autosaveKey is defined', () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: 'This is **markdown**' } });
+
+ expect(localStorage.getItem('autosave/issue/1234')).toBe('This is **markdown**');
+ });
+
+ it("loads value from local storage if autosaveKey is defined, and value isn't", () => {
+ localStorage.setItem('autosave/issue/1234', 'This is **markdown**');
+
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } });
+
+ expect(findTextarea().element.value).toBe('This is **markdown**');
+ });
+
+ it("doesn't load value from local storage if autosaveKey is defined, and value is", () => {
+ localStorage.setItem('autosave/issue/1234', 'This is **markdown**');
+
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+
+ expect(findTextarea().element.value).toBe('test markdown');
+ });
+
+ it('does not save the textarea value to local storage if autosaveKey is not defined', () => {
+ buildWrapper({ propsData: { value: 'This is **markdown**' } });
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+
+ it('does not save the textarea value to local storage if value is empty', () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234', value: '' } });
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+
+ describe('clear local storage event handler', () => {
+ it('does not clear the local storage if the autosave key is not defined', async () => {
+ buildWrapper();
+
+ await waitForPromises();
+
+ markdownEditorEventHub.$emit(CLEAR_AUTOSAVE_ENTRY_EVENT, 'issue/1234');
+
+ expect(localStorage.removeItem).not.toHaveBeenCalled();
+ });
+
+ it('does not clear the local storage if the event autosave key does not match', async () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+
+ await waitForPromises();
+
+ markdownEditorEventHub.$emit(CLEAR_AUTOSAVE_ENTRY_EVENT, 'issue/1235');
+
+ expect(localStorage.removeItem).not.toHaveBeenCalled();
+ });
+
+ it('clears the local storage if the event autosave key matches', async () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+
+ await waitForPromises();
+
+ markdownEditorEventHub.$emit(CLEAR_AUTOSAVE_ENTRY_EVENT, 'issue/1234');
+
+ expect(localStorage.removeItem).toHaveBeenCalledWith('autosave/issue/1234');
+ });
+ });
+ });
+
it('renders markdown field textarea', () => {
- buildWrapper();
+ buildWrapper({ propsData: { supportsQuickActions: true } });
expect(findTextarea().attributes()).toEqual(
expect.objectContaining({
@@ -92,6 +299,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
name: formFieldName,
placeholder: formFieldPlaceholder,
'aria-label': formFieldAriaLabel,
+ 'data-supports-quick-actions': 'true',
}),
);
@@ -99,31 +307,26 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
it('fails to render if textarea id and name is not passed', () => {
- expect(() => {
- buildWrapper({ propsData: { formFieldProps: {} } });
- }).toThrow('Invalid prop: custom validator check failed for prop "formFieldProps"');
+ expect(() => assertProps(MarkdownEditor, { ...defaultProps, formFieldProps: {} })).toThrow(
+ 'Invalid prop: custom validator check failed for prop "formFieldProps"',
+ );
});
it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => {
buildWrapper();
- findMarkdownField().vm.$emit('enableContentEditor');
-
- await nextTick();
+ await enableContentEditor();
expect(wrapper.emitted(EDITING_MODE_CONTENT_EDITOR)).toHaveLength(1);
});
it(`emits ${EDITING_MODE_MARKDOWN_FIELD} event when enableMarkdownEditor emitted from content editor`, async () => {
buildWrapper({
- stubs: { ContentEditor: stubComponent(ContentEditor) },
+ stubs: { ContentEditor: ContentEditorStub },
});
- findMarkdownField().vm.$emit('enableContentEditor');
-
- await nextTick();
-
- findContentEditor().vm.$emit('enableMarkdownEditor');
+ await enableContentEditor();
+ await enableMarkdownEditor();
expect(wrapper.emitted(EDITING_MODE_MARKDOWN_FIELD)).toHaveLength(1);
});
@@ -135,7 +338,17 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await findTextarea().setValue(newValue);
- expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ expect(wrapper.emitted('input')).toEqual([[value], [newValue]]);
+ });
+
+ it('autosaves the markdown value to local storage', async () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+
+ const newValue = 'new value';
+
+ await findTextarea().setValue(newValue);
+
+ expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue);
});
describe('when autofocus is true', () => {
@@ -159,9 +372,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
describe(`when markdown field triggers enableContentEditor event`, () => {
- beforeEach(() => {
+ beforeEach(async () => {
buildWrapper();
- findMarkdownField().vm.$emit('enableContentEditor');
+ await enableContentEditor();
});
it('displays the content editor', () => {
@@ -169,7 +382,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect.objectContaining({
renderMarkdown: expect.any(Function),
uploadsPath: window.uploads_path,
- useBottomToolbar: false,
markdown: value,
}),
);
@@ -197,17 +409,27 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
+ describe('when contentEditor is disabled', () => {
+ it('resets the editingMode to markdownField', () => {
+ localStorage.setItem('gl-markdown-editor-mode', 'contentEditor');
+
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234', enableContentEditor: false } });
+
+ expect(wrapper.vm.editingMode).toBe(EDITING_MODE_MARKDOWN_FIELD);
+ });
+ });
+
describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
- beforeEach(() => {
- buildWrapper();
- findMarkdownField().vm.$emit('enableContentEditor');
+ beforeEach(async () => {
+ buildWrapper({ propsData: { autosaveKey: 'issue/1234' } });
+ await enableContentEditor();
});
describe('when autofocus is true', () => {
beforeEach(() => {
buildWrapper({
propsData: { autofocus: true },
- stubs: { ContentEditor: stubComponent(ContentEditor) },
+ stubs: { ContentEditor: ContentEditorStub },
});
});
@@ -221,7 +443,15 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
await findContentEditor().vm.$emit('change', { markdown: newValue });
- expect(wrapper.emitted('input')).toEqual([[newValue]]);
+ expect(wrapper.emitted('input')).toEqual([[value], [newValue]]);
+ });
+
+ it('autosaves the content editor value to local storage', async () => {
+ const newValue = 'new value';
+
+ await findContentEditor().vm.$emit('change', { markdown: newValue });
+
+ expect(localStorage.getItem('autosave/issue/1234')).toBe(newValue);
});
it('bubbles up keydown event', () => {
@@ -233,9 +463,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
describe(`when richText editor triggers enableMarkdownEditor event`, () => {
- beforeEach(() => {
- findContentEditor().vm.$emit('enableMarkdownEditor');
- });
+ beforeEach(enableMarkdownEditor);
it('hides the content editor', () => {
expect(findContentEditor().exists()).toBe(false);
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 9db1b779a04..9768bc7a6dd 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
@@ -25,7 +25,7 @@ describe('Suggestion Diff component', () => {
...props,
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
@@ -34,10 +34,6 @@ describe('Suggestion Diff component', () => {
window.gon.current_user_id = 1;
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findApplyButton = () => wrapper.findComponent(ApplySuggestion);
const findApplyBatchButton = () => wrapper.find('.js-apply-batch-btn');
const findAddToBatchButton = () => wrapper.find('.js-add-to-batch-btn');
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
index f9a8b64f89b..c46a2d3e117 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
@@ -36,10 +36,6 @@ describe('SuggestionDiffRow', () => {
const findNewLineWrapper = () => wrapper.find('.new_line');
const findSuggestionContent = () => wrapper.find('[data-testid="suggestion-diff-content"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('renders correctly', () => {
it('renders the correct base suggestion markup', () => {
factory({
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
index d84483c1663..8c7f51664ad 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js
@@ -61,11 +61,6 @@ describe('Suggestion Diff component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
index 8f4235cfe41..2fdab40b4bd 100644
--- a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
@@ -1,4 +1,5 @@
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue';
const MOCK_DATA = {
@@ -48,56 +49,37 @@ const MOCK_DATA = {
};
describe('Suggestion component', () => {
- let vm;
- let diffTable;
+ let wrapper;
- beforeEach(async () => {
- const Component = Vue.extend(SuggestionsComponent);
+ const createComponent = (props = {}) => {
+ wrapper = mountExtended(SuggestionsComponent, {
+ propsData: {
+ ...MOCK_DATA,
+ ...props,
+ },
+ });
+ };
- vm = new Component({
- propsData: MOCK_DATA,
- }).$mount();
+ const findSuggestionsContainer = () => wrapper.findByTestId('suggestions-container');
- diffTable = vm.generateDiff(0).$mount().$el;
+ beforeEach(async () => {
+ createComponent();
- jest.spyOn(vm, 'renderSuggestions').mockImplementation(() => {});
- vm.renderSuggestions();
await nextTick();
});
describe('mounted', () => {
it('renders a flash container', () => {
- expect(vm.$el.querySelector('.js-suggestions-flash')).not.toBeNull();
+ expect(wrapper.find('.js-suggestions-flash').exists()).toBe(true);
});
it('renders a container for suggestions', () => {
- expect(vm.$refs.container).not.toBeNull();
+ expect(findSuggestionsContainer().exists()).toBe(true);
});
it('renders suggestions', () => {
- expect(vm.renderSuggestions).toHaveBeenCalled();
- expect(vm.$el.innerHTML.includes('oldtest')).toBe(true);
- expect(vm.$el.innerHTML.includes('newtest')).toBe(true);
- });
- });
-
- describe('generateDiff', () => {
- it('generates a diff table', () => {
- expect(diffTable.querySelector('.md-suggestion-diff')).not.toBeNull();
- });
-
- it('generates a diff table that contains contents the suggested lines', () => {
- MOCK_DATA.suggestions[0].diff_lines.forEach((line) => {
- const text = line.text.substring(1);
-
- expect(diffTable.innerHTML.includes(text)).toBe(true);
- });
- });
-
- it('generates a diff table with the correct line number for each suggested line', () => {
- const lines = diffTable.querySelectorAll('.old_line');
-
- expect(parseInt([...lines][0].innerHTML, 10)).toBe(5);
+ expect(findSuggestionsContainer().text()).toContain('oldtest');
+ expect(findSuggestionsContainer().text()).toContain('newtest');
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
index 82210e79799..33e9d6add99 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_button_spec.js
@@ -20,11 +20,6 @@ describe('toolbar_button', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const getButtonShortcutsAttr = () => {
return wrapper.findComponent(GlButton).attributes('data-md-shortcuts');
};
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index b1a1dbbeb7a..2489421b697 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -1,6 +1,5 @@
import { mount } from '@vue/test-utils';
import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
-import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
describe('toolbar', () => {
let wrapper;
@@ -11,10 +10,6 @@ describe('toolbar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('user can attach file', () => {
beforeEach(() => {
createMountedWrapper();
@@ -48,18 +43,4 @@ describe('toolbar', () => {
expect(wrapper.find('.comment-toolbar').exists()).toBe(true);
});
});
-
- describe('with content editor switcher', () => {
- beforeEach(() => {
- createMountedWrapper({
- showContentEditorSwitcher: true,
- });
- });
-
- it('re-emits event from switcher', () => {
- wrapper.findComponent(EditorModeDropdown).vm.$emit('input', 'richText');
-
- expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
- });
- });
});
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
index 2b311b75f85..6f4902e3f96 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/markdown_drawer_spec.js
@@ -36,8 +36,6 @@ describe('MarkdownDrawer', () => {
};
afterEach(() => {
- wrapper.destroy();
- wrapper = null;
Object.keys(cache).forEach((key) => delete cache[key]);
});
@@ -158,7 +156,7 @@ describe('MarkdownDrawer', () => {
renderGLFMSpy.mockClear();
});
- it('fetches the Markdown and caches it', async () => {
+ it('fetches the Markdown and caches it', () => {
expect(getRenderedMarkdown).toHaveBeenCalledTimes(1);
expect(Object.keys(cache)).toHaveLength(1);
});
@@ -201,13 +199,13 @@ describe('MarkdownDrawer', () => {
afterEach(() => {
getRenderedMarkdown.mockClear();
});
- it('shows alert', () => {
+ it('shows an alert', () => {
expect(findAlert().exists()).toBe(true);
});
});
describe('While Markdown is fetching', () => {
- beforeEach(async () => {
+ beforeEach(() => {
getRenderedMarkdown.mockReturnValue(new Promise(() => {}));
createComponent();
@@ -217,7 +215,7 @@ describe('MarkdownDrawer', () => {
getRenderedMarkdown.mockClear();
});
- it('shows skeleton', async () => {
+ it('shows skeleton', () => {
expect(findSkeleton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/vue_shared/components/memory_graph_spec.js b/spec/frontend/vue_shared/components/memory_graph_spec.js
index ae8d5ff78ba..81325fb3269 100644
--- a/spec/frontend/vue_shared/components/memory_graph_spec.js
+++ b/spec/frontend/vue_shared/components/memory_graph_spec.js
@@ -1,10 +1,8 @@
import { GlSparklineChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
describe('MemoryGraph', () => {
- const Component = Vue.extend(MemoryGraph);
let wrapper;
const metrics = [
[1573586253.853, '2.87'],
@@ -13,12 +11,10 @@ describe('MemoryGraph', () => {
[1573586433.853, '3.0066964285714284'],
];
- afterEach(() => {
- wrapper.destroy();
- });
+ const findGlSparklineChart = () => wrapper.findComponent(GlSparklineChart);
beforeEach(() => {
- wrapper = shallowMount(Component, {
+ wrapper = shallowMount(MemoryGraph, {
propsData: {
metrics,
width: 100,
@@ -27,19 +23,15 @@ describe('MemoryGraph', () => {
});
});
- describe('chartData', () => {
- it('should calculate chartData', () => {
- expect(wrapper.vm.chartData.length).toEqual(metrics.length);
- });
-
- it('should format date & MB values', () => {
+ describe('Chart data', () => {
+ it('should have formatted date & MB values', () => {
const formattedData = [
['Nov 12 2019 19:17:33', '2.87'],
['Nov 12 2019 19:18:33', '2.78'],
['Nov 12 2019 19:19:33', '2.78'],
['Nov 12 2019 19:20:33', '3.01'],
];
- expect(wrapper.vm.chartData).toEqual(formattedData);
+ expect(findGlSparklineChart().props('data')).toEqual(formattedData);
});
});
@@ -47,7 +39,7 @@ describe('MemoryGraph', () => {
it('should draw container with chart', () => {
expect(wrapper.element).toMatchSnapshot();
expect(wrapper.find('.memory-graph-container').exists()).toBe(true);
- expect(wrapper.findComponent(GlSparklineChart).exists()).toBe(true);
+ expect(findGlSparklineChart().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
index 1789610dba9..4b0b89fe1e7 100644
--- a/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/metric_images_tab_spec.js
@@ -45,13 +45,6 @@ describe('Metric images tab', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findUploadDropzone = () => wrapper.findComponent(UploadDropzone);
const findImages = () => wrapper.findAllComponents(MetricImagesTable);
const findModal = () => wrapper.findComponent(GlModal);
diff --git a/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
index 9c91dc9b5fc..12dca95e9ba 100644
--- a/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/metric_images_table_spec.js
@@ -39,13 +39,6 @@ describe('Metrics upload item', () => {
);
};
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
const findImageLink = () => wrapper.findComponent(GlLink);
const findLabelTextSpan = () => wrapper.find('[data-testid="metric-image-label-span"]');
const findCollapseButton = () => wrapper.find('[data-testid="collapse-button"]');
diff --git a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
index 537367940e0..626f6fc735e 100644
--- a/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/metric_images/store/actions_spec.js
@@ -4,11 +4,11 @@ import actionsFactory from '~/vue_shared/components/metric_images/store/actions'
import * as types from '~/vue_shared/components/metric_images/store/mutation_types';
import createStore from '~/vue_shared/components/metric_images/store';
import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { fileList, initialData } from '../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
const service = {
getMetricImages: jest.fn(),
uploadMetricImage: jest.fn(),
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index 61e4e774420..2f8f97c5b95 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -6,10 +6,6 @@ import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
describe('modal copy button', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- });
-
beforeEach(() => {
wrapper = shallowMount(ModalCopyButton, {
propsData: {
@@ -17,16 +13,9 @@ describe('modal copy button', () => {
title: 'Copy this value',
id: 'test-id',
},
- slots: {
- default: 'test',
- },
});
});
- it('should show the default slot', () => {
- expect(wrapper.text()).toBe('test');
- });
-
describe('clipboard', () => {
it('should fire a `success` event on click', async () => {
const root = createWrapper(wrapper.vm.$root);
diff --git a/spec/frontend/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
index b1bec28bffb..947ee756259 100644
--- a/spec/frontend/vue_shared/components/navigation_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
@@ -38,11 +38,6 @@ describe('navigation tabs component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('should render tabs', () => {
expect(wrapper.findAllComponents(GlTab)).toHaveLength(data.length);
});
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
index 31320b1d2a6..a116233a065 100644
--- a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
@@ -21,7 +21,7 @@ import {
searchProjectsWithinGroupQueryResponse,
} from './mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('NewResourceDropdown component', () => {
useLocalStorageSpy();
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 3bac96069ec..de53caa66c7 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
@@ -57,7 +57,9 @@ exports[`Issue placeholder note component matches snapshot 1`] = `
<div
class="note-text md"
>
- <p>
+ <p
+ dir="auto"
+ >
Foo
</p>
diff --git a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
index 17a62ae8a33..d7fcb9a25d4 100644
--- a/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
+++ b/spec/frontend/vue_shared/components/notes/noteable_warning_spec.js
@@ -22,13 +22,6 @@ describe('Issue Warning Component', () => {
},
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
- });
-
describe('when issue is locked but not confidential', () => {
beforeEach(() => {
wrapper = createComponent({
@@ -132,12 +125,6 @@ describe('Issue Warning Component', () => {
});
});
- afterEach(() => {
- wrapperLocked.destroy();
- wrapperConfidential.destroy();
- wrapperLockedAndConfidential.destroy();
- });
-
it('renders confidential & locked messages with noteable "issue"', () => {
expect(findLockedBlock(wrapperLocked).text()).toContain('This issue is locked.');
expect(findConfidentialBlock(wrapperConfidential).text()).toContain(
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index 8f9f1bb336f..7e669fb7c71 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -30,11 +30,6 @@ describe('Issue placeholder note component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches snapshot', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
index de6ab43bc41..5897b9e0ffc 100644
--- a/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -12,11 +12,6 @@ describe('Placeholder system note component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('matches snapshot', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index bcfd7a8ec70..7f3912dcadb 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -46,7 +46,6 @@ describe('system note component', () => {
});
afterEach(() => {
- vm.destroy();
mock.restore();
});
@@ -65,7 +64,7 @@ describe('system note component', () => {
it('should render svg icon', () => {
createComponent(props);
- expect(vm.find('.timeline-icon svg').exists()).toBe(true);
+ expect(vm.find('[data-testid="timeline-icon"]').exists()).toBe(true);
});
// Redcarpet Markdown renderer wraps text in `<p>` tags
diff --git a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
index bd4b6a463ab..fa9d3cd28a9 100644
--- a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
+++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
@@ -10,10 +10,6 @@ describe(`TimelineEntryItem`, () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders correctly', () => {
factory();
diff --git a/spec/frontend/vue_shared/components/ordered_layout_spec.js b/spec/frontend/vue_shared/components/ordered_layout_spec.js
index 21588569d6a..b6c8c467028 100644
--- a/spec/frontend/vue_shared/components/ordered_layout_spec.js
+++ b/spec/frontend/vue_shared/components/ordered_layout_spec.js
@@ -37,10 +37,6 @@ describe('Ordered Layout', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when slotKeys are in initial slot order', () => {
beforeEach(() => {
createComponent({ slotKeys: regularSlotOrder });
diff --git a/spec/frontend/vue_shared/components/page_size_selector_spec.js b/spec/frontend/vue_shared/components/page_size_selector_spec.js
index 5ec0b863afd..fce7ceee2fe 100644
--- a/spec/frontend/vue_shared/components/page_size_selector_spec.js
+++ b/spec/frontend/vue_shared/components/page_size_selector_spec.js
@@ -14,10 +14,6 @@ describe('Page size selector component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each(PAGE_SIZES)('shows expected text in the dropdown button for page size %s', (pageSize) => {
createWrapper({ pageSize });
diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js
index ae9c920ebd2..fc9adab2e2b 100644
--- a/spec/frontend/vue_shared/components/paginated_list_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_list_spec.js
@@ -33,10 +33,6 @@ describe('Pagination links component', () => {
[glPaginatedList] = wrapper.vm.$children;
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Paginated List Component', () => {
describe('props', () => {
// We test attrs and not props because we pass through to child component using v-bind:"$attrs"
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 86a63db0d9e..9b6f5ae3e38 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
@@ -90,12 +90,6 @@ describe('AlertManagementEmptyState', () => {
mountComponent();
});
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
const EmptyState = () => wrapper.find('.empty-state');
const ItemsTable = () => wrapper.find('.gl-table');
const ErrorAlert = () => wrapper.findComponent(GlAlert);
@@ -108,16 +102,23 @@ describe('AlertManagementEmptyState', () => {
const findStatusTabs = () => wrapper.findComponent(GlTabs);
const findStatusFilterBadge = () => wrapper.findAllComponents(GlBadge);
+ const handleFilterItems = (filters) => {
+ Filters().vm.$emit('onFilter', filters);
+ return nextTick();
+ };
+
describe('Snowplow tracking', () => {
+ const category = 'category';
+ const action = 'action';
+
beforeEach(() => {
jest.spyOn(Tracking, 'event');
mountComponent({
- props: { trackViewsOptions: { category: 'category', action: 'action' } },
+ props: { trackViewsOptions: { category, action } },
});
});
it('should track the items list page views', () => {
- const { category, action } = wrapper.vm.trackViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
});
@@ -234,14 +235,14 @@ describe('AlertManagementEmptyState', () => {
findPagination().vm.$emit('input', 3);
await nextTick();
- expect(wrapper.vm.previousPage).toBe(2);
+ expect(findPagination().props('prevPage')).toBe(2);
});
it('returns 0 when it is the first page', async () => {
findPagination().vm.$emit('input', 1);
await nextTick();
- expect(wrapper.vm.previousPage).toBe(0);
+ expect(findPagination().props('prevPage')).toBe(0);
});
});
@@ -265,14 +266,14 @@ describe('AlertManagementEmptyState', () => {
findPagination().vm.$emit('input', 1);
await nextTick();
- expect(wrapper.vm.nextPage).toBe(2);
+ expect(findPagination().props('nextPage')).toBe(2);
});
it('returns `null` when currentPage is already last page', async () => {
findStatusTabs().vm.$emit('input', 1);
findPagination().vm.$emit('input', 1);
await nextTick();
- expect(wrapper.vm.nextPage).toBeNull();
+ expect(findPagination().props('nextPage')).toBeNull();
});
});
});
@@ -320,36 +321,32 @@ describe('AlertManagementEmptyState', () => {
it('returns correctly applied filter search values', async () => {
const searchTerm = 'foo';
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- searchTerm,
- });
-
+ await handleFilterItems([{ type: 'filtered-search-term', value: { data: searchTerm } }]);
await nextTick();
- expect(wrapper.vm.filteredSearchValue).toEqual([searchTerm]);
+ expect(Filters().props('initialFilterValue')).toEqual([searchTerm]);
});
- it('updates props tied to getIncidents GraphQL query', () => {
- wrapper.vm.handleFilterItems(mockFilters);
-
- expect(wrapper.vm.authorUsername).toBe('root');
- expect(wrapper.vm.assigneeUsername).toEqual('root2');
- expect(wrapper.vm.searchTerm).toBe(mockFilters[2].value.data);
- });
+ it('updates props tied to getIncidents GraphQL query', async () => {
+ await handleFilterItems(mockFilters);
- it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- authorUsername: 'foo',
- searchTerm: 'bar',
- });
+ const [
+ {
+ value: { data: authorUsername },
+ },
+ {
+ value: { data: assigneeUsername },
+ },
+ searchTerm,
+ ] = Filters().props('initialFilterValue');
- wrapper.vm.handleFilterItems([]);
+ expect(authorUsername).toBe('root');
+ expect(assigneeUsername).toEqual('root2');
+ expect(searchTerm).toBe(mockFilters[2].value.data);
+ });
- expect(wrapper.vm.authorUsername).toBe('');
- expect(wrapper.vm.searchTerm).toBe('');
+ it('updates props `searchTerm` and `authorUsername` with empty values when passed filters param is empty', async () => {
+ await handleFilterItems([]);
+ expect(Filters().props('initialFilterValue')).toEqual([]);
});
});
});
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 112cdaf74c6..2a1a6342c38 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
@@ -25,10 +25,6 @@ describe('Pagination bar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('events', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/pagination_links_spec.js b/spec/frontend/vue_shared/components/pagination_links_spec.js
index d444ad7a733..99a4f776305 100644
--- a/spec/frontend/vue_shared/components/pagination_links_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_links_spec.js
@@ -44,10 +44,6 @@ describe('Pagination links component', () => {
glPagination = wrapper.findComponent(GlPagination);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should provide translated text to GitLab UI pagination', () => {
Object.entries(translations).forEach((entry) => {
expect(glPagination.vm[entry[0]]).toBe(entry[1]);
diff --git a/spec/frontend/vue_shared/components/panel_resizer_spec.js b/spec/frontend/vue_shared/components/panel_resizer_spec.js
index 0e261124cbf..a535fe4939c 100644
--- a/spec/frontend/vue_shared/components/panel_resizer_spec.js
+++ b/spec/frontend/vue_shared/components/panel_resizer_spec.js
@@ -27,10 +27,6 @@ describe('Panel Resizer component', () => {
el.dispatchEvent(event);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render a div element with the correct classes and styles', () => {
wrapper = mount(PanelResizer, {
propsData: {
diff --git a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
index ff4febd647e..a44a1aba8c0 100644
--- a/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
+++ b/spec/frontend/vue_shared/components/papa_parse_alert_spec.js
@@ -16,10 +16,6 @@ describe('app/assets/javascripts/vue_shared/components/papa_parse_alert.vue', ()
const findAlert = () => wrapper.findComponent(GlAlert);
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render alert with correct props', async () => {
createComponent({ errorMessages: [{ code: 'MissingQuotes' }] });
await nextTick();
diff --git a/spec/frontend/vue_shared/components/project_avatar_spec.js b/spec/frontend/vue_shared/components/project_avatar_spec.js
index af828fbca51..330ff001db9 100644
--- a/spec/frontend/vue_shared/components/project_avatar_spec.js
+++ b/spec/frontend/vue_shared/components/project_avatar_spec.js
@@ -15,10 +15,6 @@ describe('ProjectAvatar', () => {
wrapper = shallowMount(ProjectAvatar, { propsData: { ...defaultProps, ...props }, attrs });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders GlAvatar with correct props', () => {
createComponent();
@@ -29,6 +25,7 @@ describe('ProjectAvatar', () => {
entityName: defaultProps.projectName,
size: 32,
src: '',
+ fallbackOnError: true,
});
});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
index 4e0c318c84e..d704fcc0e7b 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -1,57 +1,49 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import { GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
describe('ProjectListItem component', () => {
- const Component = Vue.extend(ProjectListItem);
let wrapper;
- let vm;
- let options;
const project = JSON.parse(JSON.stringify(mockProjects))[0];
- beforeEach(() => {
- options = {
+ const createWrapper = ({ propsData } = {}) => {
+ wrapper = shallowMountExtended(ProjectListItem, {
propsData: {
project,
selected: false,
+ ...propsData,
},
- };
- });
-
- afterEach(() => {
- wrapper.vm.$destroy();
- });
-
- it('does not render a check mark icon if selected === false', () => {
- wrapper = shallowMount(Component, options);
-
- expect(wrapper.find('.js-selected-icon').exists()).toBe(false);
- });
+ });
+ };
- it('renders a check mark icon if selected === true', () => {
- options.propsData.selected = true;
+ const findProjectNamespace = () => wrapper.findByTestId('project-namespace');
+ const findProjectName = () => wrapper.findByTestId('project-name');
- wrapper = shallowMount(Component, options);
+ it.each([true, false])('renders a checkmark correctly when selected === "%s"', (selected) => {
+ createWrapper({
+ propsData: {
+ selected,
+ },
+ });
- expect(wrapper.find('.js-selected-icon').exists()).toBe(true);
+ expect(wrapper.findByTestId('selected-icon').exists()).toBe(selected);
});
- it(`emits a "clicked" event when clicked`, () => {
- wrapper = shallowMount(Component, options);
- ({ vm } = wrapper);
+ it(`emits a "clicked" event when the button is clicked`, () => {
+ createWrapper();
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- wrapper.vm.onClick();
+ expect(wrapper.emitted('click')).toBeUndefined();
+ wrapper.findComponent(GlButton).vm.$emit('click');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ expect(wrapper.emitted('click')).toHaveLength(1);
});
it(`renders the project avatar`, () => {
- wrapper = shallowMount(Component, options);
+ createWrapper();
const avatar = wrapper.findComponent(ProjectAvatar);
expect(avatar.exists()).toBe(true);
@@ -63,48 +55,73 @@ describe('ProjectListItem component', () => {
});
it(`renders a simple namespace name with a trailing slash`, () => {
- options.propsData.project.name_with_namespace = 'a / b';
-
- wrapper = shallowMount(Component, options);
- const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name_with_namespace: 'a / b',
+ },
+ },
+ });
+ const renderedNamespace = trimText(findProjectNamespace().text());
expect(renderedNamespace).toBe('a /');
});
it(`renders a properly truncated namespace with a trailing slash`, () => {
- options.propsData.project.name_with_namespace = 'a / b / c / d / e / f';
-
- wrapper = shallowMount(Component, options);
- const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name_with_namespace: 'a / b / c / d / e / f',
+ },
+ },
+ });
+ const renderedNamespace = trimText(findProjectNamespace().text());
expect(renderedNamespace).toBe('a / ... / e /');
});
it(`renders a simple namespace name of a GraphQL project`, () => {
- options.propsData.project.name_with_namespace = undefined;
- options.propsData.project.nameWithNamespace = 'test';
-
- wrapper = shallowMount(Component, options);
- const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name_with_namespace: undefined,
+ nameWithNamespace: 'test',
+ },
+ },
+ });
+ const renderedNamespace = trimText(findProjectNamespace().text());
expect(renderedNamespace).toBe('test /');
});
it(`renders the project name`, () => {
- options.propsData.project.name = 'my-test-project';
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').text());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name: 'my-test-project',
+ },
+ },
+ });
+ const renderedName = trimText(findProjectName().text());
expect(renderedName).toBe('my-test-project');
});
it(`renders the project name with highlighting in the case of a search query match`, () => {
- options.propsData.project.name = 'my-test-project';
- options.propsData.matcher = 'pro';
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').html());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name: 'my-test-project',
+ },
+ matcher: 'pro',
+ },
+ });
+ const renderedName = trimText(findProjectName().html());
const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject';
expect(renderedName).toContain(expected);
@@ -112,11 +129,16 @@ describe('ProjectListItem component', () => {
it('prevents search query and project name XSS', () => {
const alertSpy = jest.spyOn(window, 'alert');
- options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject";
- options.propsData.matcher = "pro<script>alert('XSS');</script>";
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').html());
+ createWrapper({
+ propsData: {
+ project: {
+ ...project,
+ name: "my-xss-pro<script>alert('XSS');</script>ject",
+ },
+ matcher: "pro<script>alert('XSS');</script>",
+ },
+ });
+ const renderedName = trimText(findProjectName().html());
const expected = 'my-xss-project';
expect(renderedName).toContain(expected);
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
index a0832dd7030..5e304f1c118 100644
--- a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -1,7 +1,7 @@
import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { head } from 'lodash';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import mockProjects from 'test_fixtures_static/projects.json';
import { trimText } from 'helpers/text_helper';
import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
@@ -25,7 +25,7 @@ describe('ProjectSelector component', () => {
};
beforeEach(() => {
- wrapper = mount(Vue.extend(ProjectSelector), {
+ wrapper = mount(ProjectSelector, {
propsData: {
projectSearchResults: searchResults,
selectedProjects: selected,
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
new file mode 100644
index 00000000000..3e4d5c558f6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -0,0 +1,266 @@
+import { GlAvatarLabeled, GlBadge, GlIcon, GlPopover } from '@gitlab/ui';
+import projects from 'test_fixtures/api/users/projects/get.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import {
+ VISIBILITY_TYPE_ICON,
+ VISIBILITY_LEVEL_PRIVATE_STRING,
+ PROJECT_VISIBILITY_TYPE,
+} from '~/visibility_level/constants';
+import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
+import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { FEATURABLE_DISABLED, FEATURABLE_ENABLED } from '~/featurable/constants';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+jest.mock('lodash/uniqueId', () => (prefix) => `${prefix}1`);
+
+describe('ProjectsListItem', () => {
+ let wrapper;
+
+ const [project] = convertObjectPropsToCamelCase(projects, { deep: true });
+
+ const defaultPropsData = { project };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(ProjectsListItem, {
+ propsData: { ...defaultPropsData, ...propsData },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
+ const findIssuesLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.issues });
+ const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks });
+ const findProjectTopics = () => wrapper.findByTestId('project-topics');
+ const findPopover = () => findProjectTopics().findComponent(GlPopover);
+ const findProjectDescription = () => wrapper.findByTestId('project-description');
+
+ it('renders project avatar', () => {
+ createComponent();
+
+ const avatarLabeled = findAvatarLabeled();
+
+ expect(avatarLabeled.props()).toMatchObject({
+ label: project.name,
+ labelLink: project.webUrl,
+ });
+ expect(avatarLabeled.attributes()).toMatchObject({
+ 'entity-id': project.id.toString(),
+ 'entity-name': project.name,
+ shape: 'rect',
+ size: '48',
+ });
+ });
+
+ it('renders visibility icon with tooltip', () => {
+ createComponent();
+
+ const icon = findAvatarLabeled().findComponent(GlIcon);
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+
+ expect(icon.props('name')).toBe(VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PRIVATE_STRING]);
+ expect(tooltip.value).toBe(PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]);
+ });
+
+ it('renders access role badge', () => {
+ createComponent();
+
+ expect(findAvatarLabeled().findComponent(UserAccessRoleBadge).text()).toBe(
+ ACCESS_LEVEL_LABELS[project.permissions.projectAccess.accessLevel],
+ );
+ });
+
+ describe('if project is archived', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ project: {
+ ...project,
+ archived: true,
+ },
+ },
+ });
+ });
+
+ it('renders the archived badge', () => {
+ expect(
+ wrapper
+ .findAllComponents(GlBadge)
+ .wrappers.find((badge) => badge.text() === ProjectsListItem.i18n.archived),
+ ).not.toBeUndefined();
+ });
+ });
+
+ it('renders stars count', () => {
+ createComponent();
+
+ const starsLink = wrapper.findByRole('link', { name: ProjectsListItem.i18n.stars });
+ const tooltip = getBinding(starsLink.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(ProjectsListItem.i18n.stars);
+ expect(starsLink.attributes('href')).toBe(`${project.webUrl}/-/starrers`);
+ expect(starsLink.text()).toBe(project.starCount.toString());
+ expect(starsLink.findComponent(GlIcon).props('name')).toBe('star-o');
+ });
+
+ it('renders updated at', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(project.updatedAt);
+ });
+
+ describe('when issues are enabled', () => {
+ it('renders issues count', () => {
+ createComponent();
+
+ const issuesLink = findIssuesLink();
+ const tooltip = getBinding(issuesLink.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(ProjectsListItem.i18n.issues);
+ expect(issuesLink.attributes('href')).toBe(`${project.webUrl}/-/issues`);
+ expect(issuesLink.text()).toBe(project.openIssuesCount.toString());
+ expect(issuesLink.findComponent(GlIcon).props('name')).toBe('issues');
+ });
+ });
+
+ describe('when issues are not enabled', () => {
+ it('does not render issues count', () => {
+ createComponent({
+ propsData: {
+ project: {
+ ...project,
+ issuesAccessLevel: FEATURABLE_DISABLED,
+ },
+ },
+ });
+
+ expect(findIssuesLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when forking is enabled', () => {
+ it('renders forks count', () => {
+ createComponent();
+
+ const forksLink = findForksLink();
+ const tooltip = getBinding(forksLink.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(ProjectsListItem.i18n.forks);
+ expect(forksLink.attributes('href')).toBe(`${project.webUrl}/-/forks`);
+ expect(forksLink.text()).toBe(project.openIssuesCount.toString());
+ expect(forksLink.findComponent(GlIcon).props('name')).toBe('fork');
+ });
+ });
+
+ describe('when forking is not enabled', () => {
+ it.each([
+ {
+ ...project,
+ forksCount: 2,
+ forkingAccessLevel: FEATURABLE_DISABLED,
+ },
+ {
+ ...project,
+ forksCount: undefined,
+ forkingAccessLevel: FEATURABLE_ENABLED,
+ },
+ ])('does not render forks count', (modifiedProject) => {
+ createComponent({
+ propsData: {
+ project: modifiedProject,
+ },
+ });
+
+ expect(findForksLink().exists()).toBe(false);
+ });
+ });
+
+ describe('if project has topics', () => {
+ it('renders first three topics', () => {
+ createComponent();
+
+ const firstThreeTopics = project.topics.slice(0, 3);
+ const firstThreeBadges = findProjectTopics().findAllComponents(GlBadge).wrappers.slice(0, 3);
+ const firstThreeBadgesText = firstThreeBadges.map((badge) => badge.text());
+ const firstThreeBadgesHref = firstThreeBadges.map((badge) => badge.attributes('href'));
+
+ expect(firstThreeTopics).toEqual(firstThreeBadgesText);
+ expect(firstThreeBadgesHref).toEqual(
+ firstThreeTopics.map((topic) => `/explore/projects/topics/${encodeURIComponent(topic)}`),
+ );
+ });
+
+ it('renders the rest of the topics in a popover', () => {
+ createComponent();
+
+ const topics = project.topics.slice(3);
+ const badges = findPopover().findAllComponents(GlBadge).wrappers;
+ const badgesText = badges.map((badge) => badge.text());
+ const badgesHref = badges.map((badge) => badge.attributes('href'));
+
+ expect(topics).toEqual(badgesText);
+ expect(badgesHref).toEqual(
+ topics.map((topic) => `/explore/projects/topics/${encodeURIComponent(topic)}`),
+ );
+ });
+
+ it('renders button to open popover', () => {
+ createComponent();
+
+ const expectedButtonId = 'project-topics-popover-1';
+
+ expect(wrapper.findByText('+ 2 more').attributes('id')).toBe(expectedButtonId);
+ expect(findPopover().props('target')).toBe(expectedButtonId);
+ });
+
+ describe('when topic has a name longer than 15 characters', () => {
+ it('truncates name and shows tooltip with full name', () => {
+ const topicWithLongName = 'topic with very very very long name';
+
+ createComponent({
+ propsData: {
+ project: {
+ ...project,
+ topics: [topicWithLongName, ...project.topics],
+ },
+ },
+ });
+
+ const firstTopicBadge = findProjectTopics().findComponent(GlBadge);
+ const tooltip = getBinding(firstTopicBadge.element, 'gl-tooltip');
+
+ expect(firstTopicBadge.text()).toBe('topic with ver…');
+ expect(tooltip.value).toBe(topicWithLongName);
+ });
+ });
+ });
+
+ describe('when project has a description', () => {
+ it('renders description', () => {
+ const descriptionHtml = '<p>Foo bar</p>';
+
+ createComponent({
+ propsData: {
+ project: {
+ ...project,
+ descriptionHtml,
+ },
+ },
+ });
+
+ expect(findProjectDescription().element.innerHTML).toBe(descriptionHtml);
+ });
+ });
+
+ describe('when project does not have a description', () => {
+ it('does not render description', () => {
+ createComponent();
+
+ expect(findProjectDescription().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
new file mode 100644
index 00000000000..9380e19c39e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
@@ -0,0 +1,34 @@
+import projects from 'test_fixtures/api/users/projects/get.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import ProjectsListItem from '~/vue_shared/components/projects_list/projects_list_item.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+describe('ProjectsList', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ projects: convertObjectPropsToCamelCase(projects, { deep: true }),
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProjectsList, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ it('renders list with `ProjectListItem` component', () => {
+ createComponent();
+
+ const projectsListItemWrappers = wrapper.findAllComponents(ProjectsListItem).wrappers;
+ const expectedProps = projectsListItemWrappers.map((projectsListItemWrapper) =>
+ projectsListItemWrapper.props(),
+ );
+
+ expect(expectedProps).toEqual(
+ defaultPropsData.projects.map((project) => ({
+ project,
+ })),
+ );
+ });
+});
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 e8d76991b90..eadcb6ceeb7 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
@@ -46,31 +46,15 @@ exports[`Package code instruction single line to match the default snapshot 1`]
class="input-group-append"
data-testid="instruction-button"
>
- <button
- aria-label="Copy npm install command"
- aria-live="polite"
- class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
- data-clipboard-handle-tooltip="false"
- data-clipboard-text="npm i @my-package"
- id="clipboard-button-1"
+ <clipboard-button-stub
+ category="secondary"
+ class="input-group-text"
+ size="medium"
+ text="npm i @my-package"
title="Copy npm install command"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="copy-to-clipboard-icon"
- role="img"
- >
- <use
- href="#copy-to-clipboard"
- />
- </svg>
-
- <!---->
- </button>
+ tooltipplacement="top"
+ variant="default"
+ />
</span>
</div>
</div>
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 66cf2354bc7..5c487754b87 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
@@ -8,7 +8,7 @@ exports[`History Item renders the correct markup 1`] = `
class="timeline-entry-inner"
>
<div
- class="timeline-icon"
+ 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"
>
<gl-icon-stub
name="pencil"
diff --git a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
index 8f19f0ea14d..299535775e0 100644
--- a/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
+++ b/spec/frontend/vue_shared/components/registry/code_instruction_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
@@ -14,7 +14,7 @@ describe('Package code instruction', () => {
};
function createComponent(props = {}) {
- wrapper = mount(CodeInstruction, {
+ wrapper = shallowMount(CodeInstruction, {
propsData: {
...defaultProps,
...props,
@@ -26,10 +26,6 @@ describe('Package code instruction', () => {
const findInputElement = () => wrapper.find('[data-testid="instruction-input"]');
const findMultilineInstruction = () => wrapper.find('[data-testid="multiline-instruction"]');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('single line', () => {
beforeEach(() =>
createComponent({
diff --git a/spec/frontend/vue_shared/components/registry/details_row_spec.js b/spec/frontend/vue_shared/components/registry/details_row_spec.js
index ebc9816f983..9ef1ce5647d 100644
--- a/spec/frontend/vue_shared/components/registry/details_row_spec.js
+++ b/spec/frontend/vue_shared/components/registry/details_row_spec.js
@@ -20,11 +20,6 @@ describe('DetailsRow', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('has a default slot', () => {
mountComponent();
expect(findDefaultSlot().exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/registry/history_item_spec.js b/spec/frontend/vue_shared/components/registry/history_item_spec.js
index 947520567e6..17abe06dbee 100644
--- a/spec/frontend/vue_shared/components/registry/history_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/history_item_spec.js
@@ -22,11 +22,6 @@ describe('History Item', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem);
const findGlIcon = () => wrapper.findComponent(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
index b941eb77c32..298fa163d59 100644
--- a/spec/frontend/vue_shared/components/registry/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -30,11 +30,6 @@ describe('list item', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each`
slotName | finderFunction
${'left-primary'} | ${findLeftPrimarySlot}
diff --git a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
index a04e1e237d4..278b09d80b2 100644
--- a/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/metadata_item_spec.js
@@ -14,16 +14,11 @@ describe('Metadata Item', () => {
wrapper = shallowMount(component, {
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const findIcon = () => wrapper.findComponent(GlIcon);
const findLink = (w = wrapper) => w.findComponent(GlLink);
const findText = () => wrapper.find('[data-testid="metadata-item-text"]');
diff --git a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
index 616fefe847e..b93fa37546f 100644
--- a/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
+++ b/spec/frontend/vue_shared/components/registry/persisted_dropdown_selection_spec.js
@@ -31,10 +31,6 @@ describe('Persisted dropdown selection', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('local storage sync', () => {
it('uses the local storage sync component with the correct props', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/registry/registry_search_spec.js b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
index 591447a37c2..59bb0646350 100644
--- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js
+++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js
@@ -36,11 +36,6 @@ describe('Registry Search', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('searching', () => {
it('has a filtered-search component', () => {
mountComponent();
diff --git a/spec/frontend/vue_shared/components/registry/title_area_spec.js b/spec/frontend/vue_shared/components/registry/title_area_spec.js
index efb57ddd310..ec1451de470 100644
--- a/spec/frontend/vue_shared/components/registry/title_area_spec.js
+++ b/spec/frontend/vue_shared/components/registry/title_area_spec.js
@@ -36,11 +36,6 @@ describe('title area', () => {
return acc;
}, {});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('title', () => {
it('if slot is not present defaults to prop', () => {
mountComponent();
diff --git a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
deleted file mode 100644
index cdfe311acd9..00000000000
--- a/spec/frontend/vue_shared/components/resizable_chart/__snapshots__/resizable_chart_container_spec.js.snap
+++ /dev/null
@@ -1,23 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Resizable Chart Container renders the component 1`] = `
-<div>
- <template>
- <div
- class="slot"
- >
- <span
- class="width"
- >
- 0
- </span>
-
- <span
- class="height"
- >
- 0
- </span>
- </div>
- </template>
-</div>
-`;
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 623f7d083c5..65427374e1b 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
@@ -15,8 +15,8 @@ exports[`Resizable Skeleton Loader default setup renders the bars, labels, and g
</title>
<rect
clip-path="url(#null-idClip)"
+ fill="url(#null-idGradient)"
height="130"
- style="fill: url(#null-idGradient);"
width="400"
x="0"
y="0"
@@ -234,8 +234,8 @@ exports[`Resizable Skeleton Loader with custom settings renders the correct posi
</title>
<rect
clip-path="url(#-idClip)"
+ fill="url(#-idGradient)"
height="130"
- style="fill: url(#-idGradient);"
width="400"
x="0"
y="0"
diff --git a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
deleted file mode 100644
index 7536df24ac6..00000000000
--- a/spec/frontend/vue_shared/components/resizable_chart/resizable_chart_container_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { mount } from '@vue/test-utils';
-import $ from 'jquery';
-import { nextTick } from 'vue';
-import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
-
-jest.mock('~/lib/utils/common_utils', () => ({
- debounceByAnimationFrame(callback) {
- return jest.spyOn({ callback }, 'callback');
- },
-}));
-
-describe('Resizable Chart Container', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = mount(ResizableChartContainer, {
- scopedSlots: {
- default: `
- <template #default="{ width, height }">
- <div class="slot">
- <span class="width">{{width}}</span>
- <span class="height">{{height}}</span>
- </div>
- </template>
- `,
- },
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders the component', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('updates the slot width and height props', async () => {
- const width = 1920;
- const height = 1080;
-
- // JSDOM mocks and sets clientWidth/clientHeight to 0 so we set manually
- wrapper.vm.$refs.chartWrapper = { clientWidth: width, clientHeight: height };
-
- $(document).trigger('content.resize');
-
- await nextTick();
- const widthNode = wrapper.find('.slot > .width');
- const heightNode = wrapper.find('.slot > .height');
-
- expect(parseInt(widthNode.text(), 10)).toEqual(width);
- expect(parseInt(heightNode.text(), 10)).toEqual(height);
- });
-
- it('calls onResize on manual resize', () => {
- $(document).trigger('content.resize');
- expect(wrapper.vm.debouncedResize).toHaveBeenCalled();
- });
-
- it('calls onResize on page resize', () => {
- window.dispatchEvent(new Event('resize'));
- expect(wrapper.vm.debouncedResize).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
index bfc3aeb0303..043552baf0c 100644
--- a/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
+++ b/spec/frontend/vue_shared/components/resizable_chart/skeleton_loader_spec.js
@@ -19,12 +19,6 @@ describe('Resizable Skeleton Loader', () => {
expect(labelItems.length).toBe(8);
};
- afterEach(() => {
- if (wrapper?.destroy) {
- wrapper.destroy();
- }
- });
-
describe('default setup', () => {
beforeEach(() => {
createComponent({ uniqueKey: null });
diff --git a/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js
index 5d96fe27676..11ee2e56c14 100644
--- a/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js
+++ b/spec/frontend/vue_shared/components/rich_timestamp_tooltip_spec.js
@@ -27,10 +27,6 @@ describe('RichTimestampTooltip', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders the tooltip text header', () => {
expect(wrapper.findByTestId('header-text').text()).toBe('Created just now');
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_docker_instructions_spec.js.snap b/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_docker_instructions_spec.js.snap
deleted file mode 100644
index d14f66df8a1..00000000000
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_docker_instructions_spec.js.snap
+++ /dev/null
@@ -1,3 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`RunnerDockerInstructions renders contents 1`] = `"To install Runner in a container follow the instructions described in the GitLab documentation View installation instructions Close"`;
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_kubernetes_instructions_spec.js.snap b/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_kubernetes_instructions_spec.js.snap
deleted file mode 100644
index 1172bf07dff..00000000000
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_kubernetes_instructions_spec.js.snap
+++ /dev/null
@@ -1,3 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`RunnerKubernetesInstructions renders contents 1`] = `"To install Runner in Kubernetes follow the instructions described in the GitLab documentation. View installation instructions Close"`;
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
index f9d700fe67f..c6cd963fc33 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
@@ -59,17 +59,13 @@ describe('RunnerCliInstructions component', () => {
runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockInstructions);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the instructions are shown', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
- it('should not show alert', async () => {
+ it('should not show alert', () => {
expect(findAlert().exists()).toBe(false);
});
@@ -89,13 +85,13 @@ describe('RunnerCliInstructions component', () => {
});
});
- it('binary instructions are shown', async () => {
+ it('binary instructions are shown', () => {
const instructions = findBinaryInstructions().text();
expect(instructions).toBe(installInstructions.trim());
});
- it('register command is shown with a replaced token', async () => {
+ it('register command is shown with a replaced token', () => {
const command = findRegisterCommand().text();
expect(command).toBe(
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
index 2922d261b24..94823bb640b 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
@@ -17,7 +17,11 @@ describe('RunnerDockerInstructions', () => {
});
it('renders contents', () => {
- expect(wrapper.text().replace(/\s+/g, ' ')).toMatchSnapshot();
+ expect(wrapper.text()).toContain(
+ 'To install Runner in a container follow the instructions described in the GitLab documentation',
+ );
+ expect(wrapper.text()).toContain('View installation instructions');
+ expect(wrapper.text()).toContain('Close');
});
it('renders link', () => {
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
index 0bfcc0e3d86..9d6658e002c 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
@@ -17,7 +17,11 @@ describe('RunnerKubernetesInstructions', () => {
});
it('renders contents', () => {
- expect(wrapper.text().replace(/\s+/g, ' ')).toMatchSnapshot();
+ expect(wrapper.text()).toContain(
+ 'To install Runner in Kubernetes follow the instructions described in the GitLab documentation.',
+ );
+ expect(wrapper.text()).toContain('View installation instructions');
+ expect(wrapper.text()).toContain('Close');
});
it('renders link', () => {
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 8f593b6aa1b..2eaf46e6209 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -1,10 +1,10 @@
import { GlAlert, GlModal, GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { shallowMount } from '@vue/test-utils';
+import { ErrorWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
@@ -15,6 +15,8 @@ import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/i
import { mockRunnerPlatforms } from './mock_data';
+const mockPlatformList = mockRunnerPlatforms.data.runnerPlatforms.nodes;
+
Vue.use(VueApollo);
let resizeCallback;
@@ -41,28 +43,36 @@ describe('RunnerInstructionsModal component', () => {
let runnerPlatformsHandler;
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findAlert = () => wrapper.findComponent(GlAlert);
+ const findAlert = (variant = 'danger') => {
+ const { wrappers } = wrapper
+ .findAllComponents(GlAlert)
+ .filter((w) => w.props('variant') === variant);
+ return wrappers[0] || new ErrorWrapper();
+ };
const findModal = () => wrapper.findComponent(GlModal);
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
const findRunnerCliInstructions = () => wrapper.findComponent(RunnerCliInstructions);
- const createComponent = ({ props, shown = true, ...options } = {}) => {
+ const createComponent = ({
+ props,
+ shown = true,
+ mountFn = shallowMountExtended,
+ ...options
+ } = {}) => {
const requestHandlers = [[getRunnerPlatformsQuery, runnerPlatformsHandler]];
fakeApollo = createMockApollo(requestHandlers);
- wrapper = extendedWrapper(
- shallowMount(RunnerInstructionsModal, {
- propsData: {
- modalId: 'runner-instructions-modal',
- registrationToken: 'MY_TOKEN',
- ...props,
- },
- apolloProvider: fakeApollo,
- ...options,
- }),
- );
+ wrapper = mountFn(RunnerInstructionsModal, {
+ propsData: {
+ modalId: 'runner-instructions-modal',
+ registrationToken: 'MY_TOKEN',
+ ...props,
+ },
+ apolloProvider: fakeApollo,
+ ...options,
+ });
// trigger open modal
if (shown) {
@@ -74,34 +84,47 @@ describe('RunnerInstructionsModal component', () => {
runnerPlatformsHandler = jest.fn().mockResolvedValue(mockRunnerPlatforms);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when the modal is shown', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
- it('should not show alert', async () => {
+ it('should not show alert', () => {
expect(findAlert().exists()).toBe(false);
});
+ it('should not show deprecation alert', () => {
+ expect(findAlert('warning').exists()).toBe(false);
+ });
+
it('should contain a number of platforms buttons', () => {
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
const buttons = findPlatformButtons();
- expect(buttons).toHaveLength(mockRunnerPlatforms.data.runnerPlatforms.nodes.length);
+ expect(buttons).toHaveLength(mockPlatformList.length);
});
it('should display architecture options', () => {
const { architectures } = findRunnerCliInstructions().props('platform');
- expect(architectures).toEqual(
- mockRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes,
- );
+ expect(architectures).toEqual(mockPlatformList[0].architectures.nodes);
+ });
+
+ describe.each`
+ glFeatures | deprecationAlertExists
+ ${{}} | ${false}
+ ${{ createRunnerWorkflowForAdmin: true }} | ${true}
+ ${{ createRunnerWorkflowForNamespace: true }} | ${true}
+ `('with features $glFeatures', ({ glFeatures, deprecationAlertExists }) => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures } });
+ });
+
+ it(`alert is ${deprecationAlertExists ? 'shown' : 'not shown'}`, () => {
+ expect(findAlert('warning').exists()).toBe(deprecationAlertExists);
+ });
});
describe('when the modal resizes', () => {
@@ -119,6 +142,14 @@ describe('RunnerInstructionsModal component', () => {
expect(findPlatformButtonGroup().props('vertical')).toBeUndefined();
});
});
+
+ it('should focus platform button', async () => {
+ createComponent({ shown: true, mountFn: mountExtended, attachTo: document.body });
+ wrapper.vm.show();
+ await waitForPromises();
+
+ expect(document.activeElement.textContent.trim()).toBe(mockPlatformList[0].humanReadableName);
+ });
});
describe.each([null, 'DEFINED'])('when registration token is %p', (token) => {
@@ -206,7 +237,7 @@ describe('RunnerInstructionsModal component', () => {
expect(findAlert().exists()).toBe(true);
});
- it('should show alert when instructions cannot be loaded', async () => {
+ it('should show an alert when instructions cannot be loaded', async () => {
createComponent();
await waitForPromises();
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
index 986d76d2b95..260eddbb37d 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
@@ -12,7 +12,7 @@ describe('RunnerInstructions component', () => {
const createComponent = () => {
wrapper = shallowMountExtended(RunnerInstructions, {
directives: {
- GlModal: createMockDirective(),
+ GlModal: createMockDirective('gl-tooltip'),
},
});
};
@@ -21,10 +21,6 @@ describe('RunnerInstructions component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should show the "Show runner installation instructions" button', () => {
expect(findModalButton().text()).toBe('Show runner installation instructions');
});
diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
index 09b0b3d43ad..6eebd129beb 100644
--- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
@@ -6,7 +6,7 @@ import {
expectedDownloadDropdownPropsWithTitle,
securityReportMergeRequestDownloadPathsQueryResponse,
} from 'jest/vue_shared/security_reports/mock_data';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import {
@@ -15,7 +15,7 @@ import {
} from '~/vue_shared/security_reports/constants';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('Merge request artifact Download', () => {
let wrapper;
@@ -52,10 +52,6 @@ describe('Merge request artifact Download', () => {
const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('given the query is loading', () => {
beforeEach(() => {
createWrapper({
diff --git a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
index 08d3d5b19d4..2f6e633fb34 100644
--- a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
@@ -21,11 +21,6 @@ describe('HelpIcon component', () => {
const findPopover = () => wrapper.findComponent(GlPopover);
const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' });
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('given a help path only', () => {
beforeEach(() => {
createWrapper();
diff --git a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
index f186eb848f2..61cdc329220 100644
--- a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
+++ b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
@@ -15,11 +15,6 @@ describe('SecuritySummary component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe.each([
{ message: '' },
{ message: 'foo' },
diff --git a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
index 88445b6684c..c1feb64dacb 100644
--- a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
+++ b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
@@ -40,10 +40,6 @@ describe('~/vue_shared/components/segmented_control_button_group.vue', () => {
disabled,
}));
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('default', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
index 5e829653c13..94d634f79bd 100644
--- a/spec/frontend/vue_shared/components/settings/settings_block_spec.js
+++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
@@ -16,10 +16,6 @@ describe('Settings Block', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findDefaultSlot = () => wrapper.findByTestId('default-slot');
const findTitleSlot = () => wrapper.findByTestId('title-slot');
const findDescriptionSlot = () => wrapper.findByTestId('description-slot');
diff --git a/spec/frontend/vue_shared/components/slot_switch_spec.js b/spec/frontend/vue_shared/components/slot_switch_spec.js
index f25b9877aba..3a2147c6c89 100644
--- a/spec/frontend/vue_shared/components/slot_switch_spec.js
+++ b/spec/frontend/vue_shared/components/slot_switch_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { assertProps } from 'helpers/assert_props';
import SlotSwitch from '~/vue_shared/components/slot_switch.vue';
@@ -19,14 +20,10 @@ describe('SlotSwitch', () => {
const getChildrenHtml = () => wrapper.findAll('* *').wrappers.map((c) => c.html());
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- });
-
it('throws an error if activeSlotNames is missing', () => {
- expect(createComponent).toThrow('[Vue warn]: Missing required prop: "activeSlotNames"');
+ expect(() => assertProps(SlotSwitch, {})).toThrow(
+ '[Vue warn]: Missing required prop: "activeSlotNames"',
+ );
});
it('renders no slots if activeSlotNames is empty', () => {
diff --git a/spec/frontend/vue_shared/components/smart_virtual_list_spec.js b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
index 8802a832781..e5d988f75f5 100644
--- a/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
+++ b/spec/frontend/vue_shared/components/smart_virtual_list_spec.js
@@ -1,5 +1,4 @@
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
describe('Toggle Button', () => {
@@ -16,7 +15,7 @@ describe('Toggle Button', () => {
remain,
};
- const Component = Vue.extend({
+ const Component = {
components: {
SmartVirtualScrollList,
},
@@ -26,7 +25,7 @@ describe('Toggle Button', () => {
<smart-virtual-scroll-list v-bind="$options.smartListProperties">
<li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li>
</smart-virtual-scroll-list>`,
- });
+ };
return mount(Component).vm;
};
diff --git a/spec/frontend/vue_shared/components/source_editor_spec.js b/spec/frontend/vue_shared/components/source_editor_spec.js
index ca5b990bc29..5b155207029 100644
--- a/spec/frontend/vue_shared/components/source_editor_spec.js
+++ b/spec/frontend/vue_shared/components/source_editor_spec.js
@@ -47,10 +47,6 @@ describe('Source Editor component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const triggerChangeContent = (val) => {
mockInstance.getValue.mockReturnValue(val);
const [cb] = mockInstance.onDidChangeModelContent.mock.calls[0];
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
index da9067a8ddc..395ba92d4c6 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
@@ -45,8 +45,6 @@ describe('Chunk component', () => {
createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('Intersection observer', () => {
it('renders an Intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
index f661bd6747a..9a38a96663d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_line_spec.js
@@ -10,16 +10,10 @@ const DEFAULT_PROPS = {
describe('Chunk Line component', () => {
let wrapper;
- const fileLineBlame = true;
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(ChunkLine, {
propsData: { ...DEFAULT_PROPS, ...props },
- provide: {
- glFeatures: {
- fileLineBlame,
- },
- },
});
};
@@ -31,8 +25,6 @@ describe('Chunk Line component', () => {
createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('rendering', () => {
it('renders a blame link', () => {
expect(findBlameLink().attributes()).toMatchObject({
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index 95ef11d776a..ff50326917f 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -11,7 +11,6 @@ describe('Chunk component', () => {
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
propsData: { ...CHUNK_1, ...props },
- provide: { glFeatures: { fileLineBlame: true } },
});
};
@@ -24,8 +23,6 @@ describe('Chunk component', () => {
createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('Intersection observer', () => {
it('renders an Intersection observer component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
index 0beec8e9d3e..8419a0c5ddf 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
@@ -11,6 +11,7 @@ import {
EVENT_LABEL_FALLBACK,
ROUGE_TO_HLJS_LANGUAGE_MAP,
LINES_PER_CHUNK,
+ LEGACY_FALLBACKS,
} from '~/vue_shared/components/source_viewer/constants';
import waitForPromises from 'helpers/wait_for_promises';
import LineHighlighter from '~/blob/line_highlighter';
@@ -68,8 +69,6 @@ describe('Source Viewer component', () => {
return createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
const eventData = { label: EVENT_LABEL_VIEWER, property: language };
@@ -91,14 +90,16 @@ describe('Source Viewer component', () => {
});
describe('legacy fallbacks', () => {
- it('tracks a fallback event and emits an error when viewing python files', () => {
- const fallbackLanguage = 'python';
- const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
- createComponent({ language: fallbackLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
+ it.each(LEGACY_FALLBACKS)(
+ 'tracks a fallback event and emits an error when viewing %s files',
+ (fallbackLanguage) => {
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
+ createComponent({ language: fallbackLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ },
+ );
});
describe('highlight.js', () => {
@@ -170,7 +171,7 @@ describe('Source Viewer component', () => {
});
describe('LineHighlighter', () => {
- it('instantiates the lineHighlighter class', async () => {
+ it('instantiates the lineHighlighter class', () => {
expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
});
});
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 1c75442b4a8..46b582c3668 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
@@ -25,8 +25,6 @@ describe('Source Viewer component', () => {
return createComponent();
});
- afterEach(() => wrapper.destroy());
-
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
index 6b869db4058..ffa25ae8448 100644
--- a/spec/frontend/vue_shared/components/split_button_spec.js
+++ b/spec/frontend/vue_shared/components/split_button_spec.js
@@ -2,6 +2,7 @@ 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 = [
@@ -42,12 +43,12 @@ describe('SplitButton', () => {
it('fails for empty actionItems', () => {
const actionItems = [];
- expect(() => createComponent({ actionItems })).toThrow();
+ expect(() => assertProps(SplitButton, { actionItems })).toThrow();
});
it('fails for single actionItems', () => {
const actionItems = [mockActionItems[0]];
- expect(() => createComponent({ actionItems })).toThrow();
+ expect(() => assertProps(SplitButton, { actionItems })).toThrow();
});
it('renders actionItems', () => {
diff --git a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
index 79b1f17afa0..13911d487f2 100644
--- a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
+++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
@@ -18,10 +18,6 @@ describe('StackedProgressBarComponent', () => {
wrapper = mount(StackedProgressBarComponent, { propsData: defaultConfig });
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSuccessBar = () => wrapper.find('.status-green');
const findNeutralBar = () => wrapper.find('.status-neutral');
const findFailureBar = () => wrapper.find('.status-red');
diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index 99de26ce2ae..79aba1b2516 100644
--- a/spec/frontend/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -16,10 +16,6 @@ describe('Pagination component', () => {
spy = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('render', () => {
it('should not render anything', () => {
mountComponent({
diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
index 28c5acc8110..17a363ad8b1 100644
--- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import timezoneMock from 'timezone-mock';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import { DATE_ONLY_FORMAT } from '~/lib/utils/datetime/constants';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('Time ago with tooltip component', () => {
@@ -25,7 +26,6 @@ describe('Time ago with tooltip component', () => {
};
afterEach(() => {
- vm.destroy();
timezoneMock.unregister();
});
@@ -50,12 +50,36 @@ describe('Time ago with tooltip component', () => {
expect(vm.attributes('datetime')).toEqual(timestamp);
});
+ it('should render with the timestamp provided as Date', () => {
+ buildVm({ time: new Date(timestamp) });
+
+ expect(vm.text()).toEqual(timeAgoTimestamp);
+ });
+
it('should render provided scope content with the correct timeAgo string', () => {
buildVm(null, { default: `<span>The time is {{ props.timeAgo }}</span>` });
expect(vm.text()).toEqual(`The time is ${timeAgoTimestamp}`);
});
+ describe('with User Setting timeDisplayRelative: false', () => {
+ beforeEach(() => {
+ window.gon = { time_display_relative: false };
+ });
+
+ it('should render with the correct absolute datetime in the default format', () => {
+ buildVm();
+
+ expect(vm.text()).toEqual('May 8, 2017, 2:57 PM');
+ });
+
+ it('should render with the correct absolute datetime in the requested dateTimeFormat', () => {
+ buildVm({ dateTimeFormat: DATE_ONLY_FORMAT });
+
+ expect(vm.text()).toEqual('May 8, 2017');
+ });
+ });
+
describe('number based timestamps', () => {
// Store a date object before we mock the TZ
const date = new Date();
diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
index c8351ed61d7..d8dedd8240b 100644
--- a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlDropdown } from '@gitlab/ui';
+import { GlDropdownItem, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
@@ -9,7 +9,9 @@ describe('Deploy freeze timezone dropdown', () => {
let wrapper;
let store;
- const createComponent = (searchTerm, selectedTimezone) => {
+ const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+
+ const createComponent = async (searchTerm, selectedTimezone) => {
wrapper = shallowMountExtended(TimezoneDropdown, {
store,
propsData: {
@@ -19,9 +21,8 @@ describe('Deploy freeze timezone dropdown', () => {
},
});
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ searchTerm });
+ findSearchBox().vm.$emit('input', searchTerm);
+ await nextTick();
};
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
@@ -29,14 +30,9 @@ describe('Deploy freeze timezone dropdown', () => {
const findEmptyResultsItem = () => wrapper.findByTestId('noMatchingResults');
const findHiddenInput = () => wrapper.find('input');
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('No time zones found', () => {
- beforeEach(() => {
- createComponent('UTC timezone');
+ beforeEach(async () => {
+ await createComponent('UTC timezone');
});
it('renders empty results message', () => {
@@ -45,8 +41,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent('');
+ beforeEach(async () => {
+ await createComponent('');
});
it('renders all timezones when search term is empty', () => {
@@ -55,8 +51,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
describe('Time zones found', () => {
- beforeEach(() => {
- createComponent('Alaska');
+ beforeEach(async () => {
+ await createComponent('Alaska');
});
it('renders only the time zone searched for', () => {
@@ -87,8 +83,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
describe('Selected time zone not found', () => {
- beforeEach(() => {
- createComponent('', 'Berlin');
+ beforeEach(async () => {
+ await createComponent('', 'Berlin');
});
it('renders empty selections', () => {
@@ -101,8 +97,8 @@ describe('Deploy freeze timezone dropdown', () => {
});
describe('Selected time zone found', () => {
- beforeEach(() => {
- createComponent('', 'Europe/Berlin');
+ beforeEach(async () => {
+ await createComponent('', 'Europe/Berlin');
});
it('renders selected time zone as dropdown label', () => {
diff --git a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
index ca1f7996ad6..f5da498a205 100644
--- a/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
+++ b/spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
@@ -30,8 +30,8 @@ describe('TooltipOnTruncate component', () => {
default: [MOCK_TITLE],
},
directives: {
- GlTooltip: createMockDirective(),
- GlResizeObserver: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
...options,
});
@@ -42,8 +42,8 @@ describe('TooltipOnTruncate component', () => {
...TooltipOnTruncate,
directives: {
...TooltipOnTruncate.directives,
- GlTooltip: createMockDirective(),
- GlResizeObserver: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
};
@@ -78,19 +78,15 @@ describe('TooltipOnTruncate component', () => {
await nextTick();
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when truncated', () => {
- beforeEach(async () => {
+ beforeEach(() => {
hasHorizontalOverflow.mockReturnValueOnce(true);
createComponent();
});
- it('renders tooltip', async () => {
+ it('renders tooltip', () => {
expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element);
- expect(getTooltipValue()).toMatchObject({
+ expect(getTooltipValue()).toStrictEqual({
title: MOCK_TITLE,
placement: 'top',
disabled: false,
@@ -100,7 +96,7 @@ describe('TooltipOnTruncate component', () => {
});
describe('with default target', () => {
- beforeEach(async () => {
+ beforeEach(() => {
hasHorizontalOverflow.mockReturnValueOnce(false);
createComponent();
});
@@ -144,7 +140,7 @@ describe('TooltipOnTruncate component', () => {
await nextTick();
- expect(getTooltipValue()).toMatchObject({
+ expect(getTooltipValue()).toStrictEqual({
title: MOCK_TITLE,
placement: 'top',
disabled: false,
@@ -194,20 +190,22 @@ describe('TooltipOnTruncate component', () => {
});
});
- describe('placement', () => {
- it('sets placement when tooltip is rendered', () => {
- const mockPlacement = 'bottom';
-
+ describe('tooltip customization', () => {
+ it.each`
+ property | mockValue
+ ${'placement'} | ${'bottom'}
+ ${'boundary'} | ${'viewport'}
+ `('sets $property when the tooltip is rendered', ({ property, mockValue }) => {
hasHorizontalOverflow.mockReturnValueOnce(true);
createComponent({
propsData: {
- placement: mockPlacement,
+ [property]: mockValue,
},
});
expect(hasHorizontalOverflow).toHaveBeenLastCalledWith(wrapper.element);
expect(getTooltipValue()).toMatchObject({
- placement: mockPlacement,
+ [property]: mockValue,
});
});
});
diff --git a/spec/frontend/vue_shared/components/truncated_text/truncated_text_spec.js b/spec/frontend/vue_shared/components/truncated_text/truncated_text_spec.js
new file mode 100644
index 00000000000..76467c185db
--- /dev/null
+++ b/spec/frontend/vue_shared/components/truncated_text/truncated_text_spec.js
@@ -0,0 +1,113 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { __ } from '~/locale';
+import TruncatedText from '~/vue_shared/components/truncated_text/truncated_text.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+describe('TruncatedText', () => {
+ let wrapper;
+
+ const findContent = () => wrapper.findComponent({ ref: 'content' }).element;
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(TruncatedText, {
+ propsData,
+ directives: {
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
+ },
+ stubs: {
+ GlButton,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when mounted', () => {
+ it('the content has class `gl-truncate-text-by-line`', () => {
+ expect(findContent().classList).toContain('gl-truncate-text-by-line');
+ });
+
+ it('the content has style variables for `lines` and `mobile-lines` with the correct values', () => {
+ const { style } = findContent();
+
+ expect(style).toContain('--lines');
+ expect(style.getPropertyValue('--lines')).toBe('3');
+ expect(style).toContain('--mobile-lines');
+ expect(style.getPropertyValue('--mobile-lines')).toBe('10');
+ });
+
+ it('the button is not visible', () => {
+ expect(findButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when mounted with a value for the lines property', () => {
+ const lines = 4;
+
+ beforeEach(() => {
+ createComponent({ lines });
+ });
+
+ it('the lines variable has the value of the passed property', () => {
+ expect(findContent().style.getPropertyValue('--lines')).toBe(lines.toString());
+ });
+ });
+
+ describe('when mounted with a value for the mobileLines property', () => {
+ const mobileLines = 4;
+
+ beforeEach(() => {
+ createComponent({ mobileLines });
+ });
+
+ it('the lines variable has the value of the passed property', () => {
+ expect(findContent().style.getPropertyValue('--mobile-lines')).toBe(mobileLines.toString());
+ });
+ });
+
+ describe('when resizing and the scroll height is smaller than the offset height', () => {
+ beforeEach(() => {
+ getBinding(findContent(), 'gl-resize-observer').value({
+ target: { scrollHeight: 10, offsetHeight: 20 },
+ });
+ });
+
+ it('the button remains invisible', () => {
+ expect(findButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when resizing and the scroll height is greater than the offset height', () => {
+ beforeEach(() => {
+ getBinding(findContent(), 'gl-resize-observer').value({
+ target: { scrollHeight: 20, offsetHeight: 10 },
+ });
+ });
+
+ it('the button becomes visible', () => {
+ expect(findButton().exists()).toBe(true);
+ });
+
+ it('the button text says "show more"', () => {
+ expect(findButton().text()).toBe(__('Show more'));
+ });
+
+ describe('clicking the button', () => {
+ beforeEach(() => {
+ findButton().trigger('click');
+ });
+
+ it('removes the `gl-truncate-text-by-line` class on the content', () => {
+ expect(findContent().classList).not.toContain('gl-truncate-text-by-line');
+ });
+
+ it('toggles the button text to "Show less"', () => {
+ expect(findButton().text()).toBe(__('Show less'));
+ });
+ });
+ });
+});
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 f9d615d4f68..c816fe790a8 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
@@ -5,7 +5,7 @@ exports[`Upload dropzone component correctly overrides description and drop mess
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
+ 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"
type="button"
>
<div
@@ -86,7 +86,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
+ 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"
type="button"
>
<div
@@ -171,7 +171,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
+ 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"
type="button"
>
<div
@@ -256,7 +256,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
+ 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"
type="button"
>
<div
@@ -342,7 +342,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
+ 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"
type="button"
>
<div
@@ -428,7 +428,7 @@ exports[`Upload dropzone component when dragging renders correct template when d
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
+ 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"
type="button"
>
<div
@@ -514,7 +514,7 @@ exports[`Upload dropzone component when no slot provided renders default dropzon
class="gl-w-full gl-relative"
>
<button
- class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0"
+ 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"
type="button"
>
<div
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
index a063a5591e3..24f96195e05 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
+++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
@@ -3,8 +3,6 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
-jest.mock('~/flash');
-
describe('Upload dropzone component', () => {
let wrapper;
@@ -34,11 +32,6 @@ describe('Upload dropzone component', () => {
});
}
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('when slot provided', () => {
it('renders dropzone with slot content', () => {
createComponent({
diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js
index 30a7439579f..2718be74111 100644
--- a/spec/frontend/vue_shared/components/url_sync_spec.js
+++ b/spec/frontend/vue_shared/components/url_sync_spec.js
@@ -33,10 +33,6 @@ describe('url sync component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const expectUrlSyncWithMergeUrlParams = (
query,
times,
diff --git a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
index 662c09d02bf..ba55df5512f 100644
--- a/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
+++ b/spec/frontend/vue_shared/components/usage_quotas/usage_banner_spec.js
@@ -24,10 +24,6 @@ describe('usage banner', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
slotName | finderFunction
${'left-primary-text'} | ${findLeftPrimaryTextSlot}
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
index d63b13981ac..3ae3d89af27 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -20,10 +20,6 @@ describe('User Avatar Image Component', () => {
const findAvatar = () => wrapper.findComponent(GlAvatar);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('Initialization', () => {
beforeEach(() => {
wrapper = shallowMount(UserAvatarImage, {
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index df7ce449678..90f9156af38 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -34,10 +34,6 @@ describe('User Avatar Link Component', () => {
createWrapper();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('should render GlLink with correct props', () => {
const link = wrapper.findComponent(GlAvatarLink);
expect(link.exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index 63371b1492b..075cb753301 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -50,10 +50,6 @@ describe('UserAvatarList', () => {
props = { imgSize: TEST_IMAGE_SIZE };
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('empty text', () => {
it('shows when items are empty', () => {
factory({ propsData: { items: [] } });
@@ -152,6 +148,13 @@ describe('UserAvatarList', () => {
expect(links.length).toEqual(TEST_BREAKPOINT);
});
+ it('does not emit any event on mount', async () => {
+ factory();
+ await nextTick();
+
+ expect(wrapper.emitted()).toEqual({});
+ });
+
describe('with expand clicked', () => {
beforeEach(() => {
factory();
@@ -164,13 +167,25 @@ describe('UserAvatarList', () => {
expect(links.length).toEqual(props.items.length);
});
- it('with collapse clicked, it renders avatars up to breakpoint', async () => {
- clickButton();
+ it('emits the `expanded` event', () => {
+ expect(wrapper.emitted('expanded')).toHaveLength(1);
+ });
- await nextTick();
- const links = wrapper.findAllComponents(UserAvatarLink);
+ describe('with collapse clicked', () => {
+ beforeEach(() => {
+ clickButton();
+ });
+
+ it('renders avatars up to breakpoint', async () => {
+ await nextTick();
+ const links = wrapper.findAllComponents(UserAvatarLink);
+
+ expect(links.length).toEqual(TEST_BREAKPOINT);
+ });
- expect(links.length).toEqual(TEST_BREAKPOINT);
+ it('emits the `collapsed` event', () => {
+ expect(wrapper.emitted('collapsed')).toHaveLength(1);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
index 521744154ba..a4efbda06ce 100644
--- a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
+++ b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js
@@ -28,23 +28,21 @@ const initialSlotProps = (changes = {}) => ({
});
describe('UserCalloutDismisser', () => {
- let wrapper;
-
const MOCK_FEATURE_NAME = 'mock_feature_name';
// Query handlers
- const successHandlerFactory = (dismissedCallouts = []) => async () =>
- userCalloutsResponse(dismissedCallouts);
- const anonUserHandler = async () => anonUserCalloutsResponse();
+ const successHandlerFactory = (dismissedCallouts = []) => () =>
+ Promise.resolve(userCalloutsResponse(dismissedCallouts));
+ const anonUserHandler = () => Promise.resolve(anonUserCalloutsResponse());
const errorHandler = () => Promise.reject(new Error('query error'));
const pendingHandler = () => new Promise(() => {});
// Mutation handlers
- const mutationSuccessHandlerSpy = jest.fn(async (variables) =>
- userCalloutMutationResponse(variables),
+ const mutationSuccessHandlerSpy = jest.fn((variables) =>
+ Promise.resolve(userCalloutMutationResponse(variables)),
);
- const mutationErrorHandlerSpy = jest.fn(async (variables) =>
- userCalloutMutationResponse(variables, ['mutation error']),
+ const mutationErrorHandlerSpy = jest.fn((variables) =>
+ Promise.resolve(userCalloutMutationResponse(variables, ['mutation error'])),
);
const defaultScopedSlotSpy = jest.fn();
@@ -52,7 +50,7 @@ describe('UserCalloutDismisser', () => {
const callDismissSlotProp = () => defaultScopedSlotSpy.mock.calls[0][0].dismiss();
const createComponent = ({ queryHandler, mutationHandler, ...options }) => {
- wrapper = mount(
+ mount(
UserCalloutDismisser,
merge(
{
@@ -72,10 +70,6 @@ describe('UserCalloutDismisser', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when loading', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
index 78abb89e7b8..d77e357a50c 100644
--- a/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list_spec.js
@@ -51,10 +51,6 @@ describe('User deletion obstacles list', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
- });
-
const findLinks = () => wrapper.findAllComponents(GlLink);
const findTitle = () => wrapper.findByTestId('title');
const findFooter = () => wrapper.findByTestId('footer');
@@ -65,7 +61,7 @@ describe('User deletion obstacles list', () => {
${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
`('when current user', ({ isCurrentUser, titleText, footerText }) => {
- it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call management`, async () => {
+ it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call management`, () => {
createComponent({
isCurrentUser,
});
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index f6316af6ad8..41181ab9a68 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -1,5 +1,6 @@
import { GlSkeletonLoader, GlIcon } from '@gitlab/ui';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import mrDiffCommentFixture from 'test_fixtures/merge_requests/diff_comment.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
@@ -13,11 +14,11 @@ import {
I18N_ERROR_UNFOLLOW,
} from '~/vue_shared/components/user_popover/constants';
import axios from '~/lib/utils/axios_utils';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import { followUser, unfollowUser } from '~/api/user_api';
import { mockTracking } from 'helpers/tracking_helper';
-jest.mock('~/flash');
+jest.mock('~/alert');
jest.mock('~/api/user_api', () => ({
followUser: jest.fn(),
unfollowUser: jest.fn(),
@@ -41,17 +42,14 @@ const DEFAULT_PROPS = {
};
describe('User Popover Component', () => {
- const fixtureTemplate = 'merge_requests/diff_comment.html';
-
let wrapper;
beforeEach(() => {
- loadHTMLFixture(fixtureTemplate);
+ setHTMLFixture(mrDiffCommentFixture);
gon.features = {};
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
@@ -277,7 +275,7 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.findByText('(Busy)').exists()).toBe(true);
+ expect(wrapper.findByText('Busy').exists()).toBe(true);
});
it('should hide the busy status for any other status', () => {
@@ -288,7 +286,7 @@ describe('User Popover Component', () => {
createWrapper({ user });
- expect(wrapper.findByText('(Busy)').exists()).toBe(false);
+ expect(wrapper.findByText('Busy').exists()).toBe(false);
});
it('shows pronouns when user has them set', () => {
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index b0e9584a15b..e881bfed35e 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
-import { IssuableType } from '~/issues/constants';
+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';
@@ -105,7 +105,6 @@ describe('User select dropdown', () => {
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -409,7 +408,7 @@ describe('User select dropdown', () => {
describe('when on merge request sidebar', () => {
beforeEach(() => {
- createComponent({ props: { issuableType: IssuableType.MergeRequest, issuableId: 1 } });
+ createComponent({ props: { issuableType: TYPE_MERGE_REQUEST, issuableId: 1 } });
return waitForPromises();
});
diff --git a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
index c136c2054ac..e24c5a4609d 100644
--- a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
+++ b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
@@ -3,10 +3,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
-const TestComponent = Vue.extend({
+const TestComponent = {
inject: ['vuexModule'],
template: `<div data-testid="vuexModule">{{ vuexModule }}</div> `,
-});
+};
const TEST_VUEX_MODULE = 'testVuexModule';
@@ -27,15 +27,18 @@ describe('~/vue_shared/components/vuex_module_provider', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('provides "vuexModule" set from prop', () => {
createComponent();
expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE);
});
+ it('provides "vuexModel" set from "vuex-module" prop when using @vue/compat', () => {
+ createComponent({
+ propsData: { 'vuex-module': TEST_VUEX_MODULE },
+ });
+ expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE);
+ });
+
it('does not blow up when used with vue-apollo', () => {
// See https://github.com/vuejs/vue-apollo/pull/1153 for details
Vue.use(VueApollo);
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 18afe049149..d888abc19ef 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlLink, GlModal, GlPopover } from '@gitlab/ui';
+import { GlButton, GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
@@ -9,7 +9,6 @@ import WebIdeLink, {
PREFERRED_EDITOR_KEY,
} from '~/vue_shared/components/web_ide_link.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import { KEY_WEB_IDE } from '~/vue_shared/components/constants';
import { stubComponent } from 'helpers/stub_component';
@@ -95,14 +94,7 @@ describe('Web IDE link component', () => {
let wrapper;
- function createComponent(
- props,
- {
- mountFn = shallowMountExtended,
- glFeatures = {},
- userCalloutDismisserSlotProps = { dismiss: jest.fn() },
- } = {},
- ) {
+ function createComponent(props, { mountFn = shallowMountExtended, glFeatures = {} } = {}) {
wrapper = mountFn(WebIdeLink, {
propsData: {
editUrl: TEST_EDIT_URL,
@@ -124,11 +116,6 @@ describe('Web IDE link component', () => {
<slot name="modal-footer"></slot>
</div>`,
}),
- UserCalloutDismisser: stubComponent(UserCalloutDismisser, {
- render() {
- return this.$scopedSlots.default(userCalloutDismisserSlotProps);
- },
- }),
},
});
}
@@ -137,21 +124,10 @@ describe('Web IDE link component', () => {
localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, 'true');
});
- afterEach(() => {
- wrapper.destroy();
- });
-
const findActionsButton = () => wrapper.findComponent(ActionsButton);
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findModal = () => wrapper.findComponent(GlModal);
const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
- const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
- const findNewWebIdeCalloutPopover = () => wrapper.findComponent(GlPopover);
- const findTryItOutLink = () =>
- wrapper
- .findAllComponents(GlLink)
- .filter((link) => link.text().includes('Try it out'))
- .at(0);
it.each([
{
@@ -349,7 +325,7 @@ describe('Web IDE link component', () => {
it.each(testActions)(
'emits the correct event when an action handler is called',
- async ({ props, expectedEventPayload }) => {
+ ({ props, expectedEventPayload }) => {
createComponent({ ...props, needsToFork: true, disableForkModal: true });
findActionsButton().props('actions')[0].handle();
@@ -358,7 +334,7 @@ describe('Web IDE link component', () => {
},
);
- it.each(testActions)('renders the fork confirmation modal', async ({ props }) => {
+ it.each(testActions)('renders the fork confirmation modal', ({ props }) => {
createComponent({ ...props, needsToFork: true });
expect(findForkConfirmModal().exists()).toBe(true);
@@ -450,132 +426,6 @@ describe('Web IDE link component', () => {
});
});
- describe('Web IDE callout', () => {
- describe('vscode_web_ide feature flag is enabled and the edit button is not shown', () => {
- let dismiss;
-
- beforeEach(() => {
- dismiss = jest.fn();
- createComponent(
- {
- showEditButton: false,
- },
- {
- glFeatures: { vscodeWebIde: true },
- userCalloutDismisserSlotProps: { dismiss },
- },
- );
- });
- it('does not skip the user_callout_dismisser query', () => {
- expect(findUserCalloutDismisser().props()).toEqual(
- expect.objectContaining({
- skipQuery: false,
- featureName: 'vscode_web_ide_callout',
- }),
- );
- });
-
- it('mounts new web ide callout popover', () => {
- expect(findNewWebIdeCalloutPopover().props()).toEqual(
- expect.objectContaining({
- showCloseButton: '',
- target: 'web-ide-link',
- triggers: 'manual',
- boundaryPadding: 80,
- }),
- );
- });
-
- describe.each`
- calloutStatus | shouldShowCallout | popoverVisibility | tooltipVisibility
- ${'show'} | ${true} | ${true} | ${false}
- ${'hide'} | ${false} | ${false} | ${true}
- `(
- 'when should $calloutStatus web ide callout',
- ({ shouldShowCallout, popoverVisibility, tooltipVisibility }) => {
- beforeEach(() => {
- createComponent(
- {
- showEditButton: false,
- },
- {
- glFeatures: { vscodeWebIde: true },
- userCalloutDismisserSlotProps: { shouldShowCallout, dismiss },
- },
- );
- });
-
- it(`popover visibility = ${popoverVisibility}`, () => {
- expect(findNewWebIdeCalloutPopover().props().show).toBe(popoverVisibility);
- });
-
- it(`action button tooltip visibility = ${tooltipVisibility}`, () => {
- expect(findActionsButton().props().showActionTooltip).toBe(tooltipVisibility);
- });
- },
- );
-
- it('dismisses the callout when popover close button is clicked', () => {
- findNewWebIdeCalloutPopover().vm.$emit('close-button-clicked');
-
- expect(dismiss).toHaveBeenCalled();
- });
-
- it('dismisses the callout when try it now link is clicked', () => {
- findTryItOutLink().vm.$emit('click');
-
- expect(dismiss).toHaveBeenCalled();
- });
-
- it('dismisses the callout when action button is clicked', () => {
- findActionsButton().vm.$emit('actionClicked');
-
- expect(dismiss).toHaveBeenCalled();
- });
- });
-
- describe.each`
- featureFlag | showEditButton
- ${false} | ${true}
- ${true} | ${false}
- ${false} | ${false}
- `(
- 'when vscode_web_ide=$featureFlag and showEditButton = $showEditButton',
- ({ vscodeWebIde, showEditButton }) => {
- let dismiss;
-
- beforeEach(() => {
- dismiss = jest.fn();
-
- createComponent(
- {
- showEditButton,
- },
- { glFeatures: { vscodeWebIde }, userCalloutDismisserSlotProps: { dismiss } },
- );
- });
-
- it('skips the user_callout_dismisser query', () => {
- expect(findUserCalloutDismisser().props().skipQuery).toBe(true);
- });
-
- it('displays actions button tooltip', () => {
- expect(findActionsButton().props().showActionTooltip).toBe(true);
- });
-
- it('mounts new web ide callout popover', () => {
- expect(findNewWebIdeCalloutPopover().exists()).toBe(false);
- });
-
- it('does not dismiss the callout when action button is clicked', () => {
- findActionsButton().vm.$emit('actionClicked');
-
- expect(dismiss).not.toHaveBeenCalled();
- });
- },
- );
- });
-
describe('when vscode_web_ide feature flag is enabled', () => {
describe('when is not showing edit button', () => {
describe(`when ${PREFERRED_EDITOR_RESET_KEY} is unset`, () => {
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
index 4bf84b06246..fc69e884258 100644
--- a/spec/frontend/vue_shared/directives/track_event_spec.js
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -1,50 +1,47 @@
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Tracking from '~/tracking';
import TrackEvent from '~/vue_shared/directives/track_event';
jest.mock('~/tracking');
-const Component = Vue.component('DummyElement', {
- directives: {
- TrackEvent,
- },
- data() {
- return {
- trackingOptions: null,
- };
- },
- template: '<button id="trackable" v-track-event="trackingOptions"></button>',
-});
+describe('TrackEvent directive', () => {
+ let wrapper;
-let wrapper;
-let button;
+ const clickButton = () => wrapper.find('button').trigger('click');
-describe('Error Tracking directive', () => {
- beforeEach(() => {
- wrapper = shallowMount(Component);
- button = wrapper.find('#trackable');
- });
+ const createComponent = (trackingOptions) =>
+ Vue.component('DummyElement', {
+ directives: {
+ TrackEvent,
+ },
+ data() {
+ return {
+ trackingOptions,
+ };
+ },
+ template: '<button v-track-event="trackingOptions"></button>',
+ });
+
+ const mountComponent = (trackingOptions) => shallowMount(createComponent(trackingOptions));
+
+ it('does not track the event if required arguments are not provided', () => {
+ wrapper = mountComponent();
+ clickButton();
- it('should not track the event if required arguments are not provided', () => {
- button.trigger('click');
expect(Tracking.event).not.toHaveBeenCalled();
});
- it('should track event on click if tracking info provided', async () => {
- const trackingOptions = {
+ it('tracks event on click if tracking info provided', () => {
+ wrapper = mountComponent({
category: 'Tracking',
action: 'click_trackable_btn',
label: 'Trackable Info',
- };
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ trackingOptions });
- const { category, action, label, property, value } = trackingOptions;
+ });
+ clickButton();
- await nextTick();
- button.trigger('click');
- expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property, value });
+ expect(Tracking.event).toHaveBeenCalledWith('Tracking', 'click_trackable_btn', {
+ label: 'Trackable Info',
+ });
});
});
diff --git a/spec/frontend/vue_shared/directives/validation_spec.js b/spec/frontend/vue_shared/directives/validation_spec.js
index dcd3a44a6fc..72a348c1a79 100644
--- a/spec/frontend/vue_shared/directives/validation_spec.js
+++ b/spec/frontend/vue_shared/directives/validation_spec.js
@@ -80,11 +80,6 @@ describe('validation directive', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const getFormData = () => wrapper.vm.form;
const findForm = () => wrapper.find('form');
const findInput = () => wrapper.find('input');
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
index dd011b9d84e..1d4aa1afeaf 100644
--- 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
@@ -2,7 +2,7 @@
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=\\"#issue-block\\"></use>
+ <use href=\\"file-mock#issue-block\\"></use>
</svg>
<div class=\\"gl-popover\\">
<ul class=\\"gl-list-style-none gl-p-0 gl-mb-0\\">
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
index 7b0f0f7e344..e983519d9fc 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
@@ -34,10 +34,6 @@ describe('IssuableCreateRoot', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
it('renders component container element with class "issuable-create-container"', () => {
expect(wrapper.classes()).toContain('issuable-create-container');
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 ff21b3bc356..ae2fd5ebffa 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
@@ -36,10 +36,6 @@ describe('IssuableForm', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('handleUpdateSelectedLabels', () => {
it('sets provided `labels` param to prop `selectedLabels`', () => {
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
index 76b6efa15b6..1a490359040 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js
@@ -1,16 +1,12 @@
import { shallowMount } from '@vue/test-utils';
-import { GlIcon } from '@gitlab/ui';
import {
mockRegularLabel,
mockScopedLabel,
} from 'jest/sidebar/components/labels/labels_select_widget/mock_data';
import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue';
import LabelsSelect from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
-import {
- DropdownVariant,
- LabelType,
-} from '~/sidebar/components/labels/labels_select_widget/constants';
-import { WorkspaceType } from '~/issues/constants';
+import { VARIANT_EMBEDDED } from '~/sidebar/components/labels/labels_select_widget/constants';
+import { WORKSPACE_PROJECT } from '~/issues/constants';
import { __ } from '~/locale';
const allowLabelRemove = true;
@@ -20,15 +16,13 @@ const fullPath = '/full-path';
const labelsFilterBasePath = '/labels-filter-base-path';
const initialLabels = [];
const issuableType = 'issue';
-const labelType = LabelType.project;
-const variant = DropdownVariant.Embedded;
-const workspaceType = WorkspaceType.project;
+const labelType = WORKSPACE_PROJECT;
+const variant = VARIANT_EMBEDDED;
+const workspaceType = WORKSPACE_PROJECT;
describe('IssuableLabelSelector', () => {
let wrapper;
- const findTitle = () => wrapper.find('label').text().replace(/\s+/, ' ');
- const findLabelIcon = () => wrapper.findComponent(GlIcon);
const findAllHiddenInputs = () => wrapper.findAll('input[type="hidden"]');
const findLabelSelector = () => wrapper.findComponent(LabelsSelect);
@@ -50,27 +44,11 @@ describe('IssuableLabelSelector', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
- const expectTitleWithCount = (count) => {
- const title = findTitle();
-
- expect(title).toContain(__('Labels'));
- expect(title).toContain(count.toString());
- };
-
describe('by default', () => {
beforeEach(() => {
wrapper = createComponent();
});
- it('has the selected labels count', () => {
- expectTitleWithCount(0);
- expect(findLabelIcon().props('name')).toBe('labels');
- });
-
it('has the label selector', () => {
expect(findLabelSelector().props()).toMatchObject({
allowLabelRemove,
@@ -96,7 +74,6 @@ describe('IssuableLabelSelector', () => {
it('passing initial labels applies them to the form', () => {
wrapper = createComponent({ initialLabels: [mockRegularLabel, mockScopedLabel] });
- expectTitleWithCount(2);
expect(findLabelSelector().props('selectedLabels')).toStrictEqual([
mockRegularLabel,
mockScopedLabel,
@@ -110,13 +87,11 @@ describe('IssuableLabelSelector', () => {
it('updates the selected labels on the `updateSelectedLabels` event', async () => {
wrapper = createComponent();
- expectTitleWithCount(0);
expect(findLabelSelector().props('selectedLabels')).toStrictEqual([]);
expect(findAllHiddenInputs()).toHaveLength(0);
await findLabelSelector().vm.$emit('updateSelectedLabels', { labels: [mockRegularLabel] });
- expectTitleWithCount(1);
expect(findLabelSelector().props('selectedLabels')).toStrictEqual([mockRegularLabel]);
expect(findAllHiddenInputs().wrappers.map((input) => input.element.value)).toStrictEqual([
`${mockRegularLabel.id}`,
@@ -126,7 +101,6 @@ describe('IssuableLabelSelector', () => {
it('updates the selected labels on the `onLabelRemove` event', async () => {
wrapper = createComponent({ initialLabels: [mockRegularLabel] });
- expectTitleWithCount(1);
expect(findLabelSelector().props('selectedLabels')).toStrictEqual([mockRegularLabel]);
expect(findAllHiddenInputs().wrappers.map((input) => input.element.value)).toStrictEqual([
`${mockRegularLabel.id}`,
@@ -134,7 +108,6 @@ describe('IssuableLabelSelector', () => {
await findLabelSelector().vm.$emit('onLabelRemove', mockRegularLabel.id);
- expectTitleWithCount(0);
expect(findLabelSelector().props('selectedLabels')).toStrictEqual([]);
expect(findAllHiddenInputs()).toHaveLength(0);
});
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 a0b1d64b97c..d5603d4ba4b 100644
--- a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
+++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
@@ -7,8 +7,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import { blockingIssuablesQueries } from '~/vue_shared/components/issuable_blocked_icon/constants';
-import { issuableTypes } from '~/boards/constants';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import { truncate } from '~/lib/utils/text_utility';
import {
mockIssue,
@@ -49,11 +48,6 @@ describe('IssuableBlockedIcon', () => {
await waitForApollo();
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const createWrapperWithApollo = ({
item = mockBlockedIssue1,
blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1),
@@ -121,9 +115,9 @@ describe('IssuableBlockedIcon', () => {
};
it.each`
- mockIssuable | issuableType | expectedIcon
- ${mockIssue} | ${TYPE_ISSUE} | ${'issue-block'}
- ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'}
+ mockIssuable | issuableType | expectedIcon
+ ${mockIssue} | ${TYPE_ISSUE} | ${'issue-block'}
+ ${mockEpic} | ${TYPE_EPIC} | ${'entity-blocked'}
`(
'should render blocked icon for $issuableType',
({ mockIssuable, issuableType, expectedIcon }) => {
@@ -145,7 +139,7 @@ describe('IssuableBlockedIcon', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
- it('should not query for blocking issuables by default', async () => {
+ it('should not query for blocking issuables by default', () => {
createWrapperWithApollo();
expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title);
@@ -153,9 +147,9 @@ describe('IssuableBlockedIcon', () => {
describe('on mouseenter on blocked icon', () => {
it.each`
- item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
- ${mockBlockedIssue1} | ${TYPE_ISSUE} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
- ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
+ item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
+ ${mockBlockedIssue1} | ${TYPE_ISSUE} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
+ ${mockBlockedEpic1} | ${TYPE_EPIC} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
`(
'should query for blocking issuables and render the result for $issuableType',
async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => {
@@ -201,18 +195,18 @@ describe('IssuableBlockedIcon', () => {
await mouseenter();
});
- it('should render a title of the issuable', async () => {
+ it('should render a title of the issuable', () => {
expect(findIssuableTitle().text()).toBe(mockBlockingIssue1.title);
});
- it('should render issuable reference and link to the issuable', async () => {
+ it('should render issuable reference and link to the issuable', () => {
const formattedRef = mockBlockingIssue1.reference.split('/')[1];
expect(findGlLink().text()).toBe(formattedRef);
expect(findGlLink().attributes('href')).toBe(mockBlockingIssue1.webUrl);
});
- it('should render popover title with correct blocking issuable count', async () => {
+ it('should render popover title with correct blocking issuable count', () => {
expect(findPopoverTitle().text()).toBe('Blocked by 1 issue');
});
});
@@ -247,7 +241,7 @@ describe('IssuableBlockedIcon', () => {
expect(wrapper.html()).toMatchSnapshot();
});
- it('should render popover title with correct blocking issuable count', async () => {
+ it('should render popover title with correct blocking issuable count', () => {
expect(findPopoverTitle().text()).toBe('Blocked by 4 issues');
});
@@ -255,7 +249,7 @@ describe('IssuableBlockedIcon', () => {
expect(findHiddenBlockingCount().text()).toBe('+ 1 more issue');
});
- it('should link to the blocked issue page at the related issue anchor', async () => {
+ it('should link to the blocked issue page at the related issue anchor', () => {
expect(findViewAllIssuableLink().text()).toBe('View all blocking issues');
expect(findViewAllIssuableLink().attributes('href')).toBe(
`${mockBlockedIssue2.webUrl}#related-issues`,
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
index a25f92c9cf2..c23bd002ee5 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar_spec.js
@@ -28,7 +28,6 @@ describe('IssuableBulkEditSidebar', () => {
});
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
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 2fac004875a..502fa609ebc 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
@@ -39,7 +39,6 @@ describe('IssuableItem', () => {
const mockLabels = mockIssuable.labels.nodes;
const mockAuthor = mockIssuable.author;
- const originalUrl = gon.gitlab_url;
let wrapper;
const findTimestampWrapper = () => wrapper.find('[data-testid="issuable-timestamp"]');
@@ -49,11 +48,6 @@ describe('IssuableItem', () => {
gon.gitlab_url = MOCK_GITLAB_URL;
});
- afterEach(() => {
- wrapper.destroy();
- gon.gitlab_url = originalUrl;
- });
-
describe('computed', () => {
describe('author', () => {
it('returns `issuable.author` reference', () => {
@@ -337,7 +331,7 @@ describe('IssuableItem', () => {
});
});
- it('renders spam icon when issuable is hidden', async () => {
+ it('renders spam icon when issuable is hidden', () => {
wrapper = createComponent({ issuable: { ...mockIssuable, hidden: true } });
const hiddenIcon = wrapper.findComponent(GlIcon);
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
index 371844e66f4..ec975dfdcb5 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_list_root_spec.js
@@ -47,10 +47,6 @@ describe('IssuableListRoot', () => {
const findVueDraggable = () => wrapper.findComponent(VueDraggable);
const findPageSizeSelector = () => wrapper.findComponent(PageSizeSelector);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
beforeEach(() => {
wrapper = createComponent();
@@ -337,7 +333,7 @@ describe('IssuableListRoot', () => {
describe('alert', () => {
const error = 'oopsie!';
- it('shows alert when there is an error', () => {
+ it('shows an alert when there is an error', () => {
wrapper = createComponent({ props: { error } });
expect(findAlert().text()).toBe(error);
@@ -508,7 +504,7 @@ describe('IssuableListRoot', () => {
});
});
- it('has the page size change component', async () => {
+ it('has the page size change component', () => {
expect(findPageSizeSelector().exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
index 27985895c62..9cdd4d75c42 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_tabs_spec.js
@@ -35,7 +35,6 @@ describe('IssuableTabs', () => {
afterEach(() => {
setLanguage(null);
- wrapper.destroy();
});
const findAllGlBadges = () => wrapper.findAllComponents(GlBadge);
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index b67bd0f42fe..964b48f4275 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -60,6 +60,12 @@ export const mockIssuable = {
type: 'issue',
};
+export const mockIssuableItems = (n) =>
+ [...Array(n).keys()].map((i) => ({
+ id: i,
+ ...mockIssuable,
+ }));
+
export const mockIssuables = [
mockIssuable,
{
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
index 6b20f0c77a3..02e729a00bd 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_body_spec.js
@@ -1,5 +1,5 @@
+import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { useFakeDate } from 'helpers/fake_date';
import IssuableBody from '~/vue_shared/issuable/show/components/issuable_body.vue';
@@ -13,101 +13,77 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
jest.mock('~/autosave');
-jest.mock('~/flash');
+jest.mock('~/alert');
+jest.mock('~/task_list');
const issuableBodyProps = {
...mockIssuableShowProps,
issuable: mockIssuable,
};
-const createComponent = (propsData = issuableBodyProps) =>
- shallowMount(IssuableBody, {
- propsData,
- stubs: {
- IssuableTitle,
- IssuableDescription,
- IssuableEditForm,
- TimeAgoTooltip,
- },
- slots: {
- 'status-badge': 'Open',
- 'edit-form-actions': `
- <button class="js-save">Save changes</button>
- <button class="js-cancel">Cancel</button>
- `,
- },
- });
-
describe('IssuableBody', () => {
// Some assertions expect a date later than our default
useFakeDate(2020, 11, 11);
let wrapper;
- beforeEach(() => {
- wrapper = createComponent();
- });
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(IssuableBody, {
+ propsData: {
+ ...issuableBodyProps,
+ ...propsData,
+ },
+ stubs: {
+ IssuableTitle,
+ IssuableDescription,
+ IssuableEditForm,
+ TimeAgoTooltip,
+ },
+ slots: {
+ 'status-badge': 'Open',
+ 'edit-form-actions': `
+ <button class="js-save">Save changes</button>
+ <button class="js-cancel">Cancel</button>
+ `,
+ },
+ });
+ };
+
+ const findUpdatedLink = () => wrapper.findComponent(GlLink);
+ const findIssuableEditForm = () => wrapper.findComponent(IssuableEditForm);
+ const findIssuableEditFormButton = (type) => findIssuableEditForm().find(`button.js-${type}`);
+ const findIssuableTitle = () => wrapper.findComponent(IssuableTitle);
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ createComponent();
+ TaskList.mockClear();
});
describe('computed', () => {
- describe('isUpdated', () => {
- it.each`
- updatedAt | returnValue
- ${mockIssuable.updatedAt} | ${true}
- ${null} | ${false}
- ${''} | ${false}
- `(
- 'returns $returnValue when value of `updateAt` prop is `$updatedAt`',
- async ({ updatedAt, returnValue }) => {
- wrapper.setProps({
- issuable: {
- ...mockIssuable,
- updatedAt,
- },
- });
-
- await nextTick();
-
- expect(wrapper.vm.isUpdated).toBe(returnValue);
- },
- );
- });
-
describe('updatedBy', () => {
it('returns value of `issuable.updatedBy`', () => {
- expect(wrapper.vm.updatedBy).toBe(mockIssuable.updatedBy);
+ expect(findUpdatedLink().text()).toBe(mockIssuable.updatedBy.name);
+ expect(findUpdatedLink().attributes('href')).toBe(mockIssuable.updatedBy.webUrl);
});
});
});
describe('watchers', () => {
describe('editFormVisible', () => {
- it('calls initTaskList in nextTick', async () => {
- jest.spyOn(wrapper.vm, 'initTaskList');
- wrapper.setProps({
- editFormVisible: true,
- });
-
- await nextTick();
-
- wrapper.setProps({
+ it('calls initTaskList in nextTick', () => {
+ createComponent({
editFormVisible: false,
});
- await nextTick();
-
- expect(wrapper.vm.initTaskList).toHaveBeenCalled();
+ expect(TaskList).toHaveBeenCalled();
});
});
});
describe('mounted', () => {
it('initializes TaskList instance when enabledEdit and enableTaskList props are true', () => {
- expect(wrapper.vm.taskList instanceof TaskList).toBe(true);
- expect(wrapper.vm.taskList).toMatchObject({
+ createComponent();
+ expect(TaskList).toHaveBeenCalledWith({
dataType: 'issue',
fieldName: 'description',
lockVersion: issuableBodyProps.taskListLockVersion,
@@ -118,14 +94,12 @@ describe('IssuableBody', () => {
});
it('does not initialize TaskList instance when either enabledEdit or enableTaskList prop is false', () => {
- const wrapperNoTaskList = createComponent({
+ createComponent({
...issuableBodyProps,
enableTaskList: false,
});
- expect(wrapperNoTaskList.vm.taskList).not.toBeDefined();
-
- wrapperNoTaskList.destroy();
+ expect(TaskList).toHaveBeenCalledTimes(0);
});
});
@@ -154,10 +128,8 @@ describe('IssuableBody', () => {
describe('template', () => {
it('renders issuable-title component', () => {
- const titleEl = wrapper.findComponent(IssuableTitle);
-
- expect(titleEl.exists()).toBe(true);
- expect(titleEl.props()).toMatchObject({
+ expect(findIssuableTitle().exists()).toBe(true);
+ expect(findIssuableTitle().props()).toMatchObject({
issuable: issuableBodyProps.issuable,
statusIcon: issuableBodyProps.statusIcon,
enableEdit: issuableBodyProps.enableEdit,
@@ -172,42 +144,37 @@ describe('IssuableBody', () => {
});
it('renders issuable edit info', () => {
- const editedEl = wrapper.find('small');
-
- expect(editedEl.text()).toMatchInterpolatedText('Edited 3 months ago by Administrator');
+ expect(wrapper.find('small').text()).toMatchInterpolatedText(
+ 'Edited 3 months ago by Administrator',
+ );
});
- it('renders issuable-edit-form when `editFormVisible` prop is true', async () => {
- wrapper.setProps({
+ it('renders issuable-edit-form when `editFormVisible` prop is true', () => {
+ createComponent({
editFormVisible: true,
});
- await nextTick();
-
- const editFormEl = wrapper.findComponent(IssuableEditForm);
- expect(editFormEl.exists()).toBe(true);
- expect(editFormEl.props()).toMatchObject({
+ expect(findIssuableEditForm().exists()).toBe(true);
+ expect(findIssuableEditForm().props()).toMatchObject({
issuable: issuableBodyProps.issuable,
enableAutocomplete: issuableBodyProps.enableAutocomplete,
descriptionPreviewPath: issuableBodyProps.descriptionPreviewPath,
descriptionHelpPath: issuableBodyProps.descriptionHelpPath,
});
- expect(editFormEl.find('button.js-save').exists()).toBe(true);
- expect(editFormEl.find('button.js-cancel').exists()).toBe(true);
+ expect(findIssuableEditFormButton('save').exists()).toBe(true);
+ expect(findIssuableEditFormButton('cancel').exists()).toBe(true);
});
describe('events', () => {
it('component emits `edit-issuable` event bubbled via issuable-title', () => {
- const issuableTitle = wrapper.findComponent(IssuableTitle);
-
- issuableTitle.vm.$emit('edit-issuable');
+ findIssuableTitle().vm.$emit('edit-issuable');
expect(wrapper.emitted('edit-issuable')).toHaveLength(1);
});
it.each(['keydown-title', 'keydown-description'])(
'component emits `%s` event with event object and issuableMeta params via issuable-edit-form',
- async (eventName) => {
+ (eventName) => {
const eventObj = {
preventDefault: jest.fn(),
stopPropagation: jest.fn(),
@@ -217,15 +184,11 @@ describe('IssuableBody', () => {
issuableDescription: 'foobar',
};
- wrapper.setProps({
+ createComponent({
editFormVisible: true,
});
- await nextTick();
-
- const issuableEditForm = wrapper.findComponent(IssuableEditForm);
-
- issuableEditForm.vm.$emit(eventName, eventObj, issuableMeta);
+ findIssuableEditForm().vm.$emit(eventName, eventObj, issuableMeta);
expect(wrapper.emitted(eventName)).toHaveLength(1);
expect(wrapper.emitted(eventName)[0]).toMatchObject([eventObj, issuableMeta]);
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
index ea58cc2baf5..b4f1c286158 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_description_spec.js
@@ -24,10 +24,6 @@ describe('IssuableDescription', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('mounted', () => {
it('calls `renderGFM`', () => {
expect(renderGFM).toHaveBeenCalledTimes(1);
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
index 159be4cd1ef..4a52c2a8dad 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
@@ -43,6 +43,9 @@ describe('IssuableEditForm', () => {
});
afterEach(() => {
+ // note: the order of wrapper.destroy() and jest.resetAllMocks() matters.
+ // maybe it'll help with investigation on how to remove this wrapper.destroy() call
+ // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
wrapper.destroy();
jest.resetAllMocks();
});
@@ -162,7 +165,7 @@ describe('IssuableEditForm', () => {
stopPropagation: jest.fn(),
};
- it('component emits `keydown-title` event with event object and issuableMeta params via gl-form-input', async () => {
+ it('component emits `keydown-title` event with event object and issuableMeta params via gl-form-input', () => {
const titleInputEl = wrapper.findComponent(GlFormInput);
titleInputEl.vm.$emit('keydown', eventObj, 'title');
@@ -176,7 +179,7 @@ describe('IssuableEditForm', () => {
]);
});
- it('component emits `keydown-description` event with event object and issuableMeta params via textarea', async () => {
+ it('component emits `keydown-description` event with event object and issuableMeta params via textarea', () => {
const descriptionInputEl = wrapper.find('[data-testid="description"] textarea');
descriptionInputEl.trigger('keydown', eventObj, 'description');
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 6a8b9ef77a9..fa38ab8d44d 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlIcon, GlAvatarLabeled } from '@gitlab/ui';
+import { GlButton, GlBadge, GlIcon, GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
@@ -13,7 +13,10 @@ const issuableHeaderProps = {
describe('IssuableHeader', () => {
let wrapper;
+ const findAvatar = () => wrapper.findByTestId('avatar');
const findTaskStatusEl = () => wrapper.findByTestId('task-status');
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findGlAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const createComponent = (props = {}, { stubs } = {}) => {
wrapper = shallowMountExtended(IssuableHeader, {
@@ -33,7 +36,6 @@ describe('IssuableHeader', () => {
};
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
@@ -41,7 +43,7 @@ describe('IssuableHeader', () => {
describe('authorId', () => {
it('returns numeric ID from GraphQL ID of `author` prop', () => {
createComponent();
- expect(wrapper.vm.authorId).toBe(1);
+ expect(findGlAvatarLink().attributes('data-user-id')).toBe('1');
});
});
});
@@ -53,12 +55,14 @@ describe('IssuableHeader', () => {
it('dispatches `click` event on sidebar toggle button', () => {
createComponent();
- wrapper.vm.toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
- jest.spyOn(wrapper.vm.toggleSidebarButtonEl, 'dispatchEvent').mockImplementation(jest.fn);
+ const toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
+ const dispatchEvent = jest
+ .spyOn(toggleSidebarButtonEl, 'dispatchEvent')
+ .mockImplementation(jest.fn);
- wrapper.vm.handleRightSidebarToggleClick();
+ findButton().vm.$emit('click');
- expect(wrapper.vm.toggleSidebarButtonEl.dispatchEvent).toHaveBeenCalledWith(
+ expect(dispatchEvent).toHaveBeenCalledWith(
expect.objectContaining({
type: 'click',
}),
@@ -78,7 +82,7 @@ describe('IssuableHeader', () => {
expect(statusBoxEl.text()).toContain('Open');
});
- it('renders blocked icon when issuable is blocked', async () => {
+ it('renders blocked icon when issuable is blocked', () => {
createComponent({
blocked: true,
});
@@ -89,7 +93,7 @@ describe('IssuableHeader', () => {
expect(blockedEl.findComponent(GlIcon).props('name')).toBe('lock');
});
- it('renders confidential icon when issuable is confidential', async () => {
+ it('renders confidential icon when issuable is confidential', () => {
createComponent({
confidential: true,
});
@@ -110,7 +114,7 @@ describe('IssuableHeader', () => {
href: webUrl,
target: '_blank',
};
- const avatarEl = wrapper.findByTestId('avatar');
+ const avatarEl = findAvatar();
expect(avatarEl.exists()).toBe(true);
expect(avatarEl.attributes()).toMatchObject(avatarElAttrs);
expect(avatarEl.findComponent(GlAvatarLabeled).attributes()).toMatchObject({
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
index edfd55c8bb4..f976e0499f0 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_show_root_spec.js
@@ -41,10 +41,6 @@ describe('IssuableShowRoot', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('template', () => {
const {
statusIcon,
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 6f62fb77353..39316dfa249 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
@@ -22,35 +22,35 @@ const createComponent = (propsData = issuableTitleProps) =>
'status-badge': 'Open',
},
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
describe('IssuableTitle', () => {
let wrapper;
+ const findStickyHeader = () => wrapper.findComponent('[data-testid="header"]');
+
beforeEach(() => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('methods', () => {
describe('handleTitleAppear', () => {
- it('sets value of `stickyTitleVisible` prop to false', () => {
+ it('sets value of `stickyTitleVisible` prop to false', async () => {
wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear');
+ await nextTick();
- expect(wrapper.vm.stickyTitleVisible).toBe(false);
+ expect(findStickyHeader().exists()).toBe(false);
});
});
describe('handleTitleDisappear', () => {
- it('sets value of `stickyTitleVisible` prop to true', () => {
+ it('sets value of `stickyTitleVisible` prop to true', async () => {
wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
+ await nextTick();
- expect(wrapper.vm.stickyTitleVisible).toBe(true);
+ expect(findStickyHeader().exists()).toBe(true);
});
});
});
@@ -87,14 +87,10 @@ describe('IssuableTitle', () => {
});
it('renders sticky header when `stickyTitleVisible` prop is true', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- stickyTitleVisible: true,
- });
-
+ wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear');
await nextTick();
- const stickyHeaderEl = wrapper.find('[data-testid="header"]');
+
+ const stickyHeaderEl = findStickyHeader();
expect(stickyHeaderEl.exists()).toBe(true);
expect(stickyHeaderEl.findComponent(GlBadge).props('variant')).toBe('success');
diff --git a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
index 6c9e5f85fa0..f2509aead77 100644
--- a/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/sidebar/components/issuable_sidebar_root_spec.js
@@ -38,7 +38,6 @@ describe('IssuableSidebarRoot', () => {
};
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
});
diff --git a/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js b/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js
index 52f36aa0e77..052ff518468 100644
--- a/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/components/legacy_container_spec.js
@@ -11,9 +11,7 @@ describe('Legacy container component', () => {
};
afterEach(() => {
- wrapper.destroy();
resetHTMLFixture();
- wrapper = null;
});
describe('when selector targets real node', () => {
diff --git a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
index c90131fea9a..cc8a8d86d19 100644
--- a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
@@ -27,9 +27,7 @@ describe('Welcome page', () => {
});
afterEach(() => {
- wrapper.destroy();
window.location.hash = '';
- wrapper = null;
});
it('tracks link clicks', async () => {
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 6115dc6e61b..b87ae8a232f 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
@@ -4,17 +4,21 @@ import { nextTick } from 'vue';
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';
+import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue';
+import { sidebarState } from '~/super_sidebar/constants';
-describe('Experimental new project creation app', () => {
+jest.mock('~/super_sidebar/constants');
+describe('Experimental new namespace creation app', () => {
let wrapper;
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
+ const findSuperSidebarToggle = () => wrapper.findComponent(SuperSidebarToggle);
const DEFAULT_PROPS = {
title: 'Create something',
- initialBreadcrumb: 'Something',
+ initialBreadcrumbs: [{ text: 'Something', href: '#' }],
panels: [
{ name: 'panel1', selector: '#some-selector1' },
{ name: 'panel2', selector: '#some-selector2' },
@@ -33,7 +37,6 @@ describe('Experimental new project creation app', () => {
};
afterEach(() => {
- wrapper.destroy();
window.location.hash = '';
});
@@ -46,8 +49,8 @@ describe('Experimental new project creation app', () => {
expect(findWelcomePage().exists()).toBe(true);
});
- it('does not render breadcrumbs', () => {
- expect(findBreadcrumb().exists()).toBe(false);
+ it('renders breadcrumbs', () => {
+ expect(findBreadcrumb().exists()).toBe(true);
});
});
@@ -75,7 +78,7 @@ describe('Experimental new project creation app', () => {
it('renders breadcrumbs', () => {
const breadcrumb = findBreadcrumb();
expect(breadcrumb.exists()).toBe(true);
- expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumb);
+ expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumbs[0].text);
});
});
@@ -104,4 +107,22 @@ describe('Experimental new project creation app', () => {
expect(findWelcomePage().exists()).toBe(false);
expect(findLegacyContainer().exists()).toBe(true);
});
+
+ describe.each`
+ featureFlag | isSuperSidebarCollapsed | isToggleVisible
+ ${true} | ${true} | ${true}
+ ${true} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ ${false} | ${false} | ${false}
+ `('Super sidebar toggle', ({ featureFlag, isSuperSidebarCollapsed, isToggleVisible }) => {
+ beforeEach(() => {
+ sidebarState.isCollapsed = isSuperSidebarCollapsed;
+ gon.use_new_navigation = featureFlag;
+ createComponent();
+ });
+
+ it(`${isToggleVisible ? 'is visible' : 'is not visible'}`, () => {
+ expect(findSuperSidebarToggle().exists()).toBe(isToggleVisible);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/plugins/global_toast_spec.js b/spec/frontend/vue_shared/plugins/global_toast_spec.js
index 322586a772c..0bf2737fb2b 100644
--- a/spec/frontend/vue_shared/plugins/global_toast_spec.js
+++ b/spec/frontend/vue_shared/plugins/global_toast_spec.js
@@ -1,14 +1,16 @@
-import toast, { instance } from '~/vue_shared/plugins/global_toast';
+import toast from '~/vue_shared/plugins/global_toast';
-describe('Global toast', () => {
- let spyFunc;
-
- beforeEach(() => {
- spyFunc = jest.spyOn(instance.$toast, 'show').mockImplementation(() => {});
- });
+const mockSpy = jest.fn();
+jest.mock('@gitlab/ui', () => ({
+ GlToast: (Vue) => {
+ // eslint-disable-next-line no-param-reassign
+ Vue.prototype.$toast = { show: (...args) => mockSpy(...args) };
+ },
+}));
+describe('Global toast', () => {
afterEach(() => {
- spyFunc.mockRestore();
+ mockSpy.mockRestore();
});
it("should call GitLab UI's toast method", () => {
@@ -17,7 +19,7 @@ describe('Global toast', () => {
toast(arg1, arg2);
- expect(instance.$toast.show).toHaveBeenCalledTimes(1);
- expect(instance.$toast.show).toHaveBeenCalledWith(arg1, arg2);
+ expect(mockSpy).toHaveBeenCalledTimes(1);
+ expect(mockSpy).toHaveBeenCalledWith(arg1, arg2);
});
});
diff --git a/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
index 136fe74b0d6..d258658d5e2 100644
--- a/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
+++ b/spec/frontend/vue_shared/security_configuration/components/section_layout_spec.js
@@ -21,10 +21,6 @@ describe('Section Layout component', () => {
const findHeading = () => wrapper.find('h2');
const findLoader = () => wrapper.findComponent(SectionLoader);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('basic structure', () => {
beforeEach(() => {
createComponent({ heading: 'testheading' });
diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
index 0a5e46d9263..f3d0d66cdd1 100644
--- a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
@@ -7,8 +7,10 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { humanize } from '~/lib/utils/text_utility';
-import { redirectTo } from '~/lib/utils/url_utility';
-import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import ManageViaMr, {
+ i18n,
+} from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { buildConfigureSecurityFeatureMockFactory } from './apollo_mocks';
@@ -17,6 +19,7 @@ jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);
const projectFullPath = 'namespace/project';
+const ufErrorPrefix = 'Foo:';
describe('ManageViaMr component', () => {
let wrapper;
@@ -56,8 +59,8 @@ describe('ManageViaMr component', () => {
);
}
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ gon.uf_error_prefix = ufErrorPrefix;
});
// This component supports different report types/mutations depending on
@@ -76,15 +79,19 @@ describe('ManageViaMr component', () => {
const buildConfigureSecurityFeatureMock = buildConfigureSecurityFeatureMockFactory(
mutationId,
);
- const successHandler = jest.fn(async () => buildConfigureSecurityFeatureMock());
- const noSuccessPathHandler = async () =>
+ const successHandler = jest.fn().mockResolvedValue(buildConfigureSecurityFeatureMock());
+ const noSuccessPathHandler = jest.fn().mockResolvedValue(
buildConfigureSecurityFeatureMock({
successPath: '',
- });
- const errorHandler = async () =>
- buildConfigureSecurityFeatureMock({
- errors: ['foo'],
- });
+ }),
+ );
+ const errorHandler = (message = 'foo') => {
+ return Promise.resolve(
+ buildConfigureSecurityFeatureMock({
+ errors: [message],
+ }),
+ );
+ };
const pendingHandler = () => new Promise(() => {});
describe('when feature is configured', () => {
@@ -139,8 +146,8 @@ describe('ManageViaMr component', () => {
it('should call redirect helper with correct value', async () => {
await wrapper.trigger('click');
await waitForPromises();
- expect(redirectTo).toHaveBeenCalledTimes(1);
- expect(redirectTo).toHaveBeenCalledWith('testSuccessPath');
+ expect(redirectTo).toHaveBeenCalledTimes(1); // eslint-disable-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith('testSuccessPath'); // eslint-disable-line import/no-deprecated
// This is done for UX reasons. If the loading prop is set to false
// on success, then there's a period where the button is clickable
// again. Instead, we want the button to display a loading indicator
@@ -151,9 +158,12 @@ describe('ManageViaMr component', () => {
});
describe.each`
- handler | message
- ${noSuccessPathHandler} | ${`${featureName} merge request creation mutation failed`}
- ${errorHandler} | ${'foo'}
+ handler | message
+ ${noSuccessPathHandler} | ${`${featureName} merge request creation mutation failed`}
+ ${errorHandler.bind(null, `${ufErrorPrefix} message`)} | ${'message'}
+ ${errorHandler.bind(null, 'Blah: message')} | ${i18n.genericErrorText}
+ ${errorHandler.bind(null, 'message')} | ${i18n.genericErrorText}
+ ${errorHandler} | ${i18n.genericErrorText}
`('given an error response', ({ handler, message }) => {
beforeEach(() => {
const apolloProvider = createMockApolloProvider(mutation, handler);
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 5f2b13a79c9..299a3d62421 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
@@ -15,11 +15,6 @@ describe('SecurityReportDownloadDropdown component', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('given report artifacts', () => {
beforeEach(() => {
artifacts = [
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index 221da35de3d..257f59612e8 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -14,7 +14,7 @@ import {
sastDiffSuccessMock,
secretDetectionDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
-import { createAlert } from '~/flash';
+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 HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
@@ -26,7 +26,7 @@ import {
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
-jest.mock('~/flash');
+jest.mock('~/alert');
Vue.use(VueApollo);
Vue.use(Vuex);
@@ -74,10 +74,6 @@ describe('Security reports app', () => {
const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
const findHelpIconComponent = () => wrapper.findComponent(HelpIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('given the artifacts query is loading', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/webhooks/components/form_url_app_spec.js b/spec/frontend/webhooks/components/form_url_app_spec.js
index 45a39d2dd58..cbeff184e9d 100644
--- a/spec/frontend/webhooks/components/form_url_app_spec.js
+++ b/spec/frontend/webhooks/components/form_url_app_spec.js
@@ -19,10 +19,6 @@ describe('FormUrlApp', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findAllRadioButtons = () => wrapper.findAllComponents(GlFormRadio);
const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
const findUrlMaskDisable = () => findAllRadioButtons().at(0);
diff --git a/spec/frontend/webhooks/components/form_url_mask_item_spec.js b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
index 06c743749a6..6bae0ca9854 100644
--- a/spec/frontend/webhooks/components/form_url_mask_item_spec.js
+++ b/spec/frontend/webhooks/components/form_url_mask_item_spec.js
@@ -57,12 +57,12 @@ describe('FormUrlMaskItem', () => {
});
it('renders disabled key and value', () => {
- expect(findMaskItemKey().findComponent(GlFormInput).attributes('disabled')).toBe('true');
- expect(findMaskItemValue().findComponent(GlFormInput).attributes('disabled')).toBe('true');
+ expect(findMaskItemKey().findComponent(GlFormInput).attributes('disabled')).toBeDefined();
+ expect(findMaskItemValue().findComponent(GlFormInput).attributes('disabled')).toBeDefined();
});
it('renders disabled remove button', () => {
- expect(findRemoveButton().attributes('disabled')).toBe('true');
+ expect(findRemoveButton().attributes('disabled')).toBeDefined();
});
it('displays ************ as input value', () => {
diff --git a/spec/frontend/webhooks/components/push_events_spec.js b/spec/frontend/webhooks/components/push_events_spec.js
index ccb61c4049a..6889d48e904 100644
--- a/spec/frontend/webhooks/components/push_events_spec.js
+++ b/spec/frontend/webhooks/components/push_events_spec.js
@@ -61,7 +61,7 @@ describe('Webhook push events form editor component', () => {
await nextTick();
});
- it('all_branches should be selected by default', async () => {
+ it('all_branches should be selected by default', () => {
expect(findPushEventRulesGroup().element).toMatchSnapshot();
});
diff --git a/spec/frontend/webhooks/components/test_dropdown_spec.js b/spec/frontend/webhooks/components/test_dropdown_spec.js
index 2f62ca13469..36777b0ba64 100644
--- a/spec/frontend/webhooks/components/test_dropdown_spec.js
+++ b/spec/frontend/webhooks/components/test_dropdown_spec.js
@@ -1,6 +1,6 @@
import { GlDisclosureDropdown } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { getByRole } from '@testing-library/dom';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
import HookTestDropdown from '~/webhooks/components/test_dropdown.vue';
const mockItems = [
@@ -14,17 +14,14 @@ describe('HookTestDropdown', () => {
let wrapper;
const findDisclosure = () => wrapper.findComponent(GlDisclosureDropdown);
- const clickItem = (itemText) => {
- const item = getByRole(wrapper.element, 'button', { name: itemText });
- item.dispatchEvent(new MouseEvent('click'));
- };
const createComponent = (props) => {
- wrapper = mount(HookTestDropdown, {
+ wrapper = mountExtended(HookTestDropdown, {
propsData: {
items: mockItems,
...props,
},
+ attachTo: document.body,
});
};
@@ -55,7 +52,7 @@ describe('HookTestDropdown', () => {
});
});
- clickItem(mockItems[0].text);
+ wrapper.findByTestId('disclosure-dropdown-item').find('a').trigger('click');
return railsEventPromise;
});
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index ee15034daff..000b07f4dfd 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -49,7 +49,7 @@ describe('App', () => {
store,
propsData: buildProps(),
directives: {
- GlResizeObserver: createMockDirective(),
+ GlResizeObserver: createMockDirective('gl-resize-observer'),
},
});
};
@@ -71,7 +71,6 @@ describe('App', () => {
};
afterEach(() => {
- wrapper.destroy();
unmockTracking();
});
diff --git a/spec/frontend/whats_new/components/feature_spec.js b/spec/frontend/whats_new/components/feature_spec.js
index 099054bf8ca..d69ac2803df 100644
--- a/spec/frontend/whats_new/components/feature_spec.js
+++ b/spec/frontend/whats_new/components/feature_spec.js
@@ -30,11 +30,6 @@ describe("What's new single feature", () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
it('renders the date', () => {
createWrapper({ feature: exampleFeature });
diff --git a/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js b/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
index b199f4f0c49..79717b8767e 100644
--- a/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
+++ b/spec/frontend/whats_new/utils/get_drawer_body_height_spec.js
@@ -11,10 +11,6 @@ describe('~/whats_new/utils/get_drawer_body_height', () => {
});
});
- afterEach(() => {
- drawerWrapper.destroy();
- });
-
const setClientHeight = (el, height) => {
Object.defineProperty(el, 'clientHeight', {
get() {
diff --git a/spec/frontend/whats_new/utils/notification_spec.js b/spec/frontend/whats_new/utils/notification_spec.js
index dac02ee07bd..8b5663ee764 100644
--- a/spec/frontend/whats_new/utils/notification_spec.js
+++ b/spec/frontend/whats_new/utils/notification_spec.js
@@ -1,4 +1,5 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlWhatsNewNotification from 'test_fixtures_static/whats_new_notification.html';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { setNotification, getVersionDigest } from '~/whats_new/utils/notification';
@@ -12,7 +13,7 @@ describe('~/whats_new/utils/notification', () => {
const getAppEl = () => wrapper.querySelector('.app');
beforeEach(() => {
- loadHTMLFixture('static/whats_new_notification.html');
+ setHTMLFixture(htmlWhatsNewNotification);
wrapper = document.querySelector('.whats-new-notification-fixture-root');
});
diff --git a/spec/frontend/work_items/components/app_spec.js b/spec/frontend/work_items/components/app_spec.js
index 95034085493..d799e8042b1 100644
--- a/spec/frontend/work_items/components/app_spec.js
+++ b/spec/frontend/work_items/components/app_spec.js
@@ -12,10 +12,6 @@ describe('Work Items Application', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a component', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/item_state_spec.js b/spec/frontend/work_items/components/item_state_spec.js
index c3cc2fbc556..c3bdbfe030e 100644
--- a/spec/frontend/work_items/components/item_state_spec.js
+++ b/spec/frontend/work_items/components/item_state_spec.js
@@ -21,10 +21,6 @@ describe('ItemState', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders label and dropdown', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/item_title_spec.js b/spec/frontend/work_items/components/item_title_spec.js
index 6361f8dafc4..3a84ba4bd5e 100644
--- a/spec/frontend/work_items/components/item_title_spec.js
+++ b/spec/frontend/work_items/components/item_title_spec.js
@@ -19,10 +19,6 @@ describe('ItemTitle', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders title contents', () => {
expect(findInputEl().attributes()).toMatchObject({
'data-placeholder': 'Add a title...',
@@ -51,7 +47,7 @@ describe('ItemTitle', () => {
expect(wrapper.emitted(eventName)).toBeDefined();
});
- it('renders only the text content from clipboard', async () => {
+ it('renders only the text content from clipboard', () => {
const htmlContent = '<strong>bold text</strong>';
const buildClipboardData = (data = {}) => ({
clipboardData: {
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 5901642b8a1..30577dc60cf 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,3 @@
// 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\\"></note-header-stub>"`;
+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>"`;
diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js
deleted file mode 100644
index eb4bcbf942b..00000000000
--- a/spec/frontend/work_items/components/notes/activity_filter_spec.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { ASC, DESC } from '~/notes/constants';
-
-import { mockTracking } from 'helpers/tracking_helper';
-import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-
-describe('Activity Filter', () => {
- let wrapper;
-
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findNewestFirstItem = () => wrapper.findByTestId('js-newest-first');
-
- const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => {
- wrapper = shallowMountExtended(ActivityFilter, {
- propsData: {
- sortOrder,
- loading,
- workItemType,
- },
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- describe('default', () => {
- it('has a dropdown with 2 options', () => {
- expect(findDropdown().exists()).toBe(true);
- expect(findAllDropdownItems()).toHaveLength(ActivityFilter.SORT_OPTIONS.length);
- });
-
- it('has local storage sync with the correct props', () => {
- expect(findLocalStorageSync().props('asString')).toBe(true);
- });
-
- it('emits `updateSavedSortOrder` event when update is emitted', async () => {
- findLocalStorageSync().vm.$emit('input', ASC);
-
- await nextTick();
- expect(wrapper.emitted('updateSavedSortOrder')).toHaveLength(1);
- expect(wrapper.emitted('updateSavedSortOrder')).toEqual([[ASC]]);
- });
- });
-
- describe('when asc', () => {
- describe('when the dropdown is clicked', () => {
- it('calls the right actions', async () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- findNewestFirstItem().vm.$emit('click');
- await nextTick();
-
- expect(wrapper.emitted('changeSortOrder')).toHaveLength(1);
- expect(wrapper.emitted('changeSortOrder')).toEqual([[DESC]]);
-
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'notes_sort_order_changed',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'item_track_notes_sorting',
- property: 'type_Task',
- },
- );
- });
- });
- });
-});
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
new file mode 100644
index 00000000000..5ed9d581446
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js
@@ -0,0 +1,109 @@
+import { GlDropdown, GlDropdownItem } 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';
+import { ASC, DESC } from '~/notes/constants';
+import {
+ WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ WORK_ITEM_NOTES_SORT_ORDER_KEY,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ WORK_ITEM_NOTES_FILTER_KEY,
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_ACTIVITY_FILTER_OPTIONS,
+ TRACKING_CATEGORY_SHOW,
+} from '~/work_items/constants';
+
+import { mockTracking } from 'helpers/tracking_helper';
+
+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 createComponent = ({
+ loading = false,
+ workItemType = 'Task',
+ sortFilterProp = ASC,
+ filterOptions = WORK_ITEM_ACTIVITY_SORT_OPTIONS,
+ trackingLabel = 'item_track_notes_sorting',
+ trackingAction = 'work_item_notes_sort_order_changed',
+ filterEvent = 'changeSort',
+ defaultSortFilterProp = ASC,
+ storageKey = WORK_ITEM_NOTES_SORT_ORDER_KEY,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemActivitySortFilter, {
+ propsData: {
+ loading,
+ workItemType,
+ sortFilterProp,
+ filterOptions,
+ trackingLabel,
+ trackingAction,
+ filterEvent,
+ defaultSortFilterProp,
+ storageKey,
+ },
+ });
+ };
+
+ 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'}
+ `(
+ 'When used for $usedFor',
+ ({
+ filterOptions,
+ storageKey,
+ filterEvent,
+ trackingLabel,
+ trackingAction,
+ newInputOption,
+ defaultSortFilterProp,
+ sortFilterProp,
+ nonDefaultDataTestId,
+ }) => {
+ beforeEach(() => {
+ createComponent({
+ sortFilterProp,
+ filterOptions,
+ trackingLabel,
+ trackingAction,
+ filterEvent,
+ defaultSortFilterProp,
+ storageKey,
+ });
+ });
+
+ it('has a dropdown with options equal to the length of `filterOptions`', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findAllDropdownItems()).toHaveLength(filterOptions.length);
+ });
+
+ it('has local storage sync with the correct props', () => {
+ expect(findLocalStorageSync().props('asString')).toBe(true);
+ expect(findLocalStorageSync().props('storageKey')).toBe(storageKey);
+ });
+
+ it(`emits ${filterEvent} event when local storage input is emitted`, () => {
+ findLocalStorageSync().vm.$emit('input', newInputOption);
+
+ expect(wrapper.emitted(filterEvent)).toEqual([[newInputOption]]);
+ });
+
+ 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');
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, trackingAction, {
+ category: TRACKING_CATEGORY_SHOW,
+ label: trackingLabel,
+ property: 'type_Task',
+ });
+ });
+ },
+ );
+});
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 2a65e91a906..739340f4936 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
@@ -1,26 +1,20 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { clearDraft } from '~/lib/utils/autosave';
-import { config } from '~/graphql_shared/issuable_client';
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
- workItemResponseFactory,
- workItemQueryResponse,
- projectWorkItemResponse,
createWorkItemNoteResponse,
- mockWorkItemNotesResponse,
+ workItemByIidResponseFactory,
+ workItemQueryResponse,
} from '../../mock_data';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
@@ -28,62 +22,49 @@ jest.mock('~/lib/utils/autosave');
const workItemId = workItemQueryResponse.data.workItem.id;
-describe('WorkItemCommentForm', () => {
+describe('Work item add note', () => {
let wrapper;
Vue.use(VueApollo);
const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemNoteResponse);
- const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
let workItemResponseHandler;
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findTextarea = () => wrapper.findByTestId('note-reply-textarea');
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
canUpdate = true,
- workItemResponse = workItemResponseFactory({ canUpdate }),
- queryVariables = { id: workItemId },
- fetchByIid = false,
+ workItemIid = '1',
+ workItemResponse = workItemByIidResponseFactory({ canUpdate }),
signedIn = true,
isEditing = true,
workItemType = 'Task',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
-
if (signedIn) {
window.gon.current_user_id = '1';
window.gon.current_user_avatar_url = 'avatar.png';
}
- const apolloProvider = createMockApollo(
- [
- [workItemQuery, workItemResponseHandler],
- [createNoteMutation, mutationHandler],
- [workItemByIidQuery, workItemByIidResponseHandler],
- ],
- {},
- { ...config.cacheConfig },
- );
-
- apolloProvider.clients.defaultClient.writeQuery({
- query: workItemNotesQuery,
- variables: {
- id: workItemId,
- pageSize: 100,
- },
- data: mockWorkItemNotesResponse.data,
- });
+ const apolloProvider = createMockApollo([
+ [workItemByIidQuery, workItemResponseHandler],
+ [createNoteMutation, mutationHandler],
+ ]);
const { id } = workItemQueryResponse.data.workItem;
- wrapper = shallowMount(WorkItemAddNote, {
+ wrapper = shallowMountExtended(WorkItemAddNote, {
apolloProvider,
+ provide: {
+ fullPath: 'test-project-path',
+ },
propsData: {
workItemId: id,
- fullPath: 'test-project-path',
- queryVariables,
- fetchByIid,
+ workItemIid,
workItemType,
+ markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
+ autocompleteDataSources: {},
},
stubs: {
WorkItemCommentLocked,
@@ -93,7 +74,7 @@ describe('WorkItemCommentForm', () => {
await waitForPromises();
if (isEditing) {
- wrapper.findComponent(GlButton).vm.$emit('click');
+ findTextarea().trigger('click');
}
};
@@ -135,13 +116,7 @@ describe('WorkItemCommentForm', () => {
});
it('emits `replied` event and hides form after successful mutation', async () => {
- await createComponent({
- isEditing: true,
- signedIn: true,
- queryVariables: {
- id: mockWorkItemNotesResponse.data.workItem.id,
- },
- });
+ await createComponent({ isEditing: true, signedIn: true });
findCommentForm().vm.$emit('submitForm', 'some text');
await waitForPromises();
@@ -209,25 +184,48 @@ describe('WorkItemCommentForm', () => {
expect(wrapper.emitted('error')).toEqual([[error]]);
});
- });
- it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
- createComponent({ fetchByIid: false });
- await waitForPromises();
+ it('ignores errors when mutation returns additional information as errors for quick actions', async () => {
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ createNote: {
+ note: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ __typename: 'CreateNotePayload',
+ errors: ['Commands only Removed assignee @foobar.', 'Command names ["unassign"]'],
+ },
+ },
+ }),
+ });
- expect(workItemResponseHandler).toHaveBeenCalled();
- expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
+ findCommentForm().vm.$emit('submitForm', 'updated desc');
+
+ await waitForPromises();
+
+ expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
+ });
});
- it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
- await createComponent({ fetchByIid: true, isEditing: false });
+ it('calls the work item query', async () => {
+ await createComponent();
- expect(workItemResponseHandler).not.toHaveBeenCalled();
- expect(workItemByIidResponseHandler).toHaveBeenCalled();
+ expect(workItemResponseHandler).toHaveBeenCalled();
});
- it('skips calling the handlers when missing the needed queryVariables', async () => {
- await createComponent({ queryVariables: {}, fetchByIid: false, isEditing: false });
+ it('skips calling the work item query when missing workItemIid', async () => {
+ await createComponent({ workItemIid: null, isEditing: false });
expect(workItemResponseHandler).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
index 23a9f285804..147f2904761 100644
--- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -1,11 +1,23 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import * as autosave from '~/lib/utils/autosave';
import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
+import {
+ STATE_OPEN,
+ STATE_CLOSED,
+ STATE_EVENT_REOPEN,
+ STATE_EVENT_CLOSE,
+} from '~/work_items/constants';
import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse, workItemQueryResponse } from 'jest/work_items/mock_data';
+
+Vue.use(VueApollo);
const draftComment = 'draft comment';
@@ -18,6 +30,8 @@ jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => ({
confirmAction: jest.fn().mockResolvedValue(true),
}));
+const workItemId = 'gid://gitlab/WorkItem/1';
+
describe('Work item comment form component', () => {
let wrapper;
@@ -27,14 +41,29 @@ describe('Work item comment form component', () => {
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
- const createComponent = ({ isSubmitting = false, initialValue = '' } = {}) => {
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+
+ const createComponent = ({
+ isSubmitting = false,
+ initialValue = '',
+ isNewDiscussion = false,
+ workItemState = STATE_OPEN,
+ workItemType = 'Task',
+ mutationHandler = mutationSuccessHandler,
+ } = {}) => {
wrapper = shallowMount(WorkItemCommentForm, {
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
- workItemType: 'Issue',
+ workItemState,
+ workItemId,
+ workItemType,
ariaLabel: 'test-aria-label',
autosaveKey: mockAutosaveKey,
isSubmitting,
initialValue,
+ markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
+ autocompleteDataSources: {},
+ isNewDiscussion,
},
provide: {
fullPath: 'test-project-path',
@@ -42,11 +71,11 @@ describe('Work item comment form component', () => {
});
};
- it('passes correct markdown preview path to markdown editor', () => {
+ it('passes markdown preview path to markdown editor', () => {
createComponent();
expect(findMarkdownEditor().props('renderMarkdownPath')).toBe(
- '/test-project-path/preview_markdown?target_type=Issue',
+ '/group/project/preview_markdown?target_type=WorkItem',
);
});
@@ -99,7 +128,7 @@ describe('Work item comment form component', () => {
expect(findMarkdownEditor().props('value')).toBe('new comment');
});
- it('calls `updateDraft` with correct parameters', async () => {
+ it('calls `updateDraft` with correct parameters', () => {
findMarkdownEditor().vm.$emit('input', 'new comment');
expect(autosave.updateDraft).toHaveBeenCalledWith(mockAutosaveKey, 'new comment');
@@ -161,4 +190,63 @@ describe('Work item comment form component', () => {
expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
});
+
+ describe('when used as a top level/is a new discussion', () => {
+ describe('cancel button text', () => {
+ it.each`
+ workItemState | workItemType | buttonText
+ ${STATE_OPEN} | ${'Task'} | ${'Close task'}
+ ${STATE_CLOSED} | ${'Task'} | ${'Reopen task'}
+ ${STATE_OPEN} | ${'Objective'} | ${'Close objective'}
+ ${STATE_CLOSED} | ${'Objective'} | ${'Reopen objective'}
+ ${STATE_OPEN} | ${'Key result'} | ${'Close key result'}
+ ${STATE_CLOSED} | ${'Key result'} | ${'Reopen key result'}
+ `(
+ 'is "$buttonText" when "$workItemType" state is "$workItemState"',
+ ({ workItemState, workItemType, buttonText }) => {
+ createComponent({ isNewDiscussion: true, workItemState, workItemType });
+
+ expect(findCancelButton().text()).toBe(buttonText);
+ },
+ );
+ });
+
+ describe('Close/reopen button click', () => {
+ it.each`
+ workItemState | stateEvent
+ ${STATE_OPEN} | ${STATE_EVENT_CLOSE}
+ ${STATE_CLOSED} | ${STATE_EVENT_REOPEN}
+ `(
+ 'calls mutation with "$stateEvent" when workItemState is "$workItemState"',
+ async ({ workItemState, stateEvent }) => {
+ createComponent({ isNewDiscussion: true, workItemState });
+
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemQueryResponse.data.workItem.id,
+ stateEvent,
+ },
+ });
+ },
+ );
+
+ it('emits an error message when the mutation was unsuccessful', async () => {
+ createComponent({
+ isNewDiscussion: true,
+ mutationHandler: jest.fn().mockRejectedValue('Error!'),
+ });
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
index bb65b75c4d8..fac5011b6af 100644
--- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -1,7 +1,5 @@
-import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
@@ -13,7 +11,7 @@ import {
} from 'jest/work_items/mock_data';
import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
-const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find(
+const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workspace.workItems.nodes[0].widgets.find(
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
@@ -21,9 +19,6 @@ describe('Work Item Discussion', () => {
let wrapper;
const mockWorkItemId = 'gid://gitlab/WorkItem/625';
- const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
- const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
- const findAvatar = () => wrapper.findComponent(GlAvatar);
const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
const findAllThreads = () => wrapper.findAllComponents(WorkItemNote);
const findThreadAtIndex = (index) => findAllThreads().at(index);
@@ -33,19 +28,19 @@ describe('Work Item Discussion', () => {
const createComponent = ({
discussion = [mockWorkItemCommentNote],
workItemId = mockWorkItemId,
- queryVariables = { id: workItemId },
- fetchByIid = false,
- fullPath = 'gitlab-org',
workItemType = 'Task',
} = {}) => {
wrapper = shallowMount(WorkItemDiscussion, {
+ provide: {
+ fullPath: 'gitlab-org',
+ },
propsData: {
discussion,
workItemId,
- queryVariables,
- fetchByIid,
- fullPath,
+ workItemIid: '1',
workItemType,
+ markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
+ autocompleteDataSources: {},
},
});
};
@@ -55,19 +50,6 @@ describe('Work Item Discussion', () => {
createComponent();
});
- it('Should be wrapped inside the timeline entry item', () => {
- expect(findTimelineEntryItem().exists()).toBe(true);
- });
-
- it('should have the author avatar of the work item note', () => {
- expect(findAvatarLink().exists()).toBe(true);
- expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl);
-
- expect(findAvatar().exists()).toBe(true);
- expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl);
- expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username);
- });
-
it('should not show the the toggle replies widget wrapper when no replies', () => {
expect(findToggleRepliesWidget().exists()).toBe(false);
});
@@ -88,14 +70,18 @@ describe('Work Item Discussion', () => {
expect(findToggleRepliesWidget().exists()).toBe(true);
});
- it('the number of threads should be equal to the response length', async () => {
- findToggleRepliesWidget().vm.$emit('toggle');
- await nextTick();
+ it('the number of threads should be equal to the response length', () => {
expect(findAllThreads()).toHaveLength(
mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.length,
);
});
+ it('should collapse when we click on toggle replies widget', async () => {
+ findToggleRepliesWidget().vm.$emit('toggle');
+ await nextTick();
+ expect(findAllThreads()).toHaveLength(1);
+ });
+
it('should autofocus when we click expand replies', async () => {
const mainComment = findThreadAtIndex(0);
@@ -118,7 +104,7 @@ describe('Work Item Discussion', () => {
await findWorkItemAddNote().vm.$emit('replying', 'reply text');
});
- it('should show optimistic behavior when replying', async () => {
+ it('should show optimistic behavior when replying', () => {
expect(findAllThreads()).toHaveLength(2);
expect(findWorkItemNoteReplying().exists()).toBe(true);
});
diff --git a/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js
new file mode 100644
index 00000000000..339efad0608
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_history_only_filter_note_spec.js
@@ -0,0 +1,44 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemHistoryOnlyFilterNote from '~/work_items/components/notes/work_item_history_only_filter_note.vue';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+} from '~/work_items/constants';
+
+describe('Work Item History Filter note', () => {
+ let wrapper;
+
+ const findShowAllActivityButton = () => wrapper.findByTestId('show-all-activity');
+ const findShowCommentsButton = () => wrapper.findByTestId('show-comments-only');
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(WorkItemHistoryOnlyFilterNote, {
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('timelineContent renders a string containing instruction for switching feed type', () => {
+ expect(wrapper.text()).toContain(
+ "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.",
+ );
+ });
+
+ it('emits `changeFilter` event with 0 parameter on clicking Show all activity button', () => {
+ findShowAllActivityButton().vm.$emit('click');
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ALL_NOTES]]);
+ });
+
+ it('emits `changeFilter` event with 1 parameter on clicking Show comments only button', () => {
+ findShowCommentsButton().vm.$emit('click');
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index d85cd46c1c3..99bf391e261 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,52 +1,227 @@
+import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import EmojiPicker from '~/emoji/components/picker.vue';
+import waitForPromises from 'helpers/wait_for_promises';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+
+Vue.use(VueApollo);
describe('Work Item Note Actions', () => {
let wrapper;
+ const noteId = '1';
const findReplyButton = () => wrapper.findComponent(ReplyButton);
const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
+ const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]');
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
+ const findCopyLinkButton = () => wrapper.find('[data-testid="copy-link-action"]');
+ const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]');
+ const findReportAbuseToAdminButton = () => wrapper.find('[data-testid="abuse-note-action"]');
+
+ const addEmojiMutationResolver = jest.fn().mockResolvedValue({
+ data: {
+ errors: [],
+ },
+ });
+
+ const EmojiPickerStub = {
+ props: EmojiPicker.props,
+ template: '<div></div>',
+ };
- const createComponent = ({ showReply = true, showEdit = true } = {}) => {
+ const createComponent = ({
+ showReply = true,
+ showEdit = true,
+ showAwardEmoji = true,
+ showAssignUnassign = false,
+ canReportAbuse = false,
+ } = {}) => {
wrapper = shallowMount(WorkItemNoteActions, {
propsData: {
showReply,
showEdit,
+ noteId,
+ showAwardEmoji,
+ showAssignUnassign,
+ canReportAbuse,
},
+ provide: {
+ glFeatures: {
+ workItemsMvc2: true,
+ },
+ },
+ stubs: {
+ EmojiPicker: EmojiPickerStub,
+ },
+ apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]),
});
};
- describe('Default', () => {
- it('Should show the reply button by default', () => {
+ describe('reply button', () => {
+ it('is visible by default', () => {
createComponent();
+
expect(findReplyButton().exists()).toBe(true);
});
- });
- describe('When the reply button needs to be hidden', () => {
- it('Should show the reply button by default', () => {
+ it('is hidden when showReply false', () => {
createComponent({ showReply: false });
+
expect(findReplyButton().exists()).toBe(false);
});
});
- it('shows edit button when `showEdit` prop is true', () => {
- createComponent();
+ describe('edit button', () => {
+ it('is visible when `showEdit` prop is true', () => {
+ createComponent();
+
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('is hidden when `showEdit` prop is false', () => {
+ createComponent({ showEdit: false });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('emits `startEditing` event when clicked', () => {
+ createComponent();
+ findEditButton().vm.$emit('click');
+
+ expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ });
+ });
+
+ describe('emoji picker', () => {
+ it('is visible when `showAwardEmoji` prop is true', () => {
+ createComponent();
+
+ expect(findEmojiButton().exists()).toBe(true);
+ });
+
+ it('is hidden when `showAwardEmoji` prop is false', () => {
+ createComponent({ showAwardEmoji: false });
+
+ expect(findEmojiButton().exists()).toBe(false);
+ });
+
+ it('commits mutation on click', async () => {
+ const awardName = 'carrot';
+
+ createComponent();
+
+ findEmojiButton().vm.$emit('click', awardName);
+
+ await waitForPromises();
+
+ expect(findEmojiButton().emitted('errors')).toEqual(undefined);
+ expect(addEmojiMutationResolver).toHaveBeenCalledWith({
+ awardableId: noteId,
+ name: awardName,
+ });
+ });
+ });
+
+ describe('delete note', () => {
+ it('should display the `Delete comment` dropdown item if user has a permission to delete a note', () => {
+ createComponent({
+ showEdit: true,
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDeleteNoteButton().exists()).toBe(true);
+ });
+
+ it('should not display the `Delete comment` dropdown item if user has no permission to delete a note', () => {
+ createComponent({
+ showEdit: false,
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDeleteNoteButton().exists()).toBe(false);
+ });
+
+ it('should emit `deleteNote` event when delete note action is clicked', () => {
+ createComponent({
+ showEdit: true,
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
- expect(findEditButton().exists()).toBe(true);
+ expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ });
});
- it('does not show edit button when `showEdit` prop is false', () => {
- createComponent({ showEdit: false });
+ describe('copy link', () => {
+ beforeEach(() => {
+ createComponent({});
+ });
+ it('should display Copy link always', () => {
+ expect(findCopyLinkButton().exists()).toBe(true);
+ });
- expect(findEditButton().exists()).toBe(false);
+ it('should emit `notifyCopyDone` event when copy link note action is clicked', () => {
+ findCopyLinkButton().vm.$emit('click');
+
+ expect(wrapper.emitted('notifyCopyDone')).toEqual([[]]);
+ });
});
- it('emits `startEditing` event when edit button is clicked', () => {
- createComponent();
- findEditButton().vm.$emit('click');
+ describe('assign/unassign to commenting user', () => {
+ it('should not display assign/unassign by default', () => {
+ createComponent();
+
+ expect(findAssignUnassignButton().exists()).toBe(false);
+ });
+
+ it('should display assign/unassign when the props is true', () => {
+ createComponent({
+ showAssignUnassign: true,
+ });
- expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ expect(findAssignUnassignButton().exists()).toBe(true);
+ });
+
+ it('should emit `assignUser` event when assign note action is clicked', () => {
+ createComponent({
+ showAssignUnassign: true,
+ });
+
+ findAssignUnassignButton().vm.$emit('click');
+
+ expect(wrapper.emitted('assignUser')).toEqual([[]]);
+ });
+ });
+
+ describe('report abuse to admin', () => {
+ it('should not report abuse to admin by default', () => {
+ createComponent();
+
+ expect(findReportAbuseToAdminButton().exists()).toBe(false);
+ });
+
+ it('should display assign/unassign when the props is true', () => {
+ createComponent({
+ canReportAbuse: true,
+ });
+
+ expect(findReportAbuseToAdminButton().exists()).toBe(true);
+ });
+
+ it('should emit `reportAbuse` event when report abuse action is clicked', () => {
+ createComponent({
+ canReportAbuse: true,
+ });
+
+ findReportAbuseToAdminButton().vm.$emit('click');
+
+ expect(wrapper.emitted('reportAbuse')).toEqual([[]]);
+ });
});
});
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 9b87419cee7..f2cf5171cc1 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
@@ -1,10 +1,9 @@
-import { GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import mockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { updateDraft } from '~/lib/utils/autosave';
+import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import EditedAt from '~/issues/show/components/edited.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -13,7 +12,17 @@ import NoteHeader from '~/notes/components/note_header.vue';
import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
-import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import {
+ mockAssignees,
+ mockWorkItemCommentNote,
+ updateWorkItemMutationResponse,
+ workItemByIidResponseFactory,
+ workItemQueryResponse,
+} from 'jest/work_items/mock_data';
+import { i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { mockTracking } from 'helpers/tracking_helper';
Vue.use(VueApollo);
jest.mock('~/lib/utils/autosave');
@@ -22,6 +31,7 @@ describe('Work Item Note', () => {
let wrapper;
const updatedNoteText = '# Some title';
const updatedNoteBody = '<h1 data-sourcepos="1:1-1:12" dir="auto">Some title</h1>';
+ const mockWorkItemId = workItemQueryResponse.data.workItem.id;
const successHandler = jest.fn().mockResolvedValue({
data: {
@@ -35,32 +45,50 @@ describe('Work Item Note', () => {
},
},
});
+
+ const workItemResponseHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
+
+ const updateWorkItemMutationSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
+
const errorHandler = jest.fn().mockRejectedValue('Oops');
- const findAuthorAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
const findNoteActions = () => wrapper.findComponent(NoteActions);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const findEditedAt = () => wrapper.findComponent(EditedAt);
-
- const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
const findNoteWrapper = () => wrapper.find('[data-testid="note-wrapper"]');
const createComponent = ({
note = mockWorkItemCommentNote,
isFirstNote = false,
updateNoteMutationHandler = successHandler,
+ workItemId = mockWorkItemId,
+ updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
+ assignees = mockAssignees,
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
+ provide: {
+ fullPath: 'test-project-path',
+ },
propsData: {
+ workItemId,
+ workItemIid: '1',
note,
isFirstNote,
workItemType: 'Task',
+ markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
+ autocompleteDataSources: {},
+ assignees,
},
- apolloProvider: mockApollo([[updateWorkItemNoteMutation, updateNoteMutationHandler]]),
+ apolloProvider: mockApollo([
+ [workItemByIidQuery, workItemResponseHandler],
+ [updateWorkItemNoteMutation, updateNoteMutationHandler],
+ [updateWorkItemMutation, updateWorkItemMutationHandler],
+ ]),
});
};
@@ -124,6 +152,7 @@ describe('Work Item Note', () => {
await waitForPromises();
expect(findCommentForm().exists()).toBe(false);
+ expect(clearDraft).toHaveBeenCalledWith(`${mockWorkItemCommentNote.id}-comment`);
});
describe('when mutation fails', () => {
@@ -178,8 +207,7 @@ describe('Work Item Note', () => {
},
});
- expect(findEditedAt().exists()).toBe(true);
- expect(findEditedAt().props()).toEqual({
+ expect(findEditedAt().props()).toMatchObject({
updatedAt: '2023-02-12T07:47:40Z',
updatedByName: 'Administrator',
updatedByPath: 'test-path',
@@ -198,10 +226,6 @@ describe('Work Item Note', () => {
expect(findNoteActions().exists()).toBe(true);
});
- it('should not have the Avatar link for main thread inside the timeline-entry', () => {
- expect(findAuthorAvatarLink().exists()).toBe(false);
- });
-
it('should have the reply button props', () => {
expect(findNoteActions().props('showReply')).toBe(true);
});
@@ -219,43 +243,80 @@ describe('Work Item Note', () => {
expect(findNoteActions().exists()).toBe(true);
});
- it('should have the Avatar link for comment threads', () => {
- expect(findAuthorAvatarLink().exists()).toBe(true);
- });
-
it('should not have the reply button props', () => {
expect(findNoteActions().props('showReply')).toBe(false);
});
});
- it('should display a dropdown if user has a permission to delete a note', () => {
- createComponent({
- note: {
- ...mockWorkItemCommentNote,
- userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
- },
+ describe('assign/unassign to commenting user', () => {
+ it('calls a mutation with correct variables', async () => {
+ createComponent({ assignees: mockAssignees });
+ await waitForPromises();
+ findNoteActions().vm.$emit('assignUser');
+
+ await waitForPromises();
+
+ expect(updateWorkItemMutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockWorkItemId,
+ assigneesWidget: {
+ assigneeIds: [mockAssignees[1].id],
+ },
+ },
+ });
});
- expect(findDropdown().exists()).toBe(true);
- });
+ it('emits an error and resets assignees if mutation was rejected', async () => {
+ createComponent({
+ updateWorkItemMutationHandler: errorHandler,
+ assignees: [mockAssignees[0]],
+ });
- it('should not display a dropdown if user has no permission to delete a note', () => {
- createComponent();
+ await waitForPromises();
- expect(findDropdown().exists()).toBe(false);
- });
+ expect(findNoteActions().props('isAuthorAnAssignee')).toEqual(true);
- it('should emit `deleteNote` event when delete note action is clicked', () => {
- createComponent({
- note: {
- ...mockWorkItemCommentNote,
- userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
- },
+ findNoteActions().vm.$emit('assignUser');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[i18n.updateError]]);
+ expect(findNoteActions().props('isAuthorAnAssignee')).toEqual(true);
});
- findDeleteNoteButton().vm.$emit('click');
+ it('tracks the event', async () => {
+ createComponent();
+ await waitForPromises();
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findNoteActions().vm.$emit('assignUser');
- expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'unassigned_user', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'work_item_note_actions',
+ property: 'type_Task',
+ });
+ });
+ });
+
+ describe('report abuse props', () => {
+ it.each`
+ currentUserId | canReportAbuse | sameAsAuthor
+ ${1} | ${false} | ${'same as'}
+ ${4} | ${true} | ${'not same as'}
+ `(
+ 'should be $canReportAbuse when the author is $sameAsAuthor as the author of the note',
+ ({ currentUserId, canReportAbuse }) => {
+ window.gon = {
+ current_user_id: currentUserId,
+ };
+ createComponent();
+
+ expect(findNoteActions().props('canReportAbuse')).toBe(canReportAbuse);
+ },
+ );
});
});
});
diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
new file mode 100644
index 00000000000..daf74f7a93b
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js
@@ -0,0 +1,63 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
+import { ASC } from '~/notes/constants';
+import {
+ WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+} from '~/work_items/constants';
+
+describe('Work Item Note Activity Header', () => {
+ let wrapper;
+
+ const findActivityLabelHeading = () => wrapper.find('h3');
+ const findActivityFilterDropdown = () => wrapper.findByTestId('work-item-filter');
+ const findActivitySortDropdown = () => wrapper.findByTestId('work-item-sort');
+
+ const createComponent = ({
+ disableActivityFilterSort = false,
+ sortOrder = ASC,
+ workItemType = 'Task',
+ discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemNotesActivityHeader, {
+ propsData: {
+ disableActivityFilterSort,
+ sortOrder,
+ workItemType,
+ discussionFilter,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Should have the Activity label', () => {
+ expect(findActivityLabelHeading().text()).toBe(WorkItemNotesActivityHeader.i18n.activityLabel);
+ });
+
+ it('Should have Activity filtering dropdown', () => {
+ expect(findActivityFilterDropdown().exists()).toBe(true);
+ });
+
+ it('Should have Activity sorting dropdown', () => {
+ expect(findActivitySortDropdown().exists()).toBe(true);
+ });
+
+ describe('Activity Filter', () => {
+ it('emits `changeFilter` when filtering discussions', () => {
+ findActivityFilterDropdown().vm.$emit('changeFilter', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY);
+
+ expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]);
+ });
+ });
+
+ describe('Activity Sorting', () => {
+ it('emits `changeSort` when sorting discussions/activity', () => {
+ findActivitySortDropdown().vm.$emit('changeSort', ASC);
+
+ expect(wrapper.emitted('changeSort')).toEqual([[ASC]]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/widget_wrapper_spec.js b/spec/frontend/work_items/components/widget_wrapper_spec.js
index a87233300fc..87fbd1b3830 100644
--- a/spec/frontend/work_items/components/widget_wrapper_spec.js
+++ b/spec/frontend/work_items/components/widget_wrapper_spec.js
@@ -30,7 +30,7 @@ describe('WidgetWrapper component', () => {
expect(findWidgetBody().exists()).toBe(false);
});
- it('shows alert when list loading fails', () => {
+ it('shows an alert when list loading fails', () => {
const error = 'Some error';
createComponent({ error });
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 3c312fb4552..0045abe50d0 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -1,18 +1,46 @@
-import { GlDropdownDivider, GlModal } from '@gitlab/ui';
+import { GlDropdownDivider, GlModal, GlToggle } from '@gitlab/ui';
+import Vue from 'vue';
+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 { isLoggedIn } from '~/lib/utils/common_utils';
+import toast from '~/vue_shared/plugins/global_toast';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import {
+ TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
+ TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
+ TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
+ TEST_ID_DELETE_ACTION,
+ TEST_ID_PROMOTE_ACTION,
+} from '~/work_items/constants';
+import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
+import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
+import convertWorkItemMutation from '~/work_items/graphql/work_item_convert.mutation.graphql';
+import {
+ convertWorkItemMutationResponse,
+ projectWorkItemTypesQueryResponse,
+ convertWorkItemMutationErrorResponse,
+ workItemByIidResponseFactory,
+} from '../mock_data';
-const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
-const TEST_ID_DELETE_ACTION = 'delete-action';
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/vue_shared/plugins/global_toast');
describe('WorkItemActions component', () => {
+ Vue.use(VueApollo);
+
let wrapper;
let glModalDirective;
+ let mockApollo;
const findModal = () => wrapper.findComponent(GlModal);
const findConfidentialityToggleButton = () =>
wrapper.findByTestId(TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION);
+ const findNotificationsToggleButton = () =>
+ wrapper.findByTestId(TEST_ID_NOTIFICATIONS_TOGGLE_ACTION);
const findDeleteButton = () => wrapper.findByTestId(TEST_ID_DELETE_ACTION);
+ const findPromoteButton = () => wrapper.findByTestId(TEST_ID_PROMOTE_ACTION);
const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *');
const findDropdownItemsActual = () =>
findDropdownItems().wrappers.map((x) => {
@@ -25,22 +53,49 @@ describe('WorkItemActions component', () => {
text: x.text(),
};
});
+ const findNotificationsToggle = () => wrapper.findComponent(GlToggle);
+
+ const $toast = {
+ show: jest.fn(),
+ hide: jest.fn(),
+ };
+
+ const convertWorkItemMutationSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(convertWorkItemMutationResponse);
+ const convertWorkItemMutationErrorHandler = jest
+ .fn()
+ .mockResolvedValue(convertWorkItemMutationErrorResponse);
+ const typesQuerySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const createComponent = ({
canUpdate = true,
canDelete = true,
isConfidential = false,
+ subscribed = false,
isParentConfidential = false,
+ notificationsMock = [updateWorkItemNotificationsMutation, jest.fn()],
+ convertWorkItemMutationHandler = convertWorkItemMutationSuccessHandler,
+ workItemType = 'Task',
} = {}) => {
+ const handlers = [notificationsMock];
glModalDirective = jest.fn();
+ mockApollo = createMockApollo([
+ ...handlers,
+ [convertWorkItemMutation, convertWorkItemMutationHandler],
+ [projectWorkItemTypesQuery, typesQuerySuccessHandler],
+ ]);
wrapper = shallowMountExtended(WorkItemActions, {
+ isLoggedIn: isLoggedIn(),
+ apolloProvider: mockApollo,
propsData: {
- workItemId: '123',
+ workItemId: 'gid://gitlab/WorkItem/1',
canUpdate,
canDelete,
isConfidential,
+ subscribed,
isParentConfidential,
- workItemType: 'Task',
+ workItemType,
},
directives: {
glModal: {
@@ -49,11 +104,18 @@ describe('WorkItemActions component', () => {
},
},
},
+ provide: {
+ fullPath: 'gitlab-org/gitlab',
+ glFeatures: { workItemsMvc2: true },
+ },
+ mocks: {
+ $toast,
+ },
});
};
- afterEach(() => {
- wrapper.destroy();
+ beforeEach(() => {
+ isLoggedIn.mockReturnValue(true);
});
it('renders modal', () => {
@@ -68,6 +130,13 @@ describe('WorkItemActions component', () => {
expect(findDropdownItemsActual()).toEqual([
{
+ testId: TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
+ text: '',
+ },
+ {
+ divider: true,
+ },
+ {
testId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
text: 'Turn on confidentiality',
},
@@ -137,7 +206,157 @@ describe('WorkItemActions component', () => {
});
expect(findDeleteButton().exists()).toBe(false);
- expect(wrapper.findComponent(GlDropdownDivider).exists()).toBe(false);
+ });
+ });
+
+ describe('notifications action', () => {
+ const errorMessage = 'Failed to subscribe';
+ const id = 'gid://gitlab/WorkItem/1';
+ const notificationToggledOffMessage = 'Notifications turned off.';
+ const notificationToggledOnMessage = 'Notifications turned on.';
+
+ const inputVariablesOff = {
+ id,
+ notificationsWidget: {
+ subscribed: false,
+ },
+ };
+
+ const inputVariablesOn = {
+ id,
+ notificationsWidget: {
+ subscribed: true,
+ },
+ };
+
+ const notificationsOffExpectedResponse = workItemByIidResponseFactory({
+ subscribed: false,
+ });
+
+ const toggleNotificationsOffHandler = jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: notificationsOffExpectedResponse.data.workspace.workItems.nodes[0],
+ errors: [],
+ },
+ },
+ });
+
+ const notificationsOnExpectedResponse = workItemByIidResponseFactory({
+ subscribed: true,
+ });
+
+ const toggleNotificationsOnHandler = jest.fn().mockResolvedValue({
+ data: {
+ workItemUpdate: {
+ workItem: notificationsOnExpectedResponse.data.workspace.workItems.nodes[0],
+ errors: [],
+ },
+ },
+ });
+
+ const toggleNotificationsFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
+
+ const notificationsOffMock = [
+ updateWorkItemNotificationsMutation,
+ toggleNotificationsOffHandler,
+ ];
+
+ const notificationsOnMock = [updateWorkItemNotificationsMutation, toggleNotificationsOnHandler];
+
+ const notificationsFailureMock = [
+ updateWorkItemNotificationsMutation,
+ toggleNotificationsFailureHandler,
+ ];
+
+ beforeEach(() => {
+ createComponent();
+ isLoggedIn.mockReturnValue(true);
+ });
+
+ it('renders toggle button', () => {
+ expect(findNotificationsToggleButton().exists()).toBe(true);
+ });
+
+ it.each`
+ scenario | subscribedToNotifications | notificationsMock | inputVariables | toastMessage
+ ${'turned off'} | ${false} | ${notificationsOffMock} | ${inputVariablesOff} | ${notificationToggledOffMessage}
+ ${'turned on'} | ${true} | ${notificationsOnMock} | ${inputVariablesOn} | ${notificationToggledOnMessage}
+ `(
+ 'calls mutation and displays toast when notification toggle is $scenario',
+ async ({ subscribedToNotifications, notificationsMock, inputVariables, toastMessage }) => {
+ createComponent({ notificationsMock });
+
+ await waitForPromises();
+
+ findNotificationsToggle().vm.$emit('change', subscribedToNotifications);
+
+ await waitForPromises();
+
+ expect(notificationsMock[1]).toHaveBeenCalledWith({
+ input: inputVariables,
+ });
+ expect(toast).toHaveBeenCalledWith(toastMessage);
+ },
+ );
+
+ it('emits error when the update notification mutation fails', async () => {
+ createComponent({ notificationsMock: notificationsFailureMock });
+
+ await waitForPromises();
+
+ findNotificationsToggle().vm.$emit('change', false);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[errorMessage]]);
+ });
+ });
+
+ describe('promote action', () => {
+ it.each`
+ workItemType | show
+ ${'Task'} | ${false}
+ ${'Objective'} | ${false}
+ `('does not show promote button for $workItemType', ({ workItemType, show }) => {
+ createComponent({ workItemType });
+
+ expect(findPromoteButton().exists()).toBe(show);
+ });
+
+ it('promote key result to objective', async () => {
+ createComponent({ workItemType: 'Key Result' });
+
+ // wait for work item types
+ await waitForPromises();
+
+ expect(findPromoteButton().exists()).toBe(true);
+ findPromoteButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(convertWorkItemMutationSuccessHandler).toHaveBeenCalled();
+ expect($toast.show).toHaveBeenCalledWith('Promoted to objective.');
+ });
+
+ it('emits error when promote mutation fails', async () => {
+ createComponent({
+ workItemType: 'Key Result',
+ convertWorkItemMutationHandler: convertWorkItemMutationErrorHandler,
+ });
+
+ // wait for work item types
+ await waitForPromises();
+
+ expect(findPromoteButton().exists()).toBe(true);
+ findPromoteButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(convertWorkItemMutationErrorHandler).toHaveBeenCalled();
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while promoting the key result. Please try again.'],
+ ]);
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index e85f62b881d..25b0b74c217 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -8,9 +8,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
-import { config } from '~/graphql_shared/issuable_client';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import {
@@ -22,7 +20,6 @@ import {
import {
projectMembersResponseWithCurrentUser,
mockAssignees,
- workItemQueryResponse,
currentUserResponse,
currentUserNullResponse,
projectMembersResponseWithoutCurrentUser,
@@ -78,27 +75,16 @@ describe('WorkItemAssignees component', () => {
canInviteMembers = false,
canUpdate = true,
} = {}) => {
- const apolloProvider = createMockApollo(
- [
- [userSearchQuery, searchQueryHandler],
- [currentUserQuery, currentUserQueryHandler],
- [updateWorkItemMutation, updateWorkItemMutationHandler],
- ],
- {},
- {
- typePolicies: config.cacheConfig.typePolicies,
- },
- );
-
- apolloProvider.clients.defaultClient.writeQuery({
- query: workItemQuery,
- variables: {
- id: workItemId,
- },
- data: workItemQueryResponse.data,
- });
+ const apolloProvider = createMockApollo([
+ [userSearchQuery, searchQueryHandler],
+ [currentUserQuery, currentUserQueryHandler],
+ [updateWorkItemMutation, updateWorkItemMutationHandler],
+ ]);
wrapper = mountExtended(WorkItemAssignees, {
+ provide: {
+ fullPath: 'test-project-path',
+ },
propsData: {
assignees,
workItemId,
@@ -106,17 +92,12 @@ describe('WorkItemAssignees component', () => {
workItemType: TASK_TYPE_NAME,
canUpdate,
canInviteMembers,
- fullPath: 'test-project-path',
},
attachTo: document.body,
apolloProvider,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('passes the correct data-user-id attribute', () => {
createComponent();
@@ -322,7 +303,7 @@ describe('WorkItemAssignees component', () => {
return waitForPromises();
});
- it('renders `Assign myself` button', async () => {
+ it('renders `Assign myself` button', () => {
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(true);
});
diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
new file mode 100644
index 00000000000..f87c0e3f357
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
@@ -0,0 +1,170 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import AwardList from '~/vue_shared/components/awards_list.vue';
+import WorkItemAwardEmoji from '~/work_items/components/work_item_award_emoji.vue';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import {
+ EMOJI_ACTION_REMOVE,
+ EMOJI_ACTION_ADD,
+ EMOJI_THUMBSUP,
+ EMOJI_THUMBSDOWN,
+} from '~/work_items/constants';
+
+import {
+ workItemByIidResponseFactory,
+ mockAwardsWidget,
+ updateWorkItemMutationResponseFactory,
+ mockAwardEmojiThumbsUp,
+} from '../mock_data';
+
+jest.mock('~/lib/utils/common_utils');
+Vue.use(VueApollo);
+
+describe('WorkItemAwardEmoji component', () => {
+ let wrapper;
+
+ const errorMessage = 'Failed to update the award';
+
+ const workItemQueryResponse = workItemByIidResponseFactory();
+ const workItemSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponseFactory());
+ const awardEmojiAddSuccessHandler = jest.fn().mockResolvedValue(
+ updateWorkItemMutationResponseFactory({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [mockAwardEmojiThumbsUp],
+ },
+ }),
+ );
+ const awardEmojiRemoveSuccessHandler = jest.fn().mockResolvedValue(
+ updateWorkItemMutationResponseFactory({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [],
+ },
+ }),
+ );
+ const workItemUpdateFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
+ const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0];
+
+ const createComponent = ({
+ mockWorkItemUpdateMutationHandler = [updateWorkItemMutation, workItemSuccessHandler],
+ workItem = mockWorkItem,
+ awardEmoji = { ...mockAwardsWidget, nodes: [] },
+ } = {}) => {
+ wrapper = shallowMount(WorkItemAwardEmoji, {
+ isLoggedIn: isLoggedIn(),
+ apolloProvider: createMockApollo([mockWorkItemUpdateMutationHandler]),
+ propsData: {
+ workItem,
+ awardEmoji,
+ },
+ });
+ };
+
+ const findAwardsList = () => wrapper.findComponent(AwardList);
+
+ beforeEach(() => {
+ isLoggedIn.mockReturnValue(true);
+ window.gon = {
+ current_user_id: 1,
+ };
+
+ createComponent();
+ });
+
+ it('renders the award-list component with default props', () => {
+ expect(findAwardsList().exists()).toBe(true);
+ expect(findAwardsList().props()).toEqual({
+ boundary: '',
+ canAwardEmoji: true,
+ currentUserId: 1,
+ defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN],
+ selectedClass: 'selected',
+ awards: [],
+ });
+ });
+
+ it('renders awards-list component with awards present', () => {
+ createComponent({ awardEmoji: mockAwardsWidget });
+
+ expect(findAwardsList().props('awards')).toEqual([
+ {
+ id: 1,
+ name: EMOJI_THUMBSUP,
+ user: {
+ id: 5,
+ },
+ },
+ {
+ id: 2,
+ name: EMOJI_THUMBSDOWN,
+ user: {
+ id: 5,
+ },
+ },
+ ]);
+ });
+
+ it.each`
+ expectedAssertion | action | successHandler | mockAwardEmojiNodes
+ ${'added'} | ${EMOJI_ACTION_ADD} | ${awardEmojiAddSuccessHandler} | ${[]}
+ ${'removed'} | ${EMOJI_ACTION_REMOVE} | ${awardEmojiRemoveSuccessHandler} | ${[mockAwardEmojiThumbsUp]}
+ `(
+ 'calls mutation when an award emoji is $expectedAssertion',
+ async ({ action, successHandler, mockAwardEmojiNodes }) => {
+ createComponent({
+ mockWorkItemUpdateMutationHandler: [updateWorkItemMutation, successHandler],
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: mockAwardEmojiNodes,
+ },
+ });
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ await waitForPromises();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockWorkItem.id,
+ awardEmojiWidget: {
+ action,
+ name: EMOJI_THUMBSUP,
+ },
+ },
+ });
+ },
+ );
+
+ it('emits error when the update mutation fails', async () => {
+ createComponent({
+ mockWorkItemUpdateMutationHandler: [updateWorkItemMutation, workItemUpdateFailureHandler],
+ });
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[errorMessage]]);
+ });
+
+ describe('when user is not logged in', () => {
+ beforeEach(() => {
+ isLoggedIn.mockReturnValue(false);
+
+ createComponent();
+ });
+
+ it('renders the component with required props and canAwardEmoji false', () => {
+ expect(findAwardsList().props('canAwardEmoji')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js
index fe31c01df36..68ede7d5bc0 100644
--- a/spec/frontend/work_items/components/work_item_created_updated_spec.js
+++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js
@@ -5,14 +5,12 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import { workItemResponseFactory, mockAssignees } from '../mock_data';
+import { workItemByIidResponseFactory, mockAssignees } from '../mock_data';
describe('WorkItemCreatedUpdated component', () => {
let wrapper;
let successHandler;
- let successByIidHandler;
Vue.use(VueApollo);
@@ -21,39 +19,20 @@ describe('WorkItemCreatedUpdated component', () => {
const findCreatedAtText = () => findCreatedAt().text().replace(/\s+/g, ' ');
- const createComponent = async ({
- workItemId = 'gid://gitlab/WorkItem/1',
- workItemIid = '1',
- fetchByIid = false,
- author = null,
- updatedAt,
- } = {}) => {
- const workItemQueryResponse = workItemResponseFactory({
+ const createComponent = async ({ workItemIid = '1', author = null, updatedAt } = {}) => {
+ const workItemQueryResponse = workItemByIidResponseFactory({
author,
updatedAt,
});
- const byIidResponse = {
- data: {
- workspace: {
- id: 'gid://gitlab/Project/1',
- workItems: {
- nodes: [workItemQueryResponse.data.workItem],
- },
- },
- },
- };
successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- successByIidHandler = jest.fn().mockResolvedValue(byIidResponse);
-
- const handlers = [
- [workItemQuery, successHandler],
- [workItemByIidQuery, successByIidHandler],
- ];
wrapper = shallowMount(WorkItemCreatedUpdated, {
- apolloProvider: createMockApollo(handlers),
- propsData: { workItemId, workItemIid, fetchByIid, fullPath: '/some/project' },
+ apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]),
+ provide: {
+ fullPath: '/some/project',
+ },
+ propsData: { workItemIid },
stubs: {
GlAvatarLink,
GlSprintf,
@@ -63,42 +42,34 @@ describe('WorkItemCreatedUpdated component', () => {
await waitForPromises();
};
- describe.each([true, false])('fetchByIid is %s', (fetchByIid) => {
- describe('work item id and iid undefined', () => {
- beforeEach(async () => {
- await createComponent({ workItemId: null, workItemIid: null, fetchByIid });
- });
+ it('skips the work item query when workItemIid is not defined', async () => {
+ await createComponent({ workItemIid: null });
- it('skips the work item query', () => {
- expect(successHandler).not.toHaveBeenCalled();
- expect(successByIidHandler).not.toHaveBeenCalled();
- });
- });
-
- it('shows author name and link', async () => {
- const author = mockAssignees[0];
+ expect(successHandler).not.toHaveBeenCalled();
+ });
- await createComponent({ fetchByIid, author });
+ it('shows author name and link', async () => {
+ const author = mockAssignees[0];
+ await createComponent({ author });
- expect(findCreatedAtText()).toEqual(`Created by ${author.name}`);
- });
+ expect(findCreatedAtText()).toBe(`Created by ${author.name}`);
+ });
- it('shows created time when author is null', async () => {
- await createComponent({ fetchByIid, author: null });
+ it('shows created time when author is null', async () => {
+ await createComponent({ author: null });
- expect(findCreatedAtText()).toEqual('Created');
- });
+ expect(findCreatedAtText()).toBe('Created');
+ });
- it('shows updated time', async () => {
- await createComponent({ fetchByIid });
+ it('shows updated time', async () => {
+ await createComponent();
- expect(findUpdatedAt().exists()).toBe(true);
- });
+ expect(findUpdatedAt().exists()).toBe(true);
+ });
- it('does not show updated time for new work items', async () => {
- await createComponent({ fetchByIid, updatedAt: null });
+ it('does not show updated time for new work items', async () => {
+ await createComponent({ updatedAt: null });
- expect(findUpdatedAt().exists()).toBe(false);
- });
+ expect(findUpdatedAt().exists()).toBe(false);
});
});
diff --git a/spec/frontend/work_items/components/work_item_description_rendered_spec.js b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
index 0ab2546440b..4f1d49ee2e5 100644
--- a/spec/frontend/work_items/components/work_item_description_rendered_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_rendered_spec.js
@@ -29,10 +29,6 @@ describe('WorkItemDescription', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders gfm', async () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index a12ec23c15a..62cbb1bacb6 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -12,17 +12,15 @@ import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
updateWorkItemMutationResponse,
+ workItemByIidResponseFactory,
workItemDescriptionSubscriptionResponse,
- workItemResponseFactory,
workItemQueryResponse,
- projectWorkItemResponse,
} from '../mock_data';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
@@ -37,7 +35,6 @@ describe('WorkItemDescription', () => {
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse);
- const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
let workItemResponseHandler;
let workItemsMvc;
@@ -59,28 +56,25 @@ describe('WorkItemDescription', () => {
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
canUpdate = true,
- workItemResponse = workItemResponseFactory({ canUpdate }),
+ workItemResponse = workItemByIidResponseFactory({ canUpdate }),
isEditing = false,
- queryVariables = { id: workItemId },
- fetchByIid = false,
+ workItemIid = '1',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
const { id } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemDescription, {
apolloProvider: createMockApollo([
- [workItemQuery, workItemResponseHandler],
+ [workItemByIidQuery, workItemResponseHandler],
[updateWorkItemMutation, mutationHandler],
[workItemDescriptionSubscription, subscriptionHandler],
- [workItemByIidQuery, workItemByIidResponseHandler],
]),
propsData: {
workItemId: id,
- fullPath: 'test-project-path',
- queryVariables,
- fetchByIid,
+ workItemIid,
},
provide: {
+ fullPath: 'test-project-path',
glFeatures: {
workItemsMvc,
},
@@ -99,10 +93,6 @@ describe('WorkItemDescription', () => {
}
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('editing description with workItemsMvc FF enabled', () => {
beforeEach(() => {
workItemsMvc = true;
@@ -117,10 +107,10 @@ describe('WorkItemDescription', () => {
await createComponent({ isEditing: true });
expect(findMarkdownEditor().props()).toMatchObject({
- autocompleteDataSources: autocompleteDataSources(fullPath, iid),
supportsQuickActions: true,
renderMarkdownPath: markdownPreviewPath(fullPath, iid),
quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
});
});
});
@@ -156,9 +146,7 @@ describe('WorkItemDescription', () => {
});
it('has a subscription', async () => {
- createComponent();
-
- await waitForPromises();
+ await createComponent();
expect(subscriptionHandler).toHaveBeenCalledWith({
issuableId: workItemQueryResponse.data.workItem.id,
@@ -174,13 +162,10 @@ describe('WorkItemDescription', () => {
};
await createComponent({
- workItemResponse: workItemResponseFactory({
- lastEditedAt,
- lastEditedBy,
- }),
+ workItemResponse: workItemByIidResponseFactory({ lastEditedAt, lastEditedBy }),
});
- expect(findEditedAt().props()).toEqual({
+ expect(findEditedAt().props()).toMatchObject({
updatedAt: lastEditedAt,
updatedByName: lastEditedBy.name,
updatedByPath: lastEditedBy.webPath,
@@ -313,27 +298,10 @@ describe('WorkItemDescription', () => {
});
});
- it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
- createComponent({ fetchByIid: false });
- await waitForPromises();
+ it('calls the work item query', async () => {
+ await createComponent();
expect(workItemResponseHandler).toHaveBeenCalled();
- expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
- });
-
- it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
- createComponent({ fetchByIid: true });
- await waitForPromises();
-
- expect(workItemResponseHandler).not.toHaveBeenCalled();
- expect(workItemByIidResponseHandler).toHaveBeenCalled();
- });
-
- it('skips calling the handlers when missing the needed queryVariables', async () => {
- createComponent({ queryVariables: {}, fetchByIid: false });
- await waitForPromises();
-
- expect(workItemResponseHandler).not.toHaveBeenCalled();
});
},
);
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 938cf6e6f51..e305cc310bd 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -6,21 +6,16 @@ import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
-import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
-import {
- deleteWorkItemFromTaskMutationErrorResponse,
- deleteWorkItemFromTaskMutationResponse,
- deleteWorkItemMutationErrorResponse,
- deleteWorkItemResponse,
-} from '../mock_data';
+import { deleteWorkItemMutationErrorResponse, deleteWorkItemResponse } from '../mock_data';
describe('WorkItemDetailModal component', () => {
let wrapper;
Vue.use(VueApollo);
+ const workItemId = 'gid://gitlab/WorkItem/1';
const hideModal = jest.fn();
const GlModal = {
template: `
@@ -33,37 +28,23 @@ describe('WorkItemDetailModal component', () => {
},
};
- const defaultPropsData = {
- issueGid: 'gid://gitlab/WorkItem/1',
- workItemId: 'gid://gitlab/WorkItem/2',
- };
-
const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.findComponent(GlAlert);
const findWorkItemDetail = () => wrapper.findComponent(WorkItemDetail);
const createComponent = ({
- lockVersion,
- lineNumberStart,
- lineNumberEnd,
error = false,
- deleteWorkItemFromTaskMutationHandler = jest
- .fn()
- .mockResolvedValue(deleteWorkItemFromTaskMutationResponse),
deleteWorkItemMutationHandler = jest.fn().mockResolvedValue(deleteWorkItemResponse),
} = {}) => {
const apolloProvider = createMockApollo([
- [deleteWorkItemFromTaskMutation, deleteWorkItemFromTaskMutationHandler],
[deleteWorkItemMutation, deleteWorkItemMutationHandler],
]);
wrapper = shallowMount(WorkItemDetailModal, {
apolloProvider,
propsData: {
- ...defaultPropsData,
- lockVersion,
- lineNumberStart,
- lineNumberEnd,
+ workItemId,
+ workItemIid: '1',
},
data() {
return {
@@ -82,18 +63,14 @@ describe('WorkItemDetailModal component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders WorkItemDetail', () => {
createComponent();
expect(findWorkItemDetail().props()).toEqual({
isModal: true,
- workItemId: defaultPropsData.workItemId,
- workItemParentId: defaultPropsData.issueGid,
- workItemIid: null,
+ workItemId,
+ workItemIid: '1',
+ workItemParentId: null,
});
});
@@ -147,85 +124,31 @@ describe('WorkItemDetailModal component', () => {
});
describe('delete work item', () => {
- describe('when there is task data', () => {
- it('emits workItemDeleted and closes modal', async () => {
- const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationResponse);
- createComponent({
- lockVersion: 1,
- lineNumberStart: '3',
- lineNumberEnd: '3',
- deleteWorkItemFromTaskMutationHandler: mutationMock,
- });
- const newDesc = 'updated work item desc';
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).toEqual([[newDesc]]);
- expect(hideModal).toHaveBeenCalled();
- expect(mutationMock).toHaveBeenCalledWith({
- input: {
- id: defaultPropsData.issueGid,
- lockVersion: 1,
- taskData: { id: defaultPropsData.workItemId, lineNumberEnd: 3, lineNumberStart: 3 },
- },
- });
- });
-
- it.each`
- errorType | mutationMock | errorMessage
- ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemFromTaskMutationErrorResponse)} | ${'Error'}
- ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
- `(
- 'shows an error message when there is $errorType',
- async ({ mutationMock, errorMessage }) => {
- createComponent({
- lockVersion: 1,
- lineNumberStart: '3',
- lineNumberEnd: '3',
- deleteWorkItemFromTaskMutationHandler: mutationMock,
- });
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
- expect(hideModal).not.toHaveBeenCalled();
- expect(findAlert().text()).toBe(errorMessage);
- },
- );
+ it('emits workItemDeleted and closes modal', async () => {
+ const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemResponse);
+ createComponent({ deleteWorkItemMutationHandler: mutationMock });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toEqual([[workItemId]]);
+ expect(hideModal).toHaveBeenCalled();
+ expect(mutationMock).toHaveBeenCalledWith({ input: { id: workItemId } });
});
- describe('when there is no task data', () => {
- it('emits workItemDeleted and closes modal', async () => {
- const mutationMock = jest.fn().mockResolvedValue(deleteWorkItemResponse);
- createComponent({ deleteWorkItemMutationHandler: mutationMock });
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).toEqual([[defaultPropsData.workItemId]]);
- expect(hideModal).toHaveBeenCalled();
- expect(mutationMock).toHaveBeenCalledWith({ input: { id: defaultPropsData.workItemId } });
- });
-
- it.each`
- errorType | mutationMock | errorMessage
- ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemMutationErrorResponse)} | ${'Error'}
- ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
- `(
- 'shows an error message when there is $errorType',
- async ({ mutationMock, errorMessage }) => {
- createComponent({ deleteWorkItemMutationHandler: mutationMock });
-
- findWorkItemDetail().vm.$emit('deleteWorkItem');
- await waitForPromises();
-
- expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
- expect(hideModal).not.toHaveBeenCalled();
- expect(findAlert().text()).toBe(errorMessage);
- },
- );
+ it.each`
+ errorType | mutationMock | errorMessage
+ ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(deleteWorkItemMutationErrorResponse)} | ${'Error'}
+ ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('GraphQL networkError'))} | ${'GraphQL networkError'}
+ `('shows an error message when there is $errorType', async ({ mutationMock, errorMessage }) => {
+ createComponent({ deleteWorkItemMutationHandler: mutationMock });
+
+ findWorkItemDetail().vm.$emit('deleteWorkItem');
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toBeUndefined();
+ expect(hideModal).not.toHaveBeenCalled();
+ expect(findAlert().text()).toBe(errorMessage);
});
});
});
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 64a7502671e..557ae07969e 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -9,6 +9,7 @@ import {
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -26,39 +27,42 @@ import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
+import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
+
import {
mockParent,
workItemDatesSubscriptionResponse,
- workItemResponseFactory,
+ workItemByIidResponseFactory,
workItemTitleSubscriptionResponse,
workItemAssigneesSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
- projectWorkItemResponse,
objectiveType,
+ mockWorkItemCommentNote,
} from '../mock_data';
+jest.mock('~/lib/utils/common_utils');
+
describe('WorkItemDetail component', () => {
let wrapper;
Vue.use(VueApollo);
- const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
- const workItemQueryResponseWithoutParent = workItemResponseFactory({
+ const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true });
+ const workItemQueryResponseWithoutParent = workItemByIidResponseFactory({
parent: null,
canUpdate: true,
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- const successByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
const milestoneSubscriptionHandler = jest
@@ -68,6 +72,7 @@ describe('WorkItemDetail component', () => {
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
const showModalHandler = jest.fn();
+ const { id } = workItemQueryResponse.data.workspace.workItems.nodes[0];
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -89,32 +94,32 @@ describe('WorkItemDetail component', () => {
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+ const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos);
const createComponent = ({
isModal = false,
updateInProgress = false,
- workItemId = workItemQueryResponse.data.workItem.id,
+ workItemId = id,
workItemIid = '1',
handler = successHandler,
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
- workItemsMvcEnabled = false,
workItemsMvc2Enabled = false,
- fetchByIid = false,
} = {}) => {
const handlers = [
- [workItemQuery, handler],
+ [workItemByIidQuery, handler],
[workItemTitleSubscription, subscriptionHandler],
[workItemDatesSubscription, datesSubscriptionHandler],
[workItemAssigneesSubscription, assigneesSubscriptionHandler],
[workItemMilestoneSubscription, milestoneSubscriptionHandler],
- [workItemByIidQuery, successByIidHandler],
confidentialityMock,
];
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
+ isLoggedIn: isLoggedIn(),
propsData: { isModal, workItemId, workItemIid },
data() {
return {
@@ -124,9 +129,7 @@ describe('WorkItemDetail component', () => {
},
provide: {
glFeatures: {
- workItemsMvc: workItemsMvcEnabled,
workItemsMvc2: workItemsMvc2Enabled,
- useIidInWorkItemsPath: fetchByIid,
},
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
@@ -134,6 +137,7 @@ describe('WorkItemDetail component', () => {
hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
fullPath: 'group/project',
+ reportAbusePath: '/report/abuse/path',
},
stubs: {
WorkItemWeight: true,
@@ -148,8 +152,11 @@ describe('WorkItemDetail component', () => {
});
};
+ beforeEach(() => {
+ isLoggedIn.mockReturnValue(true);
+ });
+
afterEach(() => {
- wrapper.destroy();
setWindowLocation('');
});
@@ -190,6 +197,10 @@ describe('WorkItemDetail component', () => {
it('updates the document title', () => {
expect(document.title).toEqual('Updated title · Task · test-project-path');
});
+
+ it('renders todos widget if logged in', () => {
+ expect(findWorkItemTodos().exists()).toBe(true);
+ });
});
describe('close button', () => {
@@ -224,19 +235,17 @@ describe('WorkItemDetail component', () => {
describe('confidentiality', () => {
const errorMessage = 'Mutation failed';
- const confidentialWorkItem = workItemResponseFactory({
+ const confidentialWorkItem = workItemByIidResponseFactory({
confidential: true,
});
+ const workItem = confidentialWorkItem.data.workspace.workItems.nodes[0];
// Mocks for work item without parent
- const withoutParentExpectedInputVars = {
- id: workItemQueryResponse.data.workItem.id,
- confidential: true,
- };
+ const withoutParentExpectedInputVars = { id, confidential: true };
const toggleConfidentialityWithoutParentHandler = jest.fn().mockResolvedValue({
data: {
workItemUpdate: {
- workItem: confidentialWorkItem.data.workItem,
+ workItem,
errors: [],
},
},
@@ -256,17 +265,17 @@ describe('WorkItemDetail component', () => {
// Mocks for work item with parent
const withParentExpectedInputVars = {
id: mockParent.parent.id,
- taskData: { id: workItemQueryResponse.data.workItem.id, confidential: true },
+ taskData: { id, confidential: true },
};
const toggleConfidentialityWithParentHandler = jest.fn().mockResolvedValue({
data: {
workItemUpdate: {
workItem: {
- id: confidentialWorkItem.data.workItem.id,
- descriptionHtml: confidentialWorkItem.data.workItem.description,
+ id: workItem.id,
+ descriptionHtml: workItem.description,
},
task: {
- workItem: confidentialWorkItem.data.workItem,
+ workItem,
confidential: true,
},
errors: [],
@@ -342,7 +351,7 @@ describe('WorkItemDetail component', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
- it('shows alert message when mutation fails', async () => {
+ it('shows an alert when mutation fails', async () => {
createComponent({
handler: handlerMock,
confidentialityMock: confidentialityFailureMock,
@@ -393,16 +402,17 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(false);
});
- it('shows work item type if there is not a parent', async () => {
+ it('shows work item type with reference when there is no a parent', async () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
expect(findWorkItemType().exists()).toBe(true);
+ expect(findWorkItemType().text()).toBe('Task #1');
});
describe('with parent', () => {
beforeEach(() => {
- const parentResponse = workItemResponseFactory(mockParent);
+ const parentResponse = workItemByIidResponseFactory(mockParent);
createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) });
return waitForPromises();
@@ -412,7 +422,7 @@ describe('WorkItemDetail component', () => {
expect(findParent().exists()).toBe(true);
});
- it('does not show work item type', async () => {
+ it('does not show work item type', () => {
expect(findWorkItemType().exists()).toBe(false);
});
@@ -420,6 +430,12 @@ describe('WorkItemDetail component', () => {
expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
});
+ it('shows parent title and iid', () => {
+ expect(findParentButton().text()).toBe(
+ `${mockParent.parent.title} #${mockParent.parent.iid}`,
+ );
+ });
+
it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => {
expect(findParentButton().attributes().href).toBe('../../issues/5');
});
@@ -435,12 +451,17 @@ describe('WorkItemDetail component', () => {
},
},
};
- const parentResponse = workItemResponseFactory(mockParentObjective);
+ const parentResponse = workItemByIidResponseFactory(mockParentObjective);
createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) });
await waitForPromises();
expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl);
});
+
+ it('shows work item type and iid', () => {
+ const { iid, workItemType } = workItemQueryResponse.data.workspace.workItems.nodes[0];
+ expect(findParent().text()).toContain(`${workItemType.name} #${iid}`);
+ });
});
});
@@ -470,9 +491,7 @@ describe('WorkItemDetail component', () => {
createComponent();
await waitForPromises();
- expect(titleSubscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
- });
+ expect(titleSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
});
describe('assignees subscription', () => {
@@ -481,15 +500,13 @@ describe('WorkItemDetail component', () => {
createComponent();
await waitForPromises();
- expect(assigneesSubscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
- });
+ expect(assigneesSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
});
});
describe('when the assignees widget does not exist', () => {
it('does not call the assignees subscription', async () => {
- const response = workItemResponseFactory({ assigneesWidgetPresent: false });
+ const response = workItemByIidResponseFactory({ assigneesWidgetPresent: false });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@@ -505,15 +522,13 @@ describe('WorkItemDetail component', () => {
createComponent();
await waitForPromises();
- expect(datesSubscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
- });
+ expect(datesSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
});
});
describe('when the due date widget does not exist', () => {
it('does not call the dates subscription', async () => {
- const response = workItemResponseFactory({ datesWidgetPresent: false });
+ const response = workItemByIidResponseFactory({ datesWidgetPresent: false });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@@ -536,7 +551,7 @@ describe('WorkItemDetail component', () => {
createComponent({
handler: jest
.fn()
- .mockResolvedValue(workItemResponseFactory({ assigneesWidgetPresent: false })),
+ .mockResolvedValue(workItemByIidResponseFactory({ assigneesWidgetPresent: false })),
});
await waitForPromises();
@@ -550,7 +565,7 @@ describe('WorkItemDetail component', () => {
${'renders when widget is returned from API'} | ${true} | ${true}
${'does not render when widget is not returned from API'} | ${false} | ${false}
`('$description', async ({ labelsWidgetPresent, exists }) => {
- const response = workItemResponseFactory({ labelsWidgetPresent });
+ const response = workItemByIidResponseFactory({ labelsWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@@ -566,7 +581,7 @@ describe('WorkItemDetail component', () => {
${'when widget is not returned from API'} | ${false} | ${false}
`('$description', ({ datesWidgetPresent, exists }) => {
it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
- const response = workItemResponseFactory({ datesWidgetPresent });
+ const response = workItemByIidResponseFactory({ datesWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@@ -593,7 +608,7 @@ describe('WorkItemDetail component', () => {
${'renders when widget is returned from API'} | ${true} | ${true}
${'does not render when widget is not returned from API'} | ${false} | ${false}
`('$description', async ({ milestoneWidgetPresent, exists }) => {
- const response = workItemResponseFactory({ milestoneWidgetPresent });
+ const response = workItemByIidResponseFactory({ milestoneWidgetPresent });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@@ -607,15 +622,13 @@ describe('WorkItemDetail component', () => {
createComponent();
await waitForPromises();
- expect(milestoneSubscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
- });
+ expect(milestoneSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
});
});
describe('when the assignees widget does not exist', () => {
it('does not call the milestone subscription', async () => {
- const response = workItemResponseFactory({ milestoneWidgetPresent: false });
+ const response = workItemByIidResponseFactory({ milestoneWidgetPresent: false });
const handler = jest.fn().mockResolvedValue(response);
createComponent({ handler });
await waitForPromises();
@@ -626,50 +639,25 @@ describe('WorkItemDetail component', () => {
});
});
- it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is false', async () => {
+ it('calls the work item query', async () => {
createComponent();
await waitForPromises();
- expect(successHandler).toHaveBeenCalledWith({
- id: workItemQueryResponse.data.workItem.id,
- });
- expect(successByIidHandler).not.toHaveBeenCalled();
- });
-
- it('calls the global ID work item query when `useIidInWorkItemsPath` feature flag is true but there is no `iid_path` parameter in URL', async () => {
- createComponent({ fetchByIid: true });
- await waitForPromises();
-
- expect(successHandler).toHaveBeenCalledWith({
- id: workItemQueryResponse.data.workItem.id,
- });
- expect(successByIidHandler).not.toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
});
- it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present', async () => {
- setWindowLocation(`?iid_path=true`);
-
- createComponent({ fetchByIid: true, iidPathQueryParam: 'true' });
+ it('skips the work item query when there is no workItemIid', async () => {
+ createComponent({ workItemIid: null });
await waitForPromises();
expect(successHandler).not.toHaveBeenCalled();
- expect(successByIidHandler).toHaveBeenCalledWith({
- fullPath: 'group/project',
- iid: '1',
- });
});
- it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present and is a modal', async () => {
- setWindowLocation(`?iid_path=true`);
-
- createComponent({ fetchByIid: true, iidPathQueryParam: 'true', isModal: true });
+ it('calls the work item query when isModal=true', async () => {
+ createComponent({ isModal: true });
await waitForPromises();
- expect(successHandler).not.toHaveBeenCalled();
- expect(successByIidHandler).toHaveBeenCalledWith({
- fullPath: 'group/project',
- iid: '1',
- });
+ expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
});
describe('hierarchy widget', () => {
@@ -681,7 +669,7 @@ describe('WorkItemDetail component', () => {
});
describe('work item has children', () => {
- const objectiveWorkItem = workItemResponseFactory({
+ const objectiveWorkItem = workItemByIidResponseFactory({
workItemType: objectiveType,
confidential: true,
});
@@ -709,7 +697,10 @@ describe('WorkItemDetail component', () => {
preventDefault: jest.fn(),
};
- findHierarchyTree().vm.$emit('show-modal', event, { id: 'childWorkItemId' });
+ findHierarchyTree().vm.$emit('show-modal', {
+ event,
+ modalWorkItem: { id: 'childWorkItemId' },
+ });
await waitForPromises();
expect(wrapper.findComponent(WorkItemDetailModal).props().workItemId).toBe(
@@ -738,7 +729,10 @@ describe('WorkItemDetail component', () => {
preventDefault: jest.fn(),
};
- findHierarchyTree().vm.$emit('show-modal', event, { id: 'childWorkItemId' });
+ findHierarchyTree().vm.$emit('show-modal', {
+ event,
+ modalWorkItem: { id: 'childWorkItemId' },
+ });
await waitForPromises();
expect(wrapper.emitted('update-modal')).toBeDefined();
@@ -748,21 +742,10 @@ describe('WorkItemDetail component', () => {
});
describe('notes widget', () => {
- it('does not render notes by default', async () => {
+ it('renders notes by default', async () => {
createComponent();
await waitForPromises();
- expect(findNotesWidget().exists()).toBe(false);
- });
-
- it('renders notes when the work_items_mvc flag is on', async () => {
- const notesWorkItem = workItemResponseFactory({
- notesWidgetPresent: true,
- });
- const handler = jest.fn().mockResolvedValue(notesWorkItem);
- createComponent({ workItemsMvcEnabled: true, handler });
- await waitForPromises();
-
expect(findNotesWidget().exists()).toBe(true);
});
});
@@ -773,4 +756,42 @@ describe('WorkItemDetail component', () => {
expect(findCreatedUpdated().exists()).toBe(true);
});
+
+ describe('abuse category selector', () => {
+ beforeEach(async () => {
+ setWindowLocation('?work_item_id=2');
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('should not be visible by default', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+
+ it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
+ findModal().vm.$emit('openReportAbuse', mockWorkItemCommentNote);
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+
+ findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+ });
+
+ describe('todos widget', () => {
+ beforeEach(async () => {
+ isLoggedIn.mockReturnValue(false);
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('does not renders if not logged in', () => {
+ expect(findWorkItemTodos().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_due_date_spec.js b/spec/frontend/work_items/components/work_item_due_date_spec.js
index 7ebaf8209c7..b4811db8bed 100644
--- a/spec/frontend/work_items/components/work_item_due_date_spec.js
+++ b/spec/frontend/work_items/components/work_item_due_date_spec.js
@@ -46,10 +46,6 @@ describe('WorkItemDueDate component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('when can update', () => {
describe('start date', () => {
describe('`Add start date` button', () => {
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 0b6ab5c3290..554c9a4f7b8 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -6,7 +6,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
@@ -15,11 +14,9 @@ import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constan
import {
projectLabelsResponse,
mockLabels,
- workItemQueryResponse,
- workItemResponseFactory,
+ workItemByIidResponseFactory,
updateWorkItemMutationResponse,
workItemLabelsSubscriptionResponse,
- projectWorkItemResponse,
} from '../mock_data';
Vue.use(VueApollo);
@@ -34,8 +31,9 @@ describe('WorkItemLabels component', () => {
const findEmptyState = () => wrapper.findByTestId('empty-state');
const findLabelsTitle = () => wrapper.findByTestId('labels-title');
- const workItemQuerySuccess = jest.fn().mockResolvedValue(workItemQueryResponse);
- const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
+ const workItemQuerySuccess = jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ labels: null }));
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
@@ -48,34 +46,27 @@ describe('WorkItemLabels component', () => {
workItemQueryHandler = workItemQuerySuccess,
searchQueryHandler = successSearchQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
- fetchByIid = false,
- queryVariables = { id: workItemId },
+ workItemIid = '1',
} = {}) => {
- const apolloProvider = createMockApollo([
- [workItemQuery, workItemQueryHandler],
- [labelSearchQuery, searchQueryHandler],
- [updateWorkItemMutation, updateWorkItemMutationHandler],
- [workItemLabelsSubscription, subscriptionHandler],
- [workItemByIidQuery, workItemByIidResponseHandler],
- ]);
-
wrapper = mountExtended(WorkItemLabels, {
+ apolloProvider: createMockApollo([
+ [workItemByIidQuery, workItemQueryHandler],
+ [labelSearchQuery, searchQueryHandler],
+ [updateWorkItemMutation, updateWorkItemMutationHandler],
+ [workItemLabelsSubscription, subscriptionHandler],
+ ]),
+ provide: {
+ fullPath: 'test-project-path',
+ },
propsData: {
workItemId,
+ workItemIid,
canUpdate,
- fullPath: 'test-project-path',
- queryVariables,
- fetchByIid,
},
attachTo: document.body,
- apolloProvider,
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('has a label', () => {
createComponent();
@@ -190,7 +181,7 @@ describe('WorkItemLabels component', () => {
});
it('adds new labels to the end', async () => {
- const response = workItemResponseFactory({ labels: [mockLabels[1]] });
+ const response = workItemByIidResponseFactory({ labels: [mockLabels[1]] });
const workItemQueryHandler = jest.fn().mockResolvedValue(response);
createComponent({
workItemQueryHandler,
@@ -267,24 +258,15 @@ describe('WorkItemLabels component', () => {
});
});
- it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
- createComponent({ fetchByIid: false });
+ it('calls the work item query', async () => {
+ createComponent();
await waitForPromises();
expect(workItemQuerySuccess).toHaveBeenCalled();
- expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
- });
-
- it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
- createComponent({ fetchByIid: true });
- await waitForPromises();
-
- expect(workItemQuerySuccess).not.toHaveBeenCalled();
- expect(workItemByIidResponseHandler).toHaveBeenCalled();
});
- it('skips calling the handlers when missing the needed queryVariables', async () => {
- createComponent({ queryVariables: {}, fetchByIid: false });
+ it('skips calling the work item query when missing workItemIid', async () => {
+ createComponent({ workItemIid: null });
await waitForPromises();
expect(workItemQuerySuccess).not.toHaveBeenCalled();
diff --git a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
index 5fbd8e7e1a7..688dccbda79 100644
--- a/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/okr_actions_split_button_spec.js
@@ -15,10 +15,6 @@ describe('RelatedItemsTree', () => {
wrapper = createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('OkrActionsSplitButton', () => {
describe('template', () => {
it('renders objective and key results sections', () => {
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
new file mode 100644
index 00000000000..b06be6c8083
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
@@ -0,0 +1,98 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
+import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+import { childrenWorkItems, workItemByIidResponseFactory } from '../../mock_data';
+
+describe('WorkItemChildrenWrapper', () => {
+ let wrapper;
+
+ const getWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
+
+ const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
+
+ Vue.use(VueApollo);
+
+ const createComponent = ({
+ workItemType = 'Objective',
+ confidential = false,
+ children = childrenWorkItems,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemChildrenWrapper, {
+ apolloProvider: createMockApollo([[workItemByIidQuery, getWorkItemQueryHandler]]),
+ provide: {
+ fullPath: 'test/project',
+ },
+ propsData: {
+ workItemType,
+ workItemId: 'gid://gitlab/WorkItem/515',
+ confidential,
+ children,
+ fetchByIid: true,
+ },
+ });
+ };
+
+ it('renders all hierarchy widget children', () => {
+ createComponent();
+
+ const workItemLinkChildren = findWorkItemLinkChildItems();
+ expect(workItemLinkChildren).toHaveLength(4);
+ expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe(
+ childrenWorkItems[0].confidential,
+ );
+ });
+
+ it('remove event on child triggers `removeChild` event', () => {
+ createComponent();
+ const workItem = { id: 'gid://gitlab/WorkItem/2' };
+ const firstChild = findWorkItemLinkChildItems().at(0);
+
+ firstChild.vm.$emit('removeChild', workItem);
+
+ expect(wrapper.emitted('removeChild')).toEqual([[workItem]]);
+ });
+
+ it('emits `show-modal` on `click` event', () => {
+ createComponent();
+ const firstChild = findWorkItemLinkChildItems().at(0);
+ const event = {
+ childItem: 'gid://gitlab/WorkItem/2',
+ };
+
+ firstChild.vm.$emit('click', event);
+
+ expect(wrapper.emitted('show-modal')).toEqual([[{ event, child: event.childItem }]]);
+ });
+
+ it.each`
+ description | workItemType | prefetch
+ ${'prefetches'} | ${'Issue'} | ${true}
+ ${'does not prefetch'} | ${'Objective'} | ${false}
+ `(
+ '$description work-item-link-child on mouseover when workItemType is "$workItemType"',
+ async ({ workItemType, prefetch }) => {
+ createComponent({ workItemType });
+ const firstChild = findWorkItemLinkChildItems().at(0);
+ firstChild.vm.$emit('mouseover', childrenWorkItems[0]);
+ await nextTick();
+ await waitForPromises();
+
+ jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+
+ if (prefetch) {
+ expect(getWorkItemQueryHandler).toHaveBeenCalled();
+ } else {
+ expect(getWorkItemQueryHandler).not.toHaveBeenCalled();
+ }
+ },
+ );
+});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
index e693ccfb156..07efb1c5ac8 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
@@ -1,4 +1,4 @@
-import { GlLabel, GlAvatarsInline } from '@gitlab/ui';
+import { GlAvatarsInline } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -8,10 +8,9 @@ import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/w
import { workItemObjectiveMetadataWidgets } from '../../mock_data';
describe('WorkItemLinkChildMetadata', () => {
- const { MILESTONE, ASSIGNEES, LABELS } = workItemObjectiveMetadataWidgets;
+ const { MILESTONE, ASSIGNEES } = workItemObjectiveMetadataWidgets;
const mockMilestone = MILESTONE.milestone;
const mockAssignees = ASSIGNEES.assignees.nodes;
- const mockLabels = LABELS.labels.nodes;
let wrapper;
const createComponent = ({ metadataWidgets = workItemObjectiveMetadataWidgets } = {}) => {
@@ -53,18 +52,4 @@ describe('WorkItemLinkChildMetadata', () => {
badgeSrOnlyText: '',
});
});
-
- it('renders labels', () => {
- const labels = wrapper.findAllComponents(GlLabel);
- const mockLabel = mockLabels[0];
-
- expect(labels).toHaveLength(mockLabels.length);
- expect(labels.at(0).props()).toMatchObject({
- title: mockLabel.title,
- backgroundColor: mockLabel.color,
- description: mockLabel.description,
- scoped: false,
- });
- expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped
- });
});
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 0470249d7ce..71d1a0e253f 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
@@ -1,4 +1,4 @@
-import { GlIcon } from '@gitlab/ui';
+import { GlLabel, GlIcon } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -7,10 +7,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
-import { createAlert } from '~/flash';
+import { createAlert } from '~/alert';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
@@ -29,19 +30,28 @@ import {
workItemHierarchyTreeResponse,
workItemHierarchyTreeFailureResponse,
workItemObjectiveMetadataWidgets,
+ changeIndirectWorkItemParentMutationResponse,
+ workItemUpdateFailureResponse,
} from '../../mock_data';
-jest.mock('~/flash');
+jest.mock('~/alert');
describe('WorkItemLinkChild', () => {
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
let wrapper;
let getWorkItemTreeQueryHandler;
+ let mutationChangeParentHandler;
+ const { LABELS } = workItemObjectiveMetadataWidgets;
+ const mockLabels = LABELS.labels.nodes;
+
+ const $toast = {
+ show: jest.fn(),
+ hide: jest.fn(),
+ };
Vue.use(VueApollo);
const createComponent = ({
- projectPath = 'gitlab-org/gitlab-test',
canUpdate = true,
issuableGid = WORK_ITEM_ID,
childItem = workItemTask,
@@ -49,17 +59,29 @@ describe('WorkItemLinkChild', () => {
apolloProvider = null,
} = {}) => {
getWorkItemTreeQueryHandler = jest.fn().mockResolvedValue(workItemHierarchyTreeResponse);
+ mutationChangeParentHandler = jest
+ .fn()
+ .mockResolvedValue(changeIndirectWorkItemParentMutationResponse);
wrapper = shallowMountExtended(WorkItemLinkChild, {
apolloProvider:
- apolloProvider || createMockApollo([[getWorkItemTreeQuery, getWorkItemTreeQueryHandler]]),
+ apolloProvider ||
+ createMockApollo([
+ [getWorkItemTreeQuery, getWorkItemTreeQueryHandler],
+ [updateWorkItemMutation, mutationChangeParentHandler],
+ ]),
+ provide: {
+ fullPath: 'gitlab-org/gitlab-test',
+ },
propsData: {
- projectPath,
canUpdate,
issuableGid,
childItem,
workItemType,
},
+ mocks: {
+ $toast,
+ },
});
};
@@ -67,10 +89,6 @@ describe('WorkItemLinkChild', () => {
createAlert.mockClear();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it.each`
status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
@@ -113,6 +131,22 @@ describe('WorkItemLinkChild', () => {
expect(titleEl.text()).toBe(workItemTask.title);
});
+ describe('renders item title correctly for relative instance', () => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: '/test' };
+ createComponent();
+ titleEl = wrapper.findByTestId('item-title');
+ });
+
+ it('renders item title with correct href', () => {
+ expect(titleEl.attributes('href')).toBe('/test/gitlab-org/gitlab-test/-/work_items/4');
+ });
+
+ it('renders item title with correct text', () => {
+ expect(titleEl.text()).toBe(workItemTask.title);
+ });
+ });
+
it.each`
action | event | emittedEvent
${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
@@ -159,6 +193,20 @@ describe('WorkItemLinkChild', () => {
expect(findMetadataComponent().exists()).toBe(false);
});
+
+ it('renders labels', () => {
+ const labels = wrapper.findAllComponents(GlLabel);
+ const mockLabel = mockLabels[0];
+
+ expect(labels).toHaveLength(mockLabels.length);
+ expect(labels.at(0).props()).toMatchObject({
+ title: mockLabel.title,
+ backgroundColor: mockLabel.color,
+ description: mockLabel.description,
+ scoped: false,
+ });
+ expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped
+ });
});
describe('item menu', () => {
@@ -188,7 +236,7 @@ describe('WorkItemLinkChild', () => {
it('removeChild event on menu triggers `click-remove-child` event', () => {
itemMenuEl.vm.$emit('removeChild');
- expect(wrapper.emitted('removeChild')).toEqual([[workItemTask.id]]);
+ expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
});
});
@@ -196,6 +244,13 @@ describe('WorkItemLinkChild', () => {
const findExpandButton = () => wrapper.findByTestId('expand-child');
const findTreeChildren = () => wrapper.findComponent(WorkItemTreeChildren);
+ const getWidgetHierarchy = () =>
+ workItemHierarchyTreeResponse.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+ const getChildrenNodes = () => getWidgetHierarchy().children.nodes;
+ const findFirstItem = () => getChildrenNodes()[0];
+
beforeEach(() => {
getWorkItemTreeQueryHandler.mockClear();
createComponent({
@@ -218,10 +273,8 @@ describe('WorkItemLinkChild', () => {
expect(getWorkItemTreeQueryHandler).toHaveBeenCalled();
expect(findTreeChildren().exists()).toBe(true);
- const widgetHierarchy = workItemHierarchyTreeResponse.data.workItem.widgets.find(
- (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
- );
- expect(findTreeChildren().props('children')).toEqual(widgetHierarchy.children.nodes);
+ const childrenNodes = getChildrenNodes();
+ expect(findTreeChildren().props('children')).toEqual(childrenNodes);
});
it('does not fetch children if already fetched once while clicking expand button', async () => {
@@ -270,5 +323,74 @@ describe('WorkItemLinkChild', () => {
expect(wrapper.emitted('click')).toEqual([['event']]);
});
+
+ it('shows toast on removing child item', async () => {
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ findTreeChildren().vm.$emit('removeChild', findFirstItem());
+ await waitForPromises();
+
+ expect($toast.show).toHaveBeenCalledWith('Child removed', {
+ action: { onClick: expect.any(Function), text: 'Undo' },
+ });
+ });
+
+ it('renders correct number of children after the removal', async () => {
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ const childrenNodes = getChildrenNodes();
+ expect(findTreeChildren().props('children')).toEqual(childrenNodes);
+
+ findTreeChildren().vm.$emit('removeChild', findFirstItem());
+ await waitForPromises();
+
+ expect(findTreeChildren().props('children')).toEqual([]);
+ });
+
+ it('calls correct mutation with correct variables', async () => {
+ const firstItem = findFirstItem();
+
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ findTreeChildren().vm.$emit('removeChild', firstItem);
+
+ expect(mutationChangeParentHandler).toHaveBeenCalledWith({
+ input: {
+ id: firstItem.id,
+ hierarchyWidget: {
+ parentId: null,
+ },
+ },
+ });
+ });
+
+ it('shows the alert when workItem update fails', async () => {
+ mutationChangeParentHandler = jest.fn().mockRejectedValue(workItemUpdateFailureResponse);
+ const apolloProvider = createMockApollo([
+ [getWorkItemTreeQuery, getWorkItemTreeQueryHandler],
+ [updateWorkItemMutation, mutationChangeParentHandler],
+ ]);
+
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ apolloProvider,
+ });
+
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ findTreeChildren().vm.$emit('removeChild', findFirstItem());
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Object),
+ message: 'Something went wrong while removing child.',
+ });
+ });
});
});
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 480f8fbcc58..5f7f56d7063 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
@@ -61,7 +61,7 @@ describe('WorkItemLinksForm', () => {
formType,
},
provide: {
- projectPath: 'project/path',
+ fullPath: 'project/path',
hasIterationsFeature,
},
});
@@ -75,10 +75,6 @@ describe('WorkItemLinksForm', () => {
const findConfidentialCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('creating a new work item', () => {
beforeEach(async () => {
await createComponent();
@@ -154,7 +150,7 @@ describe('WorkItemLinksForm', () => {
const confidentialCheckbox = findConfidentialCheckbox();
const confidentialTooltip = wrapper.findComponent(GlTooltip);
- expect(confidentialCheckbox.attributes('disabled')).toBe('true');
+ expect(confidentialCheckbox.attributes('disabled')).toBeDefined();
expect(confidentialCheckbox.attributes('checked')).toBe('true');
expect(confidentialTooltip.exists()).toBe(true);
expect(confidentialTooltip.text()).toBe(
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
index e3f3b74f296..f02a9fbd021 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
@@ -13,14 +13,10 @@ describe('WorkItemLinksMenu', () => {
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findRemoveDropdownItem = () => wrapper.findComponent(GlDropdownItem);
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders dropdown and dropdown items', () => {
expect(findDropdown().exists()).toBe(true);
expect(findRemoveDropdownItem().exists()).toBe(true);
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index ec51f92b578..786f8604039 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -5,17 +5,16 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { resolvers } from '~/graphql_shared/issuable_client';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
-import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
+import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import { FORM_TYPES } from '~/work_items/constants';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
-import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
getIssueDetailsResponse,
@@ -23,8 +22,10 @@ import {
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
changeWorkItemParentMutationResponse,
+ workItemByIidResponseFactory,
workItemQueryResponse,
- projectWorkItemResponse,
+ mockWorkItemCommentNote,
+ childrenWorkItems,
} from '../../mock_data';
Vue.use(VueApollo);
@@ -44,23 +45,23 @@ describe('WorkItemLinks', () => {
const mutationChangeParentHandler = jest
.fn()
.mockResolvedValue(changeWorkItemParentMutationResponse);
-
- const childWorkItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- const childWorkItemByIidHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
+ const childWorkItemByIidHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
+ const responseWithAddChildPermission = jest.fn().mockResolvedValue(workItemHierarchyResponse);
+ const responseWithoutAddChildPermission = jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ adminParentLink: false }));
const createComponent = async ({
data = {},
- fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
+ fetchHandler = responseWithAddChildPermission,
mutationHandler = mutationChangeParentHandler,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
- fetchByIid = false,
} = {}) => {
mockApollo = createMockApollo(
[
- [getWorkItemLinksQuery, fetchHandler],
+ [workItemQuery, fetchHandler],
[changeWorkItemParentMutation, mutationHandler],
- [workItemQuery, childWorkItemQueryHandler],
[issueDetailsQuery, issueDetailsQueryHandler],
[workItemByIidQuery, childWorkItemByIidHandler],
],
@@ -75,12 +76,9 @@ describe('WorkItemLinks', () => {
};
},
provide: {
- projectPath: 'project/path',
- iid: '1',
+ fullPath: 'project/path',
hasIterationsFeature,
- glFeatures: {
- useIidInWorkItemsPath: fetchByIid,
- },
+ reportAbusePath: '/report/abuse/path',
},
propsData: { issuableId: 1 },
apolloProvider: mockApollo,
@@ -106,16 +104,31 @@ describe('WorkItemLinks', () => {
const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
const findToggleCreateFormButton = () => wrapper.findByTestId('toggle-create-form');
- const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
- const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0);
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
const findChildrenCount = () => wrapper.findByTestId('children-count');
+ const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+ const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
afterEach(() => {
mockApollo = null;
setWindowLocation('');
});
+ it.each`
+ expectedAssertion | workItemFetchHandler | value
+ ${'renders'} | ${responseWithAddChildPermission} | ${true}
+ ${'does not render'} | ${responseWithoutAddChildPermission} | ${false}
+ `(
+ '$expectedAssertion "Add" button in hierarchy widget header when "userPermissions.adminParentLink" is $value',
+ async ({ workItemFetchHandler, value }) => {
+ createComponent({ fetchHandler: workItemFetchHandler });
+ await waitForPromises();
+
+ expect(findToggleFormDropdown().exists()).toBe(value);
+ },
+ );
+
describe('add link form', () => {
it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => {
await createComponent();
@@ -157,12 +170,12 @@ describe('WorkItemLinks', () => {
findToggleCreateFormButton().vm.$emit('click');
await nextTick();
- expect(findWorkItemLinkChildItems()).toHaveLength(4);
+ expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
findAddLinksForm().vm.$emit('addWorkItemChild', workItem);
await waitForPromises();
- expect(findWorkItemLinkChildItems()).toHaveLength(5);
+ expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(5);
});
});
@@ -178,13 +191,14 @@ describe('WorkItemLinks', () => {
});
});
- it('renders all hierarchy widget children', async () => {
+ it('renders hierarchy widget children container', async () => {
await createComponent();
- expect(findWorkItemLinkChildItems()).toHaveLength(4);
+ expect(findWorkItemLinkChildrenWrapper().exists()).toBe(true);
+ expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
});
- it('shows alert when list loading fails', async () => {
+ it('shows an alert when list loading fails', async () => {
const errorMessage = 'Some error';
await createComponent({
fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
@@ -212,7 +226,7 @@ describe('WorkItemLinks', () => {
});
it('does not display link menu on children', () => {
- expect(findWorkItemLinkChildItems().at(0).props('canUpdate')).toBe(false);
+ expect(findWorkItemLinkChildrenWrapper().props('canUpdate')).toBe(false);
});
});
@@ -222,11 +236,11 @@ describe('WorkItemLinks', () => {
beforeEach(async () => {
await createComponent({ mutationHandler: mutationChangeParentHandler });
- firstChild = findFirstWorkItemLinkChild();
+ [firstChild] = childrenWorkItems;
});
it('calls correct mutation with correct variables', async () => {
- firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
+ findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
await waitForPromises();
@@ -241,7 +255,7 @@ describe('WorkItemLinks', () => {
});
it('shows toast when mutation succeeds', async () => {
- firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
+ findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
await waitForPromises();
@@ -251,12 +265,12 @@ describe('WorkItemLinks', () => {
});
it('renders correct number of children after removal', async () => {
- expect(findWorkItemLinkChildItems()).toHaveLength(4);
+ expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
- firstChild.vm.$emit('removeChild', firstChild.vm.childItem.id);
+ findWorkItemLinkChildrenWrapper().vm.$emit('removeChild', firstChild);
await waitForPromises();
- expect(findWorkItemLinkChildItems()).toHaveLength(3);
+ expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(3);
});
});
@@ -275,144 +289,54 @@ describe('WorkItemLinks', () => {
});
});
- describe('when work item is fetched by id', () => {
- describe('prefetching child items', () => {
- let firstChild;
-
- beforeEach(async () => {
- await createComponent();
-
- firstChild = findFirstWorkItemLinkChild();
- });
-
- it('does not fetch the child work item by id before hovering work item links', () => {
- expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
- });
-
- it('fetches the child work item by id if link is hovered for 250+ ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- await waitForPromises();
-
- expect(childWorkItemQueryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/2',
- });
- });
-
- it('does not fetch the child work item by id if link is hovered for less than 250 ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(200);
- firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
- await waitForPromises();
-
- expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
- });
-
- it('does not fetch work item by iid if link is hovered for 250+ ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- await waitForPromises();
+ it('starts prefetching work item by iid if URL contains work_item_iid query parameter', async () => {
+ setWindowLocation('?work_item_iid=5');
+ await createComponent();
- expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
- });
+ expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
+ iid: '5',
+ fullPath: 'project/path',
});
+ });
- it('starts prefetching work item by id if URL contains work item id', async () => {
- setWindowLocation('?work_item_id=5');
- await createComponent();
+ it('does not open the modal if work item iid URL parameter is not found in child items', async () => {
+ setWindowLocation('?work_item_iid=555');
+ await createComponent();
- expect(childWorkItemQueryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/5',
- });
- });
+ expect(showModal).not.toHaveBeenCalled();
+ expect(findWorkItemDetailModal().props('workItemIid')).toBe(null);
+ });
- it('does not open the modal if work item id URL parameter is not found in child items', async () => {
- setWindowLocation('?work_item_id=555');
- await createComponent();
+ it('opens the modal if work item iid URL parameter is found in child items', async () => {
+ setWindowLocation('?work_item_iid=2');
+ await createComponent();
- expect(showModal).not.toHaveBeenCalled();
- expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe(null);
- });
+ expect(showModal).toHaveBeenCalled();
+ expect(findWorkItemDetailModal().props('workItemIid')).toBe('2');
+ });
- it('opens the modal if work item id URL parameter is found in child items', async () => {
+ describe('abuse category selector', () => {
+ beforeEach(async () => {
setWindowLocation('?work_item_id=2');
await createComponent();
-
- expect(showModal).toHaveBeenCalled();
- expect(wrapper.findComponent(WorkItemDetailModal).props('workItemId')).toBe(
- 'gid://gitlab/WorkItem/2',
- );
});
- });
-
- describe('when work item is fetched by iid', () => {
- describe('prefetching child items', () => {
- let firstChild;
-
- beforeEach(async () => {
- setWindowLocation('?iid_path=true');
- await createComponent({ fetchByIid: true });
-
- firstChild = findFirstWorkItemLinkChild();
- });
-
- it('does not fetch the child work item by iid before hovering work item links', () => {
- expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
- });
-
- it('fetches the child work item by iid if link is hovered for 250+ ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- await waitForPromises();
-
- expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
- fullPath: 'project/path',
- iid: '2',
- });
- });
-
- it('does not fetch the child work item by iid if link is hovered for less than 250 ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(200);
- firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
- await waitForPromises();
- expect(childWorkItemByIidHandler).not.toHaveBeenCalled();
- });
-
- it('does not fetch work item by id if link is hovered for 250+ ms', async () => {
- firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- await waitForPromises();
-
- expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
- });
+ it('should not be visible by default', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(false);
});
- it('starts prefetching work item by iid if URL contains work item id', async () => {
- setWindowLocation('?work_item_iid=5&iid_path=true');
- await createComponent({ fetchByIid: true });
+ it('should be visible when the work item modal emits `openReportAbuse` event', async () => {
+ findWorkItemDetailModal().vm.$emit('openReportAbuse', mockWorkItemCommentNote);
- expect(childWorkItemByIidHandler).toHaveBeenCalledWith({
- iid: '5',
- fullPath: 'project/path',
- });
- });
- });
+ await nextTick();
- it('does not open the modal if work item iid URL parameter is not found in child items', async () => {
- setWindowLocation('?work_item_iid=555&iid_path=true');
- await createComponent({ fetchByIid: true });
+ expect(findAbuseCategorySelector().exists()).toBe(true);
- expect(showModal).not.toHaveBeenCalled();
- expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe(null);
- });
+ findAbuseCategorySelector().vm.$emit('close-drawer');
- it('opens the modal if work item iid URL parameter is found in child items', async () => {
- setWindowLocation('?work_item_iid=2&iid_path=true');
- await createComponent({ fetchByIid: true });
+ await nextTick();
- expect(showModal).toHaveBeenCalled();
- expect(wrapper.findComponent(WorkItemDetailModal).props('workItemIid')).toBe('2');
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index 0236fe2e60d..06716584879 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -1,65 +1,44 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
+import { nextTick } from 'vue';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
+import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
-import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
} from '~/work_items/constants';
-import { childrenWorkItems, workItemObjectiveWithChild } from '../../mock_data';
+import { childrenWorkItems } from '../../mock_data';
describe('WorkItemTree', () => {
- let getWorkItemQueryHandler;
let wrapper;
const findEmptyState = () => wrapper.findByTestId('tree-empty');
const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton);
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
- const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
-
- Vue.use(VueApollo);
+ const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper);
const createComponent = ({
workItemType = 'Objective',
parentWorkItemType = 'Objective',
confidential = false,
children = childrenWorkItems,
- apolloProvider = null,
+ canUpdate = true,
} = {}) => {
- const mockWorkItemResponse = {
- data: {
- workItem: {
- ...workItemObjectiveWithChild,
- workItemType: {
- ...workItemObjectiveWithChild.workItemType,
- name: workItemType,
- },
- },
- },
- };
- getWorkItemQueryHandler = jest.fn().mockResolvedValue(mockWorkItemResponse);
-
wrapper = shallowMountExtended(WorkItemTree, {
- apolloProvider:
- apolloProvider || createMockApollo([[workItemQuery, getWorkItemQueryHandler]]),
+ provide: {
+ fullPath: 'test/project',
+ },
propsData: {
workItemType,
parentWorkItemType,
workItemId: 'gid://gitlab/WorkItem/515',
confidential,
children,
- projectPath: 'test/project',
+ canUpdate,
},
});
@@ -78,14 +57,11 @@ describe('WorkItemTree', () => {
expect(findEmptyState().exists()).toBe(true);
});
- it('renders all hierarchy widget children', () => {
+ it('renders hierarchy widget children container', () => {
createComponent();
- const workItemLinkChildren = findWorkItemLinkChildItems();
- expect(workItemLinkChildren).toHaveLength(4);
- expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe(
- childrenWorkItems[0].confidential,
- );
+ expect(findWorkItemLinkChildrenWrapper().exists()).toBe(true);
+ expect(findWorkItemLinkChildrenWrapper().props().children).toHaveLength(4);
});
it('does not display form by default', () => {
@@ -118,47 +94,19 @@ describe('WorkItemTree', () => {
},
);
- it('remove event on child triggers `removeChild` event', () => {
- createComponent();
- const firstChild = findWorkItemLinkChildItems().at(0);
-
- firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2');
-
- expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
- });
-
- it('emits `show-modal` on `click` event', () => {
- createComponent();
- const firstChild = findWorkItemLinkChildItems().at(0);
- const event = {
- childItem: 'gid://gitlab/WorkItem/2',
- };
+ describe('when no permission to update', () => {
+ beforeEach(() => {
+ createComponent({
+ canUpdate: false,
+ });
+ });
- firstChild.vm.$emit('click', event);
+ it('does not display button to toggle Add form', () => {
+ expect(findToggleFormSplitButton().exists()).toBe(false);
+ });
- expect(wrapper.emitted('show-modal')).toEqual([[event, event.childItem]]);
+ it('does not display link menu on children', () => {
+ expect(findWorkItemLinkChildrenWrapper().props('canUpdate')).toBe(false);
+ });
});
-
- it.each`
- description | workItemType | prefetch
- ${'prefetches'} | ${'Issue'} | ${true}
- ${'does not prefetch'} | ${'Objective'} | ${false}
- `(
- '$description work-item-link-child on mouseover when workItemType is "$workItemType"',
- async ({ workItemType, prefetch }) => {
- createComponent({ workItemType });
- const firstChild = findWorkItemLinkChildItems().at(0);
- firstChild.vm.$emit('mouseover', childrenWorkItems[0]);
- await nextTick();
- await waitForPromises();
-
- jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
-
- if (prefetch) {
- expect(getWorkItemQueryHandler).toHaveBeenCalled();
- } else {
- expect(getWorkItemQueryHandler).not.toHaveBeenCalled();
- }
- },
- );
});
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
index 5997de01274..c42c9a573e5 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -9,27 +9,20 @@ import {
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
-import { resolvers, config } from '~/graphql_shared/issuable_client';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import {
projectMilestonesResponse,
projectMilestonesResponseWithNoMilestones,
mockMilestoneWidgetResponse,
- workItemResponseFactory,
updateWorkItemMutationErrorResponse,
- workItemMilestoneSubscriptionResponse,
- projectWorkItemResponse,
updateWorkItemMutationResponse,
-} from 'jest/work_items/mock_data';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
-import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+} from '../mock_data';
describe('WorkItemMilestone component', () => {
Vue.use(VueApollo);
@@ -38,7 +31,6 @@ describe('WorkItemMilestone component', () => {
const workItemId = 'gid://gitlab/WorkItem/1';
const workItemType = 'Task';
- const fullPath = 'full-path';
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
@@ -52,72 +44,36 @@ describe('WorkItemMilestone component', () => {
const findDropdownTextAtIndex = (index) => findDropdownTexts().at(index);
const findInputGroup = () => wrapper.findComponent(GlFormGroup);
- const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
- const workItemQueryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
-
- const networkResolvedValue = new Error();
-
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse);
const successSearchWithNoMatchingMilestones = jest
.fn()
.mockResolvedValue(projectMilestonesResponseWithNoMilestones);
- const milestoneSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemMilestoneSubscriptionResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
.mockResolvedValue(updateWorkItemMutationResponse);
- const showDropdown = () => {
- findDropdown().vm.$emit('shown');
- };
-
- const hideDropdown = () => {
- findDropdown().vm.$emit('hide');
- };
+ const showDropdown = () => findDropdown().vm.$emit('shown');
+ const hideDropdown = () => findDropdown().vm.$emit('hide');
const createComponent = ({
canUpdate = true,
milestone = mockMilestoneWidgetResponse,
searchQueryHandler = successSearchQueryHandler,
- fetchByIid = false,
mutationHandler = successUpdateWorkItemMutationHandler,
} = {}) => {
- const apolloProvider = createMockApollo(
- [
- [workItemQuery, workItemQueryHandler],
- [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ wrapper = shallowMountExtended(WorkItemMilestone, {
+ apolloProvider: createMockApollo([
[projectMilestonesQuery, searchQueryHandler],
[updateWorkItemMutation, mutationHandler],
- [workItemByIidQuery, workItemByIidResponseHandler],
- ],
- resolvers,
- {
- typePolicies: config.cacheConfig.typePolicies,
- },
- );
-
- apolloProvider.clients.defaultClient.writeQuery({
- query: workItemQuery,
- variables: {
- id: workItemId,
+ ]),
+ provide: {
+ fullPath: 'full-path',
},
- data: workItemQueryResponse.data,
- });
-
- wrapper = shallowMountExtended(WorkItemMilestone, {
- apolloProvider,
propsData: {
canUpdate,
workItemMilestone: milestone,
workItemId,
workItemType,
- fullPath,
- queryVariables: {
- id: workItemId,
- },
- fetchByIid,
},
stubs: {
GlDropdown,
@@ -244,7 +200,7 @@ describe('WorkItemMilestone component', () => {
it.each`
errorType | expectedErrorMessage | mockValue | resolveFunction
${'graphql error'} | ${'Something went wrong while updating the task. Please try again.'} | ${updateWorkItemMutationErrorResponse} | ${'mockResolvedValue'}
- ${'network error'} | ${'Something went wrong while updating the task. Please try again.'} | ${networkResolvedValue} | ${'mockRejectedValue'}
+ ${'network error'} | ${'Something went wrong while updating the task. Please try again.'} | ${new Error()} | ${'mockRejectedValue'}
`(
'emits an error when there is a $errorType',
async ({ mockValue, expectedErrorMessage, resolveFunction }) => {
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 3db848a0ad2..c2821cc99f9 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -9,34 +9,37 @@ import SystemNote from '~/work_items/components/notes/system_note.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
-import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
-import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
+import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue';
import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
+import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_item_note_created.subscription.graphql';
+import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql';
+import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql';
import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants';
import { ASC, DESC } from '~/notes/constants';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
mockWorkItemNotesResponse,
workItemQueryResponse,
mockWorkItemNotesByIidResponse,
mockMoreWorkItemNotesResponse,
mockWorkItemNotesResponseWithComments,
+ workItemNotesCreateSubscriptionResponse,
+ workItemNotesUpdateSubscriptionResponse,
+ workItemNotesDeleteSubscriptionResponse,
} from '../mock_data';
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
+const mockWorkItemIid = workItemQueryResponse.data.workItem.iid;
const mockNotesWidgetResponse = mockWorkItemNotesResponse.data.workItem.widgets.find(
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
-const mockNotesByIidWidgetResponse = mockWorkItemNotesByIidResponse.data.workspace.workItems.nodes[0].widgets.find(
+const mockMoreNotesWidgetResponse = mockMoreWorkItemNotesResponse.data.workspace.workItems.nodes[0].widgets.find(
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
-const mockMoreNotesWidgetResponse = mockMoreWorkItemNotesResponse.data.workItem.widgets.find(
- (widget) => widget.type === WIDGET_TYPE_NOTES,
-);
-
-const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find(
+const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workspace.workItems.nodes[0].widgets.find(
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
@@ -53,19 +56,14 @@ describe('WorkItemNotes component', () => {
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
const findAllListItems = () => wrapper.findAll('ul.timeline > *');
- const findActivityLabel = () => wrapper.find('label');
- const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findSortingFilter = () => wrapper.findComponent(ActivityFilter);
+ const findActivityHeader = () => wrapper.findComponent(WorkItemNotesActivityHeader);
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion);
const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
const findDeleteNoteModal = () => wrapper.findComponent(GlModal);
- const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
- const workItemNotesByIidQueryHandler = jest
- .fn()
- .mockResolvedValue(mockWorkItemNotesByIidResponse);
+ const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesByIidResponse);
const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
const workItemNotesWithCommentsQueryHandler = jest
.fn()
@@ -73,33 +71,41 @@ describe('WorkItemNotes component', () => {
const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({
data: { destroyNote: { note: null, __typename: 'DestroyNote' } },
});
+ const notesCreateSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemNotesCreateSubscriptionResponse);
+ const notesUpdateSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemNotesUpdateSubscriptionResponse);
+ const notesDeleteSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue(workItemNotesDeleteSubscriptionResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
workItemId = mockWorkItemId,
- fetchByIid = false,
+ workItemIid = mockWorkItemIid,
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
+ isModal = false,
} = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
- [workItemNotesQuery, defaultWorkItemNotesQueryHandler],
- [workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
+ [workItemNotesByIidQuery, defaultWorkItemNotesQueryHandler],
[deleteWorkItemNoteMutation, deleteWINoteMutationHandler],
+ [workItemNoteCreatedSubscription, notesCreateSubscriptionHandler],
+ [workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler],
+ [workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler],
]),
+ provide: {
+ fullPath: 'test-path',
+ },
propsData: {
workItemId,
- queryVariables: {
- id: workItemId,
- },
- fullPath: 'test-path',
- fetchByIid,
+ workItemIid,
workItemType: 'task',
- },
- provide: {
- glFeatures: {
- useIidInWorkItemsPath: fetchByIid,
- },
+ reportAbusePath: '/report/abuse/path',
+ isModal,
},
stubs: {
GlModal: stubComponent(GlModal, { methods: { show: showModal } }),
@@ -107,23 +113,12 @@ describe('WorkItemNotes component', () => {
});
};
- beforeEach(async () => {
+ beforeEach(() => {
createComponent();
});
- it('renders activity label', () => {
- expect(findActivityLabel().exists()).toBe(true);
- });
-
- it('passes correct props to comment form component', async () => {
- createComponent({
- workItemId: mockWorkItemId,
- fetchByIid: false,
- defaultWorkItemNotesQueryHandler: workItemNotesByIidQueryHandler,
- });
- await waitForPromises();
-
- expect(findWorkItemAddNote().props('fetchByIid')).toEqual(false);
+ it('has the work item note activity header', () => {
+ expect(findActivityHeader().exists()).toBe(true);
});
describe('when notes are loading', () => {
@@ -143,28 +138,16 @@ describe('WorkItemNotes component', () => {
it('renders system notes to the length of the response', async () => {
await waitForPromises();
+ expect(workItemNotesQueryHandler).toHaveBeenCalledWith({
+ after: undefined,
+ fullPath: 'test-path',
+ iid: '1',
+ pageSize: 30,
+ });
expect(findAllSystemNotes()).toHaveLength(mockNotesWidgetResponse.discussions.nodes.length);
});
});
- describe('when the notes are fetched by `iid`', () => {
- beforeEach(async () => {
- createComponent({ workItemId: mockWorkItemId, fetchByIid: true });
- await waitForPromises();
- });
-
- it('renders the notes list to the length of the response', () => {
- expect(workItemNotesByIidQueryHandler).toHaveBeenCalled();
- expect(findAllSystemNotes()).toHaveLength(
- mockNotesByIidWidgetResponse.discussions.nodes.length,
- );
- });
-
- it('passes correct props to comment form component', () => {
- expect(findWorkItemAddNote().props('fetchByIid')).toEqual(true);
- });
- });
-
describe('Pagination', () => {
describe('When there is no next page', () => {
it('fetch more notes is not called', async () => {
@@ -182,15 +165,17 @@ describe('WorkItemNotes component', () => {
it('fetch more notes should be called', async () => {
expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'test-path',
+ iid: '1',
pageSize: DEFAULT_PAGE_SIZE_NOTES,
- id: 'gid://gitlab/WorkItem/1',
});
await nextTick();
expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({
- pageSize: 45,
- id: 'gid://gitlab/WorkItem/1',
+ fullPath: 'test-path',
+ iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_NOTES,
after: mockMoreNotesWidgetResponse.discussions.pageInfo.endCursor,
});
});
@@ -203,26 +188,22 @@ describe('WorkItemNotes component', () => {
await waitForPromises();
});
- it('filter exists', () => {
- expect(findSortingFilter().exists()).toBe(true);
- });
-
- it('sorts the list when the `changeSortOrder` event is emitted', async () => {
+ it('sorts the list when the `changeSort` event is emitted', async () => {
expect(findSystemNoteAtIndex(0).props('note').id).toEqual(firstSystemNodeId);
- await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+ await findActivityHeader().vm.$emit('changeSort', DESC);
expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId);
});
it('puts form at start of list in when sorting by newest first', async () => {
- await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+ await findActivityHeader().vm.$emit('changeSort', DESC);
expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true);
});
it('puts form at end of list in when sorting by oldest first', async () => {
- await findSortingFilter().vm.$emit('changeSortOrder', ASC);
+ await findActivityHeader().vm.$emit('changeSort', ASC);
expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true);
});
@@ -250,9 +231,11 @@ describe('WorkItemNotes component', () => {
const commentIndex = 0;
const firstCommentNote = findWorkItemCommentNoteAtIndex(commentIndex);
- expect(firstCommentNote.props('discussion')).toEqual(
- mockDiscussions[commentIndex].notes.nodes,
- );
+ expect(firstCommentNote.props()).toMatchObject({
+ discussion: mockDiscussions[commentIndex].notes.nodes,
+ autocompleteDataSources: autocompleteDataSources('test-path', mockWorkItemIid),
+ markdownPreviewPath: markdownPreviewPath('test-path', mockWorkItemIid),
+ });
});
});
@@ -334,4 +317,31 @@ describe('WorkItemNotes component', () => {
['Something went wrong when deleting a comment. Please try again'],
]);
});
+
+ describe('Notes subscriptions', () => {
+ beforeEach(async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+ });
+
+ it('has create notes subscription', () => {
+ expect(notesCreateSubscriptionHandler).toHaveBeenCalledWith({
+ noteableId: mockWorkItemId,
+ });
+ });
+
+ it('has delete notes subscription', () => {
+ expect(notesDeleteSubscriptionHandler).toHaveBeenCalledWith({
+ noteableId: mockWorkItemId,
+ });
+ });
+
+ it('has update notes subscription', () => {
+ expect(notesUpdateSubscriptionHandler).toHaveBeenCalledWith({
+ noteableId: mockWorkItemId,
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_state_spec.js b/spec/frontend/work_items/components/work_item_state_spec.js
index b24d940d56a..d1262057c73 100644
--- a/spec/frontend/work_items/components/work_item_state_spec.js
+++ b/spec/frontend/work_items/components/work_item_state_spec.js
@@ -44,10 +44,6 @@ describe('WorkItemState component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders state', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_title_spec.js b/spec/frontend/work_items/components/work_item_title_spec.js
index a549aad5cd8..34391b74cf7 100644
--- a/spec/frontend/work_items/components/work_item_title_spec.js
+++ b/spec/frontend/work_items/components/work_item_title_spec.js
@@ -41,10 +41,6 @@ describe('WorkItemTitle component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders title', () => {
createComponent();
diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/work_item_todos_spec.js
new file mode 100644
index 00000000000..83b61a04298
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_todos_spec.js
@@ -0,0 +1,97 @@
+import { GlButton, GlIcon } from '@gitlab/ui';
+import Vue from 'vue';
+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 WorkItemTodos from '~/work_items/components/work_item_todos.vue';
+import { ADD, TODO_DONE_ICON, TODO_ADD_ICON } from '~/work_items/constants';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateGlobalTodoCount } from '~/sidebar/utils';
+import { workItemResponseFactory, updateWorkItemMutationResponseFactory } from '../mock_data';
+
+jest.mock('~/sidebar/utils');
+
+describe('WorkItemTodo component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const findTodoWidget = () => wrapper.findComponent(GlButton);
+ const findTodoIcon = () => wrapper.findComponent(GlIcon);
+
+ const errorMessage = 'Failed to add item';
+ const workItemQueryResponse = workItemResponseFactory({ canUpdate: true });
+ const successHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponseFactory({ canUpdate: true }));
+ const failureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
+
+ const inputVariables = {
+ id: 'gid://gitlab/WorkItem/1',
+ currentUserTodosWidget: {
+ action: ADD,
+ },
+ };
+
+ const createComponent = ({
+ currentUserTodosMock = [updateWorkItemMutation, successHandler],
+ currentUserTodos = [],
+ } = {}) => {
+ const handlers = [currentUserTodosMock];
+ wrapper = shallowMountExtended(WorkItemTodos, {
+ apolloProvider: createMockApollo(handlers),
+ propsData: {
+ workItem: workItemQueryResponse.data.workItem,
+ currentUserTodos,
+ },
+ });
+ };
+
+ it('renders the widget', () => {
+ createComponent();
+
+ expect(findTodoWidget().exists()).toBe(true);
+ expect(findTodoIcon().props('name')).toEqual(TODO_ADD_ICON);
+ expect(findTodoIcon().classes('gl-fill-blue-500')).toBe(false);
+ });
+
+ it('renders mark as done button when there is pending item', () => {
+ createComponent({
+ currentUserTodos: [
+ {
+ node: {
+ id: 'gid://gitlab/Todo/1',
+ state: 'pending',
+ },
+ },
+ ],
+ });
+
+ expect(findTodoIcon().props('name')).toEqual(TODO_DONE_ICON);
+ expect(findTodoIcon().classes('gl-fill-blue-500')).toBe(true);
+ });
+
+ it('calls update mutation when to do button is clicked', async () => {
+ createComponent();
+
+ findTodoWidget().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ input: inputVariables,
+ });
+ expect(updateGlobalTodoCount).toHaveBeenCalled();
+ });
+
+ it('emits error when the update mutation fails', async () => {
+ createComponent({ currentUserTodosMock: [updateWorkItemMutation, failureHandler] });
+
+ findTodoWidget().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[errorMessage]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_type_icon_spec.js b/spec/frontend/work_items/components/work_item_type_icon_spec.js
index 182fb0f8cb6..a5e955c4dbf 100644
--- a/spec/frontend/work_items/components/work_item_type_icon_spec.js
+++ b/spec/frontend/work_items/components/work_item_type_icon_spec.js
@@ -9,7 +9,7 @@ function createComponent(propsData) {
wrapper = shallowMount(WorkItemTypeIcon, {
propsData,
directives: {
- GlTooltip: createMockDirective(),
+ GlTooltip: createMockDirective('gl-tooltip'),
},
});
}
@@ -17,10 +17,6 @@ function createComponent(propsData) {
describe('Work Item type component', () => {
const findIcon = () => wrapper.findComponent(GlIcon);
- afterEach(() => {
- wrapper.destroy();
- });
-
describe.each`
workItemType | workItemIconName | iconName | text | showTooltipOnHover
${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false}
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index d4832fe376d..05c6a21bb38 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -46,6 +46,29 @@ export const mockMilestone = {
dueDate: '2022-10-24',
};
+export const mockAwardEmojiThumbsUp = {
+ name: 'thumbsup',
+ __typename: 'AwardEmoji',
+ user: {
+ id: 'gid://gitlab/User/5',
+ __typename: 'UserCore',
+ },
+};
+
+export const mockAwardEmojiThumbsDown = {
+ name: 'thumbsdown',
+ __typename: 'AwardEmoji',
+ user: {
+ id: 'gid://gitlab/User/5',
+ __typename: 'UserCore',
+ },
+};
+
+export const mockAwardsWidget = {
+ nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiThumbsDown],
+ __typename: 'AwardEmojiConnection',
+};
+
export const workItemQueryResponse = {
data: {
workItem: {
@@ -82,6 +105,9 @@ export const workItemQueryResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
+ adminParentLink: false,
+ __typename: 'WorkItemPermissions',
},
widgets: [
{
@@ -182,6 +208,9 @@ export const updateWorkItemMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
+ adminParentLink: false,
+ __typename: 'WorkItemPermissions',
},
widgets: [
{
@@ -239,6 +268,100 @@ export const updateWorkItemMutationErrorResponse = {
},
};
+export const convertWorkItemMutationErrorResponse = {
+ data: {
+ workItemConvert: {
+ __typename: 'WorkItemConvertPayload',
+ errors: ['Error!'],
+ workItem: {},
+ },
+ },
+};
+
+export const convertWorkItemMutationResponse = {
+ data: {
+ workItemConvert: {
+ __typename: 'WorkItemConvertPayload',
+ errors: [],
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
+ title: 'Updated title',
+ state: 'OPEN',
+ description: 'description',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: '2022-08-08T12:41:54Z',
+ closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ archived: false,
+ },
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/4',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ },
+ userPermissions: {
+ deleteWorkItem: false,
+ updateWorkItem: false,
+ setWorkItemMetadata: false,
+ adminParentLink: false,
+ __typename: 'WorkItemPermissions',
+ },
+ widgets: [
+ {
+ type: 'HIERARCHY',
+ children: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ workItemType: {
+ id: '1',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ },
+ ],
+ },
+ __typename: 'WorkItemConnection',
+ },
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ type: 'ASSIGNEES',
+ allowsMultipleAssignees: true,
+ canInviteMembers: true,
+ assignees: {
+ nodes: [mockAssignees[0]],
+ },
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ type: 'LABELS',
+ allowsScopedLabels: false,
+ labels: {
+ nodes: mockLabels,
+ },
+ },
+ ],
+ },
+ },
+ },
+};
+
export const mockParent = {
parent: {
id: 'gid://gitlab/Issue/1',
@@ -284,6 +407,11 @@ export const objectiveType = {
export const workItemResponseFactory = ({
canUpdate = false,
canDelete = false,
+ adminParentLink = false,
+ notificationsWidgetPresent = true,
+ currentUserTodosWidgetPresent = true,
+ awardEmojiWidgetPresent = true,
+ subscribed = true,
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
datesWidgetPresent = true,
@@ -306,12 +434,13 @@ export const workItemResponseFactory = ({
author = mockAssignees[0],
createdAt = '2022-08-03T12:41:54Z',
updatedAt = '2022-08-08T12:32:54Z',
+ awardEmoji = mockAwardsWidget,
} = {}) => ({
data: {
workItem: {
__typename: 'WorkItem',
id: 'gid://gitlab/WorkItem/1',
- iid: 1,
+ iid: '1',
title: 'Updated title',
state: 'OPEN',
description: 'description',
@@ -330,6 +459,9 @@ export const workItemResponseFactory = ({
userPermissions: {
deleteWorkItem: canDelete,
updateWorkItem: canUpdate,
+ setWorkItemMetadata: canUpdate,
+ adminParentLink,
+ __typename: 'WorkItemPermissions',
},
widgets: [
{
@@ -466,30 +598,87 @@ export const workItemResponseFactory = ({
type: 'NOTES',
}
: { type: 'MOCK TYPE' },
+ notificationsWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetNotifications',
+ type: 'NOTIFICATIONS',
+ subscribed,
+ }
+ : { type: 'MOCK TYPE' },
+ currentUserTodosWidgetPresent
+ ? {
+ type: 'CURRENT_USER_TODOS',
+ currentUserTodos: {
+ edges: [
+ {
+ node: {
+ id: 'gid://gitlab/Todo/1',
+ state: 'pending',
+ __typename: 'Todo',
+ },
+ __typename: 'TodoEdge',
+ },
+ ],
+ __typename: 'TodoConnection',
+ },
+ __typename: 'WorkItemWidgetCurrentUserTodos',
+ }
+ : { type: 'MOCK TYPE' },
+ awardEmojiWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetAwardEmoji',
+ type: 'AWARD_EMOJI',
+ awardEmoji,
+ }
+ : { type: 'MOCK TYPE' },
],
},
},
});
+export const workItemByIidResponseFactory = (options) => {
+ const response = workItemResponseFactory(options);
+ return {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [response.data.workItem],
+ },
+ },
+ },
+ };
+};
+
+export const updateWorkItemMutationResponseFactory = (options) => {
+ const response = workItemResponseFactory(options);
+ return {
+ data: {
+ workItemUpdate: {
+ workItem: response.data.workItem,
+ errors: [],
+ },
+ },
+ };
+};
+
export const getIssueDetailsResponse = ({ confidential = false } = {}) => ({
data: {
- workspace: {
- id: 'gid://gitlab/Project/1',
- issuable: {
- id: 'gid://gitlab/Issue/4',
- confidential,
- iteration: {
- id: 'gid://gitlab/Iteration/1124',
- __typename: 'Iteration',
- },
- milestone: {
- id: 'gid://gitlab/Milestone/28',
- __typename: 'Milestone',
- },
- __typename: 'Issue',
+ issue: {
+ id: 'gid://gitlab/Issue/4',
+ confidential,
+ iteration: {
+ id: 'gid://gitlab/Iteration/1124',
+ __typename: 'Iteration',
},
- __typename: 'Project',
+ milestone: {
+ id: 'gid://gitlab/Milestone/28',
+ __typename: 'Milestone',
+ },
+ __typename: 'Issue',
},
+ __typename: 'Project',
},
});
@@ -503,6 +692,8 @@ export const projectWorkItemTypesQueryResponse = {
{ id: 'gid://gitlab/WorkItems::Type/1', name: 'Issue' },
{ id: 'gid://gitlab/WorkItems::Type/2', name: 'Incident' },
{ id: 'gid://gitlab/WorkItems::Type/3', name: 'Task' },
+ { id: 'gid://gitlab/WorkItems::Type/4', name: 'Objective' },
+ { id: 'gid://gitlab/WorkItems::Type/5', name: 'Key Result' },
],
},
},
@@ -542,6 +733,9 @@ export const createWorkItemMutationResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
+ adminParentLink: false,
+ __typename: 'WorkItemPermissions',
},
widgets: [],
},
@@ -560,83 +754,6 @@ export const createWorkItemMutationErrorResponse = {
},
};
-export const createWorkItemFromTaskMutationResponse = {
- data: {
- workItemCreateFromTask: {
- __typename: 'WorkItemCreateFromTaskPayload',
- errors: [],
- workItem: {
- __typename: 'WorkItem',
- description: 'New description',
- id: 'gid://gitlab/WorkItem/1',
- iid: '1',
- title: 'Updated title',
- state: 'OPEN',
- confidential: false,
- createdAt: '2022-08-03T12:41:54Z',
- closedAt: null,
- project: {
- __typename: 'Project',
- id: '1',
- fullPath: 'test-project-path',
- archived: false,
- },
- workItemType: {
- __typename: 'WorkItemType',
- id: 'gid://gitlab/WorkItems::Type/5',
- name: 'Task',
- iconName: 'issue-type-task',
- },
- userPermissions: {
- deleteWorkItem: false,
- updateWorkItem: false,
- },
- widgets: [
- {
- __typename: 'WorkItemWidgetDescription',
- type: 'DESCRIPTION',
- description: 'New description',
- descriptionHtml: '<p>New description</p>',
- lastEditedAt: '2022-09-21T06:18:42Z',
- lastEditedBy: {
- name: 'Administrator',
- webPath: '/root',
- },
- },
- ],
- },
- newWorkItem: {
- __typename: 'WorkItem',
- id: 'gid://gitlab/WorkItem/1000000',
- iid: '100',
- title: 'Updated title',
- state: 'OPEN',
- createdAt: '2022-08-03T12:41:54Z',
- closedAt: null,
- description: '',
- confidential: false,
- project: {
- __typename: 'Project',
- id: '1',
- fullPath: 'test-project-path',
- archived: false,
- },
- workItemType: {
- __typename: 'WorkItemType',
- id: 'gid://gitlab/WorkItems::Type/5',
- name: 'Task',
- iconName: 'issue-type-task',
- },
- userPermissions: {
- deleteWorkItem: false,
- updateWorkItem: false,
- },
- widgets: [],
- },
- },
- },
-};
-
export const deleteWorkItemResponse = {
data: { workItemDelete: { errors: [], __typename: 'WorkItemDeletePayload' } },
};
@@ -661,24 +778,6 @@ export const deleteWorkItemMutationErrorResponse = {
},
};
-export const deleteWorkItemFromTaskMutationResponse = {
- data: {
- workItemDeleteTask: {
- workItem: { id: 123, descriptionHtml: 'updated work item desc' },
- errors: [],
- },
- },
-};
-
-export const deleteWorkItemFromTaskMutationErrorResponse = {
- data: {
- workItemDeleteTask: {
- workItem: { id: 123, descriptionHtml: 'updated work item desc' },
- errors: ['Error'],
- },
- },
-};
-
export const workItemDatesSubscriptionResponse = {
data: {
issuableDatesUpdated: {
@@ -831,15 +930,20 @@ export const workItemHierarchyEmptyResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
+ state: 'OPEN',
workItemType: {
- id: 'gid://gitlab/WorkItems::Type/6',
+ id: 'gid://gitlab/WorkItems::Type/1',
name: 'Issue',
iconName: 'issue-type-issue',
__typename: 'WorkItemType',
},
title: 'New title',
+ description: '',
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: mockAssignees[0],
project: {
__typename: 'Project',
id: '1',
@@ -849,14 +953,13 @@ export const workItemHierarchyEmptyResponse = {
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
+ adminParentLink: false,
+ __typename: 'WorkItemPermissions',
},
confidential: false,
widgets: [
{
- type: 'DESCRIPTION',
- __typename: 'WorkItemWidgetDescription',
- },
- {
type: 'HIERARCHY',
parent: null,
hasChildren: false,
@@ -876,6 +979,8 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
data: {
workItem: {
id: 'gid://gitlab/WorkItem/1',
+ iid: '1',
+ state: 'OPEN',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/6',
name: 'Issue',
@@ -883,9 +988,17 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
__typename: 'WorkItemType',
},
title: 'New title',
+ description: '',
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
+ closedAt: null,
+ author: mockAssignees[0],
userPermissions: {
deleteWorkItem: false,
updateWorkItem: false,
+ setWorkItemMetadata: false,
+ adminParentLink: false,
+ __typename: 'WorkItemPermissions',
},
project: {
__typename: 'Project',
@@ -896,10 +1009,6 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
confidential: false,
widgets: [
{
- type: 'DESCRIPTION',
- __typename: 'WorkItemWidgetDescription',
- },
- {
type: 'HIERARCHY',
parent: null,
hasChildren: true,
@@ -952,6 +1061,7 @@ export const workItemTask = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [],
__typename: 'WorkItem',
};
@@ -969,6 +1079,7 @@ export const confidentialWorkItemTask = {
confidential: true,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [],
__typename: 'WorkItem',
};
@@ -986,6 +1097,7 @@ export const closedWorkItemTask = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: '2022-08-12T13:07:52Z',
+ widgets: [],
__typename: 'WorkItem',
};
@@ -1007,6 +1119,7 @@ export const childrenWorkItems = [
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ widgets: [],
__typename: 'WorkItem',
},
];
@@ -1017,15 +1130,21 @@ export const workItemHierarchyResponse = {
id: 'gid://gitlab/WorkItem/1',
iid: '1',
workItemType: {
- id: 'gid://gitlab/WorkItems::Type/6',
- name: 'Objective',
- iconName: 'issue-type-objective',
+ id: 'gid://gitlab/WorkItems::Type/1',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
__typename: 'WorkItemType',
},
title: 'New title',
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
+ adminParentLink: true,
+ __typename: 'WorkItemPermissions',
+ },
+ author: {
+ ...mockAssignees[0],
},
confidential: false,
project: {
@@ -1034,12 +1153,13 @@ export const workItemHierarchyResponse = {
fullPath: 'test-project-path',
archived: false,
},
+ description: 'Issue description',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
+ closedAt: null,
widgets: [
{
- type: 'DESCRIPTION',
- __typename: 'WorkItemWidgetDescription',
- },
- {
type: 'HIERARCHY',
parent: null,
hasChildren: true,
@@ -1110,6 +1230,9 @@ export const workItemObjectiveWithChild = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
+ adminParentLink: true,
+ __typename: 'WorkItemPermissions',
},
author: {
...mockAssignees[0],
@@ -1176,6 +1299,9 @@ export const workItemHierarchyTreeResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
+ adminParentLink: true,
+ __typename: 'WorkItemPermissions',
},
confidential: false,
project: {
@@ -1238,6 +1364,69 @@ export const workItemHierarchyTreeFailureResponse = {
],
};
+export const changeIndirectWorkItemParentMutationResponse = {
+ data: {
+ workItemUpdate: {
+ workItem: {
+ __typename: 'WorkItem',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2411',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ userPermissions: {
+ deleteWorkItem: true,
+ updateWorkItem: true,
+ setWorkItemMetadata: true,
+ adminParentLink: true,
+ __typename: 'WorkItemPermissions',
+ },
+ description: null,
+ id: 'gid://gitlab/WorkItem/13',
+ iid: '13',
+ state: 'OPEN',
+ title: 'Objective 2',
+ confidential: false,
+ createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
+ closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
+ project: {
+ __typename: 'Project',
+ id: '1',
+ fullPath: 'test-project-path',
+ archived: false,
+ },
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: null,
+ hasChildren: false,
+ children: {
+ nodes: [],
+ },
+ },
+ ],
+ },
+ errors: [],
+ __typename: 'WorkItemUpdatePayload',
+ },
+ },
+};
+
+export const workItemUpdateFailureResponse = {
+ data: {},
+ errors: [
+ {
+ message: 'Something went wrong',
+ },
+ ],
+};
+
export const changeWorkItemParentMutationResponse = {
data: {
workItemUpdate: {
@@ -1252,6 +1441,9 @@ export const changeWorkItemParentMutationResponse = {
userPermissions: {
deleteWorkItem: true,
updateWorkItem: true,
+ setWorkItemMetadata: true,
+ adminParentLink: true,
+ __typename: 'WorkItemPermissions',
},
description: null,
id: 'gid://gitlab/WorkItem/2',
@@ -1617,17 +1809,6 @@ export const projectMilestonesResponseWithNoMilestones = {
},
};
-export const projectWorkItemResponse = {
- data: {
- workspace: {
- id: 'gid://gitlab/Project/1',
- workItems: {
- nodes: [workItemQueryResponse.data.workItem],
- },
- },
- },
-};
-
export const mockWorkItemNotesResponse = {
data: {
workItem: {
@@ -1681,6 +1862,8 @@ 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',
lastEditedBy: null,
system: true,
internal: false,
@@ -1724,6 +1907,8 @@ 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',
lastEditedBy: null,
system: true,
internal: false,
@@ -1766,6 +1951,8 @@ 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',
lastEditedBy: null,
system: true,
internal: false,
@@ -1868,6 +2055,8 @@ 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',
lastEditedBy: null,
system: true,
internal: false,
@@ -1913,6 +2102,8 @@ 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',
lastEditedBy: null,
system: true,
internal: false,
@@ -1959,6 +2150,8 @@ 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',
lastEditedBy: null,
system: true,
internal: false,
@@ -2008,180 +2201,197 @@ export const mockWorkItemNotesByIidResponse = {
};
export const mockMoreWorkItemNotesResponse = {
data: {
- workItem: {
- id: 'gid://gitlab/WorkItem/600',
- iid: '60',
- widgets: [
- {
- __typename: 'WorkItemWidgetIteration',
- },
- {
- __typename: 'WorkItemWidgetWeight',
- },
- {
- __typename: 'WorkItemWidgetAssignees',
- },
- {
- __typename: 'WorkItemWidgetLabels',
- },
- {
- __typename: 'WorkItemWidgetDescription',
- },
- {
- __typename: 'WorkItemWidgetHierarchy',
- },
- {
- __typename: 'WorkItemWidgetStartAndDueDate',
- },
- {
- __typename: 'WorkItemWidgetMilestone',
- },
- {
- type: 'NOTES',
- discussions: {
- pageInfo: {
- hasNextPage: true,
- hasPreviousPage: false,
- startCursor: null,
- endCursor: 'endCursor',
- __typename: 'PageInfo',
- },
- nodes: [
+ workspace: {
+ id: 'gid://gitlab/Project/6',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/600',
+ iid: '60',
+ widgets: [
{
- id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
- notes: {
- nodes: [
- {
- id: 'gid://gitlab/Note/2428',
- body: 'added #31 as parent issue',
- bodyHtml:
- '<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
- systemNoteIconName: 'link',
- createdAt: '2022-11-14T04:18:59Z',
- lastEditedAt: null,
- lastEditedBy: null,
- system: true,
- internal: false,
- discussion: {
- id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e',
- },
- userPermissions: {
- adminNote: false,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
- },
- author: {
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 'gid://gitlab/User/1',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- __typename: 'UserCore',
- },
- __typename: 'Note',
- },
- ],
- __typename: 'NoteConnection',
- },
- __typename: 'Discussion',
+ __typename: 'WorkItemWidgetIteration',
},
{
- id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
- notes: {
- nodes: [
- {
- id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83823',
- body: 'changed milestone to %v4.0',
- bodyHtml:
- '<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
- systemNoteIconName: 'clock',
- createdAt: '2022-11-14T04:18:59Z',
- lastEditedAt: null,
- lastEditedBy: null,
- system: true,
- internal: false,
- discussion: {
- id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e',
- },
- userPermissions: {
- adminNote: false,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
- },
- author: {
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 'gid://gitlab/User/1',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- __typename: 'UserCore',
- },
- __typename: 'Note',
- },
- ],
- __typename: 'NoteConnection',
- },
- __typename: 'Discussion',
+ __typename: 'WorkItemWidgetWeight',
},
{
- id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
- notes: {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: 'endCursor',
+ __typename: 'PageInfo',
+ },
nodes: [
{
- id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
- body: 'changed weight to **89**',
- bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
- systemNoteIconName: 'weight',
- createdAt: '2022-11-25T07:16:20Z',
- lastEditedAt: null,
- lastEditedBy: null,
- system: true,
- internal: false,
- discussion: {
- id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/2428',
+ body: 'added #31 as parent issue',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
+ 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',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
},
- userPermissions: {
- adminNote: false,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ notes: {
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83823',
+ body: 'changed milestone to %v4.0',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
+ 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',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
},
- author: {
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 'gid://gitlab/User/1',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- __typename: 'UserCore',
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ body: 'changed weight to **89**',
+ bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
+ 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',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
},
- __typename: 'Note',
+ __typename: 'Discussion',
},
],
- __typename: 'NoteConnection',
+ __typename: 'DiscussionConnection',
},
- __typename: 'Discussion',
+ __typename: 'WorkItemWidgetNotes',
},
],
- __typename: 'DiscussionConnection',
+ __typename: 'WorkItem',
},
- __typename: 'WorkItemWidgetNotes',
- },
- ],
- __typename: 'WorkItem',
+ ],
+ },
},
},
};
@@ -2205,6 +2415,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',
lastEditedBy: null,
discussion: {
id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
@@ -2252,6 +2463,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',
lastEditedBy: null,
system: false,
internal: false,
@@ -2279,171 +2491,313 @@ export const mockWorkItemCommentNote = {
export const mockWorkItemNotesResponseWithComments = {
data: {
- workItem: {
- id: 'gid://gitlab/WorkItem/600',
- iid: '60',
- widgets: [
- {
- __typename: 'WorkItemWidgetIteration',
- },
- {
- __typename: 'WorkItemWidgetWeight',
- },
- {
- __typename: 'WorkItemWidgetAssignees',
- },
- {
- __typename: 'WorkItemWidgetLabels',
- },
- {
- __typename: 'WorkItemWidgetDescription',
- },
- {
- __typename: 'WorkItemWidgetHierarchy',
- },
- {
- __typename: 'WorkItemWidgetStartAndDueDate',
- },
- {
- __typename: 'WorkItemWidgetMilestone',
- },
- {
- type: 'NOTES',
- discussions: {
- pageInfo: {
- hasNextPage: false,
- hasPreviousPage: false,
- startCursor: null,
- endCursor: null,
- __typename: 'PageInfo',
- },
- nodes: [
+ workspace: {
+ id: 'gid://gitlab/Project/6',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/600',
+ iid: '60',
+ widgets: [
{
- id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
- notes: {
- nodes: [
- {
- id: 'gid://gitlab/DiscussionNote/174',
- body: 'Separate thread',
- bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Separate thread</p>',
- system: false,
- internal: false,
- systemNoteIconName: null,
- createdAt: '2023-01-12T07:47:40Z',
- lastEditedAt: null,
- lastEditedBy: null,
- discussion: {
- id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
- __typename: 'Discussion',
- },
- author: {
- id: 'gid://gitlab/User/1',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- __typename: 'UserCore',
- },
- userPermissions: {
- adminNote: true,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
- },
- __typename: 'Note',
- },
- {
- id: 'gid://gitlab/DiscussionNote/235',
- body: 'Thread comment',
- bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Thread comment</p>',
- system: false,
- internal: false,
- systemNoteIconName: null,
- createdAt: '2023-01-18T09:09:54Z',
- lastEditedAt: null,
- lastEditedBy: null,
- discussion: {
- id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
- __typename: 'Discussion',
- },
- author: {
- id: 'gid://gitlab/User/1',
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- __typename: 'UserCore',
- },
- userPermissions: {
- adminNote: true,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
- },
- __typename: 'Note',
- },
- ],
- __typename: 'NoteConnection',
- },
- __typename: 'Discussion',
+ __typename: 'WorkItemWidgetIteration',
},
{
- id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
- notes: {
+ __typename: 'WorkItemWidgetWeight',
+ },
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
nodes: [
{
- id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
- body: 'Main thread 2',
- bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Main thread 2</p>',
- systemNoteIconName: 'weight',
- createdAt: '2022-11-25T07:16:20Z',
- lastEditedAt: null,
- lastEditedBy: null,
- system: false,
- internal: false,
- discussion: {
- id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
- },
- userPermissions: {
- adminNote: false,
- awardEmoji: true,
- readNote: true,
- createNote: true,
- resolveNote: true,
- repositionNote: true,
- __typename: 'NotePermissions',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/174',
+ body: 'Separate thread',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Separate thread</p>',
+ system: false,
+ internal: false,
+ 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',
+ lastEditedBy: null,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/235',
+ body: 'Thread comment',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Thread comment</p>',
+ system: false,
+ internal: false,
+ 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',
+ lastEditedBy: null,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
},
- author: {
- avatarUrl:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
- id: 'gid://gitlab/User/1',
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- __typename: 'UserCore',
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
+ body: 'Main thread 2',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Main thread 2</p>',
+ 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',
+ lastEditedBy: null,
+ system: false,
+ internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
},
- __typename: 'Note',
+ __typename: 'Discussion',
},
],
- __typename: 'NoteConnection',
+ __typename: 'DiscussionConnection',
},
- __typename: 'Discussion',
+ __typename: 'WorkItemWidgetNotes',
},
],
- __typename: 'DiscussionConnection',
+ __typename: 'WorkItem',
},
- __typename: 'WorkItemWidgetNotes',
+ ],
+ },
+ },
+ },
+};
+
+export const workItemNotesCreateSubscriptionResponse = {
+ data: {
+ workItemNoteCreated: {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d81864',
+ body: 'changed weight to **89**',
+ bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
+ 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',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9881864',
+ body: 'changed weight to **89**',
+ bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
+ 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',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
},
- ],
- __typename: 'WorkItem',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ },
+};
+
+export const workItemNotesUpdateSubscriptionResponse = {
+ data: {
+ workItemNoteUpdated: {
+ id: 'gid://gitlab/Note/0f2f195ec0d1ef95ee9d5b10446b8e96a9883894',
+ body: 'changed title',
+ bodyHtml: '<p dir="auto">changed title<strong>89</strong></p>',
+ 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',
+ lastEditedBy: null,
+ system: true,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ },
+};
+
+export const workItemNotesDeleteSubscriptionResponse = {
+ data: {
+ workItemNoteDeleted: {
+ id: 'gid://gitlab/DiscussionNote/235',
+ discussionId: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ lastDiscussionNote: false,
},
},
};
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index 387c8a355fa..c369a454286 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -37,7 +37,6 @@ describe('Create work item component', () => {
props = {},
queryHandler = querySuccessHandler,
mutationHandler = createWorkItemSuccessHandler,
- fetchByIid = false,
} = {}) => {
fakeApollo = createMockApollo(
[
@@ -66,15 +65,11 @@ describe('Create work item component', () => {
},
provide: {
fullPath: 'full-path',
- glFeatures: {
- useIidInWorkItemsPath: fetchByIid,
- },
},
});
};
afterEach(() => {
- wrapper.destroy();
fakeApollo = null;
});
@@ -109,9 +104,7 @@ describe('Create work item component', () => {
expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
name: 'workItem',
- params: {
- id: '1',
- },
+ params: { id: '1' },
});
});
@@ -149,7 +142,7 @@ describe('Create work item component', () => {
});
it('displays a list of work item types', () => {
- expect(findSelect().attributes('options').split(',')).toHaveLength(4);
+ expect(findSelect().attributes('options').split(',')).toHaveLength(6);
});
it('selects a work item type on click', async () => {
@@ -210,18 +203,4 @@ describe('Create work item component', () => {
'Something went wrong when creating work item. Please try again.',
);
});
-
- it('performs a correct redirect when `useIidInWorkItemsPath` feature flag is enabled', async () => {
- createComponent({ fetchByIid: true });
- findTitleInput().vm.$emit('title-input', 'Test title');
-
- wrapper.find('form').trigger('submit');
- await waitForPromises();
-
- expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
- name: 'workItem',
- params: { id: '1' },
- query: { iid_path: 'true' },
- });
- });
});
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index a766962771a..c480affe484 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -44,10 +44,6 @@ describe('Work items root component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders WorkItemDetail', () => {
createComponent();
@@ -79,7 +75,7 @@ describe('Work items root component', () => {
expect(visitUrl).toHaveBeenCalledWith(issuesListPath);
});
- it('shows alert if delete fails', async () => {
+ it('shows an alert if delete fails', async () => {
const deleteWorkItemHandler = jest.fn().mockRejectedValue(deleteWorkItemFailureResponse);
createComponent({
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index ef9ae4a2eab..b5d54a7c319 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -3,17 +3,19 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
+ currentUserResponse,
workItemAssigneesSubscriptionResponse,
workItemDatesSubscriptionResponse,
- workItemResponseFactory,
+ workItemByIidResponseFactory,
workItemTitleSubscriptionResponse,
workItemLabelsSubscriptionResponse,
workItemMilestoneSubscriptionResponse,
workItemDescriptionSubscriptionResponse,
} from 'jest/work_items/mock_data';
+import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import App from '~/work_items/components/app.vue';
-import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import workItemDatesSubscription from '~/work_items/graphql/work_item_dates.subscription.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
@@ -30,7 +32,8 @@ describe('Work items router', () => {
Vue.use(VueApollo);
- const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
+ const workItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
+ const currentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
const assigneesSubscriptionHandler = jest
@@ -51,7 +54,8 @@ describe('Work items router', () => {
}
const handlers = [
- [workItemQuery, workItemQueryHandler],
+ [workItemByIidQuery, workItemQueryHandler],
+ [currentUserQuery, currentUserQueryHandler],
[workItemDatesSubscription, datesSubscriptionHandler],
[workItemTitleSubscription, titleSubscriptionHandler],
[workItemAssigneesSubscription, assigneesSubscriptionHandler],
@@ -70,11 +74,13 @@ describe('Work items router', () => {
hasIterationsFeature: false,
hasOkrsFeature: false,
hasIssuableHealthStatusFeature: false,
+ reportAbusePath: '/report/abuse/path',
},
stubs: {
WorkItemWeight: true,
WorkItemIteration: true,
WorkItemHealthStatus: true,
+ WorkItemNotes: true,
},
});
};
@@ -88,7 +94,6 @@ describe('Work items router', () => {
});
afterEach(() => {
- wrapper.destroy();
window.location.hash = '';
});
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
index aa24b80cf08..b8af5f10a5a 100644
--- a/spec/frontend/work_items/utils_spec.js
+++ b/spec/frontend/work_items/utils_spec.js
@@ -1,4 +1,9 @@
-import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
+import {
+ autocompleteDataSources,
+ markdownPreviewPath,
+ getWorkItemTodoOptimisticResponse,
+} from '~/work_items/utils';
+import { workItemResponseFactory } from './mock_data';
describe('autocompleteDataSources', () => {
beforeEach(() => {
@@ -25,3 +30,17 @@ describe('markdownPreviewPath', () => {
);
});
});
+
+describe('getWorkItemTodoOptimisticResponse', () => {
+ it.each`
+ scenario | pendingTodo | result
+ ${'empty'} | ${false} | ${0}
+ ${'present'} | ${true} | ${1}
+ `('returns correct response when pending item list is $scenario', ({ pendingTodo, result }) => {
+ const workItem = workItemResponseFactory({ canUpdate: true });
+ expect(
+ getWorkItemTodoOptimisticResponse({ workItem, pendingTodo }).workItemUpdate.workItem
+ .widgets[0].currentUserTodos.edges.length,
+ ).toBe(result);
+ });
+});
diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js
index 124ff5f1608..22fd7d5f48a 100644
--- a/spec/frontend/work_items_hierarchy/components/app_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/app_spec.js
@@ -24,10 +24,6 @@ describe('WorkItemsHierarchy App', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('survey banner', () => {
it('shows when the banner is visible', () => {
createComponent({}, { bannerVisible: true });
diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
index 084aaa754ab..dfdef7915dd 100644
--- a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
+++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
@@ -40,10 +40,6 @@ describe('WorkItemsHierarchy Hierarchy', () => {
);
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('available structure', () => {
let items = [];
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index 85f1dbdc305..97a9f95e8e1 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -2,8 +2,9 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Dropzone from 'dropzone';
import $ from 'jquery';
-import Mousetrap from 'mousetrap';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import htmlSnippetsShow from 'test_fixtures/snippets/show.html';
+import { Mousetrap } from '~/lib/mousetrap';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GLForm from '~/gl_form';
import * as utils from '~/lib/utils/common_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -13,7 +14,8 @@ describe('ZenMode', () => {
let mock;
let zen;
let dropzoneForElementSpy;
- const fixtureName = 'snippets/show.html';
+
+ const getTextarea = () => $('.notes-form textarea');
function enterZen() {
$('.notes-form .js-zen-enter').click();
@@ -24,7 +26,7 @@ describe('ZenMode', () => {
}
function escapeKeydown() {
- $('.notes-form textarea').trigger(
+ getTextarea().trigger(
$.Event('keydown', {
keyCode: 27,
}),
@@ -35,7 +37,7 @@ describe('ZenMode', () => {
mock = new MockAdapter(axios);
mock.onGet().reply(HTTP_STATUS_OK);
- loadHTMLFixture(fixtureName);
+ setHTMLFixture(htmlSnippetsShow);
const form = $('.js-new-note-form');
new GLForm(form); // eslint-disable-line no-new
@@ -50,6 +52,12 @@ describe('ZenMode', () => {
});
afterEach(() => {
+ $(document).off('click', '.js-zen-enter');
+ $(document).off('click', '.js-zen-leave');
+ $(document).off('zen_mode:enter');
+ $(document).off('zen_mode:leave');
+ $(document).off('keydown');
+
resetHTMLFixture();
});
@@ -62,14 +70,14 @@ describe('ZenMode', () => {
$('.div-dropzone').addClass('js-invalid-dropzone');
exitZen();
- expect(dropzoneForElementSpy.mock.calls.length).toEqual(0);
+ expect(dropzoneForElementSpy).not.toHaveBeenCalled();
});
it('should call dropzone if element is dropzone valid', () => {
$('.div-dropzone').removeClass('js-invalid-dropzone');
exitZen();
- expect(dropzoneForElementSpy.mock.calls.length).toEqual(2);
+ expect(dropzoneForElementSpy).toHaveBeenCalledTimes(1);
});
});
@@ -82,10 +90,10 @@ describe('ZenMode', () => {
});
it('removes textarea styling', () => {
- $('.notes-form textarea').attr('style', 'height: 400px');
+ getTextarea().attr('style', 'height: 400px');
enterZen();
- expect($('.notes-form textarea')).not.toHaveAttr('style');
+ expect(getTextarea()).not.toHaveAttr('style');
});
});
@@ -116,4 +124,15 @@ describe('ZenMode', () => {
expect(utils.scrollToElement).toHaveBeenCalled();
});
});
+
+ it('restores textarea style', () => {
+ const style = 'color: red; overflow-y: hidden;';
+ getTextarea().attr('style', style);
+ expect(getTextarea()).toHaveAttr('style', style);
+
+ enterZen();
+ exitZen();
+
+ expect(getTextarea()).toHaveAttr('style', style);
+ });
});