From 6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 19 Sep 2022 23:18:09 +0000 Subject: Add latest changes from gitlab-org/gitlab@15-4-stable-ee --- spec/frontend/__helpers__/datetime_helpers.js | 2 +- spec/frontend/__helpers__/dl_locator_helper.js | 13 +- .../keep_alive_component_helper_spec.js | 6 +- .../matchers/to_validate_json_schema_spec.js | 4 +- spec/frontend/__helpers__/shared_test_setup.js | 3 - spec/frontend/__mocks__/sortablejs/index.js | 2 +- .../components/access_token_table_app_spec.js | 15 + .../components/expires_at_field_spec.js | 16 + .../components/new_access_token_app_spec.js | 41 +- spec/frontend/access_tokens/index_spec.js | 2 +- .../components/add_context_commits_modal_spec.js | 12 +- .../components/review_tab_container_spec.js | 4 +- .../devops_score/components/devops_score_spec.js | 4 +- .../admin/topics/components/topic_select_spec.js | 91 ++++ .../alert_management_empty_state_spec.js | 2 +- .../alert_management_list_wrapper_spec.js | 8 +- .../components/alert_management_table_spec.js | 6 +- .../components/alert_mapping_builder_spec.js | 12 +- .../alerts_settings/components/alerts_form_spec.js | 2 +- .../components/alerts_integrations_list_spec.js | 10 +- .../components/alerts_settings_form_spec.js | 6 +- .../components/alerts_settings_wrapper_spec.js | 18 +- .../analytics/components/activity_chart_spec.js | 2 +- .../analytics/shared/components/daterange_spec.js | 15 +- .../shared/components/metric_popover_spec.js | 6 +- .../components/projects_dropdown_filter_spec.js | 6 +- .../analytics/usage_trends/components/app_spec.js | 6 +- .../components/usage_trends_count_chart_spec.js | 12 +- .../usage_trends/components/users_chart_spec.js | 8 +- spec/frontend/analytics/usage_trends/utils_spec.js | 6 +- spec/frontend/api/harbor_registry_spec.js | 107 +++++ .../keep_latest_artifact_checkbox_spec.js | 4 +- .../components/recovery_codes_spec.js | 8 +- .../authentication/two_factor_auth/index_spec.js | 2 +- spec/frontend/autosave_spec.js | 44 ++ .../badges/components/badge_settings_spec.js | 6 +- .../components/diff_file_drafts_spec.js | 4 +- .../batch_comments/components/draft_note_spec.js | 8 +- .../components/preview_dropdown_spec.js | 2 +- .../batch_comments/components/preview_item_spec.js | 2 +- .../components/publish_dropdown_spec.js | 4 +- .../batch_comments/components/review_bar_spec.js | 4 +- .../components/submit_dropdown_spec.js | 35 +- .../stores/modules/batch_comments/actions_spec.js | 3 +- spec/frontend/behaviors/bind_in_out_spec.js | 6 +- spec/frontend/blob/sketch/index_spec.js | 22 +- spec/frontend/boards/board_card_inner_spec.js | 20 + spec/frontend/boards/board_list_helper.js | 1 + .../__snapshots__/board_blocked_icon_spec.js.snap | 2 +- .../boards/components/board_blocked_icon_spec.js | 74 ++- .../components/board_card_move_to_position_spec.js | 133 ++++++ spec/frontend/boards/components/board_card_spec.js | 9 +- .../boards/components/board_new_issue_spec.js | 2 +- .../boards/components/issue_due_date_spec.js | 2 +- spec/frontend/boards/components/item_count_spec.js | 4 +- spec/frontend/boards/mock_data.js | 76 +++ spec/frontend/boards/stores/actions_spec.js | 38 +- spec/frontend/boards/stores/mutations_spec.js | 25 + .../branches/components/divergence_graph_spec.js | 4 +- spec/frontend/captcha/captcha_modal_spec.js | 2 +- .../components/lock_popovers_spec.js | 4 +- spec/frontend/chronic_duration_spec.js | 2 +- spec/frontend/ci_lint/components/ci_lint_spec.js | 6 +- .../components/secure_files_list_spec.js | 4 +- .../components/triggers_list_spec.js | 12 +- .../components/ci_project_variables_spec.js | 215 +++++++++ .../components/ci_variable_modal_spec.js | 30 +- .../components/ci_variable_popover_spec.js | 2 +- .../components/legacy_ci_variable_modal_spec.js | 8 +- spec/frontend/ci_variable_list/mocks.js | 15 +- .../ci_variable_list/store/mutations_spec.js | 2 +- .../agent_integration_status_row_spec.js | 96 ++++ .../agents/components/integration_status_spec.js | 111 +++++ .../clusters/agents/components/show_spec.js | 6 + .../clusters_list/components/agent_table_spec.js | 6 +- .../clusters_list/components/agents_spec.js | 2 +- .../components/ancestor_notice_spec.js | 2 +- .../components/clusters_main_view_spec.js | 2 +- .../clusters_list/components/clusters_spec.js | 2 +- .../components/install_agent_modal_spec.js | 2 +- .../components/node_error_help_text_spec.js | 2 +- .../code_navigation/components/app_spec.js | 4 +- .../code_navigation/components/popover_spec.js | 8 +- spec/frontend/code_navigation/utils/index_spec.js | 4 +- .../commit/commit_box_pipeline_mini_graph_spec.js | 216 ++++++++- .../commit_pipeline_status_component_spec.js | 4 +- spec/frontend/commit/mock_data.js | 211 +++++---- .../commit/pipelines/pipelines_table_spec.js | 27 ++ .../components/dropdown_spec.js | 6 +- .../__snapshots__/toolbar_link_button_spec.js.snap | 62 +-- .../components/bubble_menus/bubble_menu_spec.js | 126 +++++ .../bubble_menus/code_block_bubble_menu_spec.js | 296 ++++++++++++ .../components/bubble_menus/code_block_spec.js | 296 ------------ .../bubble_menus/formatting_bubble_menu_spec.js | 90 ++++ .../components/bubble_menus/formatting_spec.js | 87 ---- .../bubble_menus/link_bubble_menu_spec.js | 305 ++++++++++++ .../components/bubble_menus/link_spec.js | 227 --------- .../bubble_menus/media_bubble_menu_spec.js | 237 ++++++++++ .../components/bubble_menus/media_spec.js | 234 --------- .../components/content_editor_alert_spec.js | 25 + .../components/content_editor_spec.js | 213 ++++++--- .../components/editor_state_observer_spec.js | 26 +- .../components/loading_indicator_spec.js | 46 +- .../components/toolbar_image_button_spec.js | 21 +- .../components/toolbar_link_button_spec.js | 18 +- .../components/toolbar_more_dropdown_spec.js | 17 + .../components/toolbar_table_button_spec.js | 14 + .../components/toolbar_text_style_dropdown_spec.js | 4 +- .../components/wrappers/code_block_spec.js | 6 +- .../extensions/paste_markdown_spec.js | 21 +- .../remark_markdown_processing_spec.js | 73 +++ .../render_html_and_json_for_all_examples.js | 6 + .../content_editor/services/content_editor_spec.js | 95 ++-- .../services/markdown_serializer_spec.js | 21 + spec/frontend/crm/form_spec.js | 5 +- spec/frontend/crm/mock_data.js | 22 + spec/frontend/crm/organizations_root_spec.js | 92 +++- .../__snapshots__/total_time_spec.js.snap | 12 +- spec/frontend/cycle_analytics/base_spec.js | 2 +- .../cycle_analytics/path_navigation_spec.js | 12 +- .../cycle_analytics/value_stream_metrics_spec.js | 2 +- .../components/deploy_freeze_modal_spec.js | 2 +- .../components/deploy_freeze_settings_spec.js | 4 +- .../components/timezone_dropdown_spec.js | 6 +- spec/frontend/deprecated_jquery_dropdown_spec.js | 4 +- .../components/design_notes/design_note_spec.js | 1 + .../design_notes/design_reply_form_spec.js | 40 ++ .../components/design_presentation_spec.js | 2 +- .../list/__snapshots__/item_spec.js.snap | 4 +- .../components/toolbar/index_spec.js | 2 +- .../pages/__snapshots__/index_spec.js.snap | 8 +- .../pages/design/__snapshots__/index_spec.js.snap | 4 +- .../frontend/design_management/pages/index_spec.js | 2 +- spec/frontend/diffs/components/app_spec.js | 36 +- .../components/collapsed_files_warning_spec.js | 4 +- spec/frontend/diffs/components/commit_item_spec.js | 6 +- .../diffs/components/commit_widget_spec.js | 2 +- .../components/compare_dropdown_layout_spec.js | 2 +- .../diffs/components/diff_code_quality_spec.js | 3 - .../diffs/components/diff_comment_cell_spec.js | 8 +- .../frontend/diffs/components/diff_content_spec.js | 12 +- .../diffs/components/diff_discussion_reply_spec.js | 4 +- .../diffs/components/diff_discussions_spec.js | 16 +- .../diffs/components/diff_file_header_spec.js | 23 +- .../diffs/components/diff_file_row_spec.js | 8 +- spec/frontend/diffs/components/diff_file_spec.js | 12 +- .../diffs/components/diff_gutter_avatars_spec.js | 12 +- .../diffs/components/diff_line_note_form_spec.js | 2 +- spec/frontend/diffs/components/diff_line_spec.js | 65 +++ spec/frontend/diffs/components/diff_stats_spec.js | 2 +- spec/frontend/diffs/components/diff_view_spec.js | 21 +- .../diffs/components/image_diff_overlay_spec.js | 2 +- spec/frontend/diffs/components/no_changes_spec.js | 2 +- spec/frontend/diffs/components/tree_list_spec.js | 4 +- .../components/source_editor_toolbar_spec.js | 2 +- .../editor/source_editor_extension_spec.js | 2 +- .../frontend/editor/source_editor_instance_spec.js | 6 +- .../editor/source_editor_webide_ext_spec.js | 6 +- spec/frontend/emoji/components/category_spec.js | 10 +- spec/frontend/emoji/components/utils_spec.js | 4 +- spec/frontend/emoji/index_spec.js | 98 ++-- spec/frontend/environments/deployment_spec.js | 83 ++-- .../environments/environment_table_spec.js | 2 +- .../frontend/environments/environments_app_spec.js | 1 + spec/frontend/environments/graphql/mock_data.js | 38 ++ .../environments/new_environment_item_spec.js | 2 +- spec/frontend/environments/new_environment_spec.js | 2 +- .../components/error_tracking_list_spec.js | 6 +- .../components/stacktrace_entry_spec.js | 4 +- .../error_tracking_settings/components/app_spec.js | 10 +- .../components/project_dropdown_spec.js | 12 +- .../components/environments_dropdown_spec.js | 2 +- .../feature_flags/store/edit/actions_spec.js | 8 +- .../feature_flags/store/index/actions_spec.js | 8 +- .../feature_flags/store/new/actions_spec.js | 4 +- .../recent_searches_dropdown_content_spec.js | 12 +- .../filtered_search/droplab/drop_down_spec.js | 12 +- spec/frontend/fixtures/api_merge_requests.rb | 2 +- spec/frontend/fixtures/api_projects.rb | 2 +- spec/frontend/fixtures/application_settings.rb | 2 +- spec/frontend/fixtures/blob.rb | 2 +- spec/frontend/fixtures/branches.rb | 2 +- spec/frontend/fixtures/clusters.rb | 2 +- spec/frontend/fixtures/deploy_keys.rb | 8 +- spec/frontend/fixtures/groups.rb | 2 +- spec/frontend/fixtures/issues.rb | 2 +- spec/frontend/fixtures/jobs.rb | 2 +- spec/frontend/fixtures/labels.rb | 2 +- spec/frontend/fixtures/merge_requests.rb | 2 +- spec/frontend/fixtures/merge_requests_diffs.rb | 2 +- spec/frontend/fixtures/metrics_dashboard.rb | 2 +- spec/frontend/fixtures/pipeline_schedules.rb | 2 +- spec/frontend/fixtures/pipelines.rb | 2 +- spec/frontend/fixtures/projects.rb | 2 +- spec/frontend/fixtures/raw.rb | 2 +- spec/frontend/fixtures/search.rb | 69 +-- spec/frontend/fixtures/snippet.rb | 2 +- spec/frontend/fixtures/startup_css.rb | 16 +- spec/frontend/fixtures/todos.rb | 2 +- spec/frontend/flash_spec.js | 2 +- .../components/frequent_items_list_item_spec.js | 14 +- .../components/frequent_items_list_spec.js | 10 +- .../components/frequent_items_search_input_spec.js | 2 +- spec/frontend/frequent_items/utils_spec.js | 8 +- spec/frontend/google_cloud/databases/panel_spec.js | 17 + spec/frontend/google_tag_manager/index_spec.js | 45 +- spec/frontend/groups/components/app_spec.js | 14 +- .../frontend/groups/components/empty_state_spec.js | 2 +- spec/frontend/groups/components/group_item_spec.js | 34 +- spec/frontend/groups/components/groups_spec.js | 4 +- .../components/invite_members_banner_spec.js | 14 +- spec/frontend/groups/components/item_caret_spec.js | 4 +- spec/frontend/groups/components/item_stats_spec.js | 2 +- .../groups/components/item_stats_value_spec.js | 2 +- .../groups/components/item_type_icon_spec.js | 2 +- .../groups/components/overview_tabs_spec.js | 187 ++++++++ .../components/visibility_level_dropdown_spec.js | 70 --- spec/frontend/header_search/init_spec.js | 10 +- spec/frontend/header_search/mock_data.js | 44 +- spec/frontend/header_search/store/actions_spec.js | 66 ++- spec/frontend/header_search/store/getters_spec.js | 24 - .../ide/components/commit_sidebar/list_spec.js | 2 +- .../components/commit_sidebar/radio_group_spec.js | 8 +- .../ide/components/preview/navigator_spec.js | 4 +- .../ide/components/shared/tokened_input_spec.js | 2 +- spec/frontend/ide/init_gitlab_web_ide_spec.js | 62 +++ spec/frontend/ide/stores/actions/file_spec.js | 2 +- .../ide/stores/modules/commit/actions_spec.js | 2 +- .../components/group_dropdown_spec.js | 2 +- .../import_groups/components/import_table_spec.js | 18 +- .../components/import_target_cell_spec.js | 4 +- .../components/bitbucket_status_table_spec.js | 10 +- .../components/import_projects_table_spec.js | 18 +- .../components/provider_repo_table_row_spec.js | 16 +- .../import_projects/store/getters_spec.js | 4 +- .../incidents/components/incidents_list_spec.js | 14 +- .../components/incidents_settings_tabs_spec.js | 6 +- .../edit/components/trigger_fields_spec.js | 4 +- .../import_project_members_modal_spec.js | 2 +- .../components/invite_members_modal_spec.js | 32 ++ .../components/user_limit_notification_spec.js | 37 +- .../issuable/components/issue_assignees_spec.js | 2 +- .../issuable/components/issue_milestone_spec.js | 2 +- spec/frontend/issuable/issuable_form_spec.js | 231 ++++++--- .../related_issues/components/issue_token_spec.js | 6 +- .../components/related_issues_block_spec.js | 4 +- .../components/related_issues_list_spec.js | 4 +- .../issues/create_merge_request_dropdown_spec.js | 2 +- .../list/components/issue_card_time_info_spec.js | 10 +- .../issues/list/components/issues_list_app_spec.js | 29 +- .../jira_issues_import_status_app_spec.js | 10 +- .../new/components/title_suggestions_item_spec.js | 6 +- .../new/components/title_suggestions_spec.js | 2 +- .../components/related_merge_requests_spec.js | 4 +- spec/frontend/issues/show/components/app_spec.js | 2 +- .../issues/show/components/description_spec.js | 56 ++- .../issues/show/components/edit_actions_spec.js | 82 ---- .../show/components/fields/description_spec.js | 2 +- .../issues/show/components/fields/title_spec.js | 2 +- .../issues/show/components/header_actions_spec.js | 10 +- .../incidents/create_timeline_events_form_spec.js | 17 +- .../incidents/edit_timeline_event_spec.js | 44 ++ .../components/incidents/highlight_bar_spec.js | 2 +- .../components/incidents/incident_tabs_spec.js | 8 +- .../issues/show/components/incidents/mock_data.js | 42 ++ .../incidents/timeline_events_form_spec.js | 40 +- .../incidents/timeline_events_item_spec.js | 27 +- .../incidents/timeline_events_list_spec.js | 157 +++++-- .../incidents/timeline_events_tab_spec.js | 19 +- .../issues/show/components/incidents/utils_spec.js | 6 +- .../issues/show/components/pinned_links_spec.js | 2 +- .../components/sentry_error_stack_trace_spec.js | 8 +- .../branches/components/new_branch_form_spec.js | 2 +- .../jira_connect/subscriptions/api_spec.js | 118 ++++- .../add_namespace_modal/groups_list_spec.js | 2 +- .../subscriptions/components/app_spec.js | 21 +- .../components/sign_in_oauth_button_spec.js | 87 ++-- .../pages/sign_in/sign_in_gitlab_com_spec.js | 2 +- .../sign_in_gitlab_multiversion/index_spec.js | 33 +- .../subscriptions/store/actions_spec.js | 16 +- .../jira_import/components/jira_import_app_spec.js | 10 +- .../components/jira_import_form_spec.js | 15 +- .../components/jira_import_progress_spec.js | 2 +- .../components/jira_import_setup_spec.js | 2 +- .../jobs/components/artifacts_block_spec.js | 176 ------- spec/frontend/jobs/components/commit_block_spec.js | 70 --- spec/frontend/jobs/components/empty_state_spec.js | 140 ------ .../jobs/components/environments_block_spec.js | 265 ----------- spec/frontend/jobs/components/erased_block_spec.js | 63 --- .../filtered_search/jobs_filtered_search_spec.js | 34 +- .../jobs/components/filtered_search/utils_spec.js | 19 + .../jobs/components/job/artifacts_block_spec.js | 176 +++++++ .../jobs/components/job/commit_block_spec.js | 70 +++ .../jobs/components/job/empty_state_spec.js | 140 ++++++ .../jobs/components/job/environments_block_spec.js | 265 +++++++++++ .../jobs/components/job/erased_block_spec.js | 63 +++ spec/frontend/jobs/components/job/job_app_spec.js | 440 +++++++++++++++++ .../jobs/components/job/job_container_item_spec.js | 98 ++++ .../components/job/job_log_controllers_spec.js | 315 +++++++++++++ .../job/job_retry_forward_deployment_modal_spec.js | 76 +++ .../job/job_sidebar_details_container_spec.js | 140 ++++++ .../job/job_sidebar_retry_button_spec.js | 69 +++ .../jobs/components/job/jobs_container_spec.js | 147 ++++++ .../job/legacy_manual_variables_form_spec.js | 156 ++++++ .../components/job/legacy_sidebar_header_spec.js | 91 ++++ .../components/job/manual_variables_form_spec.js | 156 ++++++ .../jobs/components/job/sidebar_detail_row_spec.js | 55 +++ .../jobs/components/job/sidebar_header_spec.js | 91 ++++ spec/frontend/jobs/components/job/sidebar_spec.js | 166 +++++++ .../jobs/components/job/stages_dropdown_spec.js | 192 ++++++++ .../jobs/components/job/stuck_block_spec.js | 101 ++++ .../jobs/components/job/trigger_block_spec.js | 85 ++++ .../job/unmet_prerequisites_block_spec.js | 41 ++ spec/frontend/jobs/components/job_app_spec.js | 440 ----------------- .../jobs/components/job_container_item_spec.js | 98 ---- .../jobs/components/job_log_controllers_spec.js | 315 ------------- .../job_retry_forward_deployment_modal_spec.js | 76 --- .../job_sidebar_details_container_spec.js | 140 ------ .../components/job_sidebar_retry_button_spec.js | 69 --- .../jobs/components/jobs_container_spec.js | 147 ------ .../jobs/components/log/line_header_spec.js | 4 +- spec/frontend/jobs/components/log/line_spec.js | 2 +- .../jobs/components/manual_variables_form_spec.js | 156 ------ .../jobs/components/sidebar_detail_row_spec.js | 55 --- spec/frontend/jobs/components/sidebar_spec.js | 227 --------- .../jobs/components/stages_dropdown_spec.js | 192 -------- spec/frontend/jobs/components/stuck_block_spec.js | 101 ---- .../jobs/components/table/job_table_app_spec.js | 14 + .../frontend/jobs/components/trigger_block_spec.js | 85 ---- .../components/unmet_prerequisites_block_spec.js | 41 -- spec/frontend/jobs/store/actions_spec.js | 20 +- spec/frontend/jobs/store/mutations_spec.js | 4 +- .../labels/components/delete_label_modal_spec.js | 2 +- spec/frontend/lib/dompurify_spec.js | 2 +- spec/frontend/lib/gfm/index_spec.js | 376 ++++++++------- .../lib/utils/apollo_startup_js_link_spec.js | 2 +- spec/frontend/lib/utils/common_utils_spec.js | 2 +- .../datetime/date_calculation_utility_spec.js | 18 +- .../lib/utils/finite_state_machine_spec.js | 4 +- spec/frontend/lib/utils/is_navigating_away_spec.js | 2 +- spec/frontend/lib/utils/navigation_utility_spec.js | 2 +- spec/frontend/lib/utils/poll_spec.js | 4 +- spec/frontend/lib/utils/text_markdown_spec.js | 27 ++ spec/frontend/lib/utils/text_utility_spec.js | 35 +- .../frontend/lib/utils/vuex_module_mappers_spec.js | 2 +- spec/frontend/locale/sprintf_spec.js | 18 +- .../members/components/avatars/user_avatar_spec.js | 2 +- spec/frontend/members/mock_data.js | 1 + spec/frontend/members/store/actions_spec.js | 4 +- spec/frontend/members/utils_spec.js | 16 +- .../components/merge_conflict_resolver_app_spec.js | 4 +- .../frontend/merge_conflicts/store/actions_spec.js | 14 +- spec/frontend/merge_request_tabs_spec.js | 2 +- .../components/milestone_combobox_spec.js | 8 +- .../__snapshots__/dashboard_template_spec.js.snap | 4 +- .../components/charts/stacked_column_spec.js | 2 +- .../monitoring/components/dashboard_panel_spec.js | 2 +- .../monitoring/components/dashboard_spec.js | 10 +- .../components/dashboards_dropdown_spec.js | 3 - .../components/duplicate_dashboard_form_spec.js | 2 +- .../components/duplicate_dashboard_modal_spec.js | 2 +- spec/frontend/monitoring/store/actions_spec.js | 2 +- spec/frontend/nav/components/top_nav_app_spec.js | 5 +- .../nav/components/top_nav_dropdown_menu_spec.js | 2 +- .../nav/components/top_nav_menu_item_spec.js | 2 +- .../nav/components/top_nav_menu_sections_spec.js | 68 ++- spec/frontend/nav/mock_data.js | 2 +- spec/frontend/notebook/cells/output/latex_spec.js | 2 +- spec/frontend/notebook/index_spec.js | 10 +- spec/frontend/notebook/lib/highlight_spec.js | 15 - .../frontend/notes/components/comment_form_spec.js | 8 +- .../notes/components/discussion_counter_spec.js | 46 +- .../notes/components/discussion_filter_spec.js | 81 +++- .../notes/components/discussion_notes_spec.js | 8 +- .../discussion_resolve_with_issue_button_spec.js | 2 +- .../components/multiline_comment_form_spec.js | 2 +- .../note_actions/timeline_event_button_spec.js | 35 ++ spec/frontend/notes/components/note_body_spec.js | 10 +- spec/frontend/notes/components/note_form_spec.js | 12 +- spec/frontend/notes/components/note_header_spec.js | 30 +- .../notes/components/noteable_discussion_spec.js | 2 +- .../notes/components/noteable_note_spec.js | 2 +- .../notes/components/sort_discussion_spec.js | 102 ---- .../notes/mixins/discussion_navigation_spec.js | 31 +- spec/frontend/notes/stores/actions_spec.js | 101 ++++ spec/frontend/notes/stores/getters_spec.js | 6 +- spec/frontend/notes/stores/mutation_spec.js | 2 +- .../components/custom_notifications_modal_spec.js | 14 +- .../components/notifications_dropdown_spec.js | 11 +- .../components/metrics_settings_spec.js | 14 +- .../explorer/components/delete_button_spec.js | 4 +- .../components/details_page/delete_alert_spec.js | 4 +- .../components/details_page/details_header_spec.js | 4 +- .../details_page/partial_cleanup_alert_spec.js | 2 +- .../components/details_page/status_alert_spec.js | 4 +- .../components/details_page/tags_list_row_spec.js | 4 +- .../components/details_page/tags_loader_spec.js | 4 +- .../components/list_page/cleanup_status_spec.js | 2 +- .../components/list_page/cli_commands_spec.js | 85 ---- .../components/list_page/image_list_row_spec.js | 2 +- .../components/list_page/image_list_spec.js | 4 +- .../components/list_page/registry_header_spec.js | 2 +- .../explorer/pages/details_spec.js | 20 +- .../explorer/pages/index_spec.js | 2 +- .../components/details/artifacts_list_row_spec.js | 143 ++++++ .../components/details/artifacts_list_spec.js | 75 +++ .../components/details/details_header_spec.js | 85 ++++ .../components/list/harbor_list_header_spec.js | 9 +- .../components/list/harbor_list_row_spec.js | 38 +- .../components/list/harbor_list_spec.js | 10 +- .../components/tags/tags_header_spec.js | 52 ++ .../components/tags/tags_list_row_spec.js | 75 +++ .../components/tags/tags_list_spec.js | 66 +++ .../harbor_registry/mock_data.js | 269 ++++------- .../harbor_registry/pages/details_spec.js | 162 +++++++ .../harbor_registry/pages/index_spec.js | 2 +- .../harbor_registry/pages/list_spec.js | 42 +- .../harbor_registry/pages/tags_spec.js | 125 +++++ .../components/details/components/app_spec.js | 4 +- .../details/components/package_files_spec.js | 4 +- .../details/components/package_history_spec.js | 4 +- .../list/components/infrastructure_title_spec.js | 4 +- .../list/components/packages_list_app_spec.js | 8 +- .../list/components/packages_list_spec.js | 10 +- .../__snapshots__/package_list_row_spec.js.snap | 2 +- .../shared/infrastructure_icon_and_name_spec.js | 2 +- .../components/details/nuget_installation_spec.js | 2 +- .../__snapshots__/package_list_row_spec.js.snap | 2 +- .../components/list/package_list_row_spec.js | 6 +- .../components/list/packages_list_spec.js | 2 +- .../components/list/packages_title_spec.js | 4 +- .../list/tokens/package_type_token_spec.js | 8 +- .../package_registry/pages/details_spec.js | 14 +- .../__snapshots__/settings_titles_spec.js.snap | 18 - .../components/dependency_proxy_settings_spec.js | 2 +- .../group/components/duplicates_settings_spec.js | 143 ------ .../group/components/exceptions_input_spec.js | 108 +++++ .../group/components/generic_settings_spec.js | 54 --- .../group/components/maven_settings_spec.js | 54 --- .../group/components/package_settings_spec.js | 115 +++-- .../group/components/settings_titles_spec.js | 35 -- .../settings/components/cleanup_image_tags_spec.js | 164 +++++++ .../container_expiration_policy_form_spec.js | 109 ++--- .../components/container_expiration_policy_spec.js | 64 +-- .../components/expiration_dropdown_spec.js | 4 +- .../settings/components/expiration_input_spec.js | 6 +- .../components/expiration_run_text_spec.js | 4 +- .../settings/components/expiration_toggle_spec.js | 2 +- .../packages_cleanup_policy_form_spec.js | 2 +- .../components/registry_settings_app_spec.js | 96 +++- .../shared/components/cli_commands_spec.js | 85 ++++ .../components/package_icon_and_name_spec.js | 2 +- .../metrics_and_profiling/usage_statistics_spec.js | 2 +- .../bitbucket_server_status_table_spec.js | 4 +- .../components/bulk_imports_history_app_spec.js | 8 +- .../components/import_error_details_spec.js | 6 +- .../history/components/import_history_app_spec.js | 10 +- .../pages/profiles/show/emoji_menu_spec.js | 115 ----- .../forks/new/components/fork_form_spec.js | 173 +++---- .../forks/new/components/project_namespace_spec.js | 177 +++++++ .../pages/projects/graphs/code_coverage_spec.js | 8 +- .../merge_requests/edit/update_form_spec.js | 59 +++ .../components/pipeline_schedule_callout_spec.js | 2 +- .../permissions/components/settings_panel_spec.js | 223 ++++++--- .../shared/wikis/components/wiki_content_spec.js | 2 +- .../shared/wikis/components/wiki_form_spec.js | 50 +- .../performance_bar/components/add_request_spec.js | 2 +- .../components/detailed_metric_spec.js | 2 +- spec/frontend/persistent_user_callout_spec.js | 2 +- .../components/commit/commit_form_spec.js | 2 +- .../components/commit/commit_section_spec.js | 2 +- .../components/file-nav/branch_switcher_spec.js | 2 +- .../components/file-tree/container_spec.js | 2 +- .../header/pipeline_editor_mini_graph_spec.js | 109 +++++ .../header/pipline_editor_mini_graph_spec.js | 2 +- .../components/lint/ci_lint_results_spec.js | 4 +- .../components/lint/ci_lint_warnings_spec.js | 4 +- .../components/pipeline_editor_tabs_spec.js | 2 +- .../popovers/walkthrough_popover_spec.js | 2 +- .../components/ui/editor_tab_spec.js | 2 +- .../pipeline_editor/pipeline_editor_app_spec.js | 3 +- .../pipeline_editor/pipeline_editor_home_spec.js | 2 +- .../components/legacy_pipeline_new_form_spec.js | 456 ++++++++++++++++++ .../components/pipeline_new_form_spec.js | 12 +- .../pipeline_new/components/refs_dropdown_spec.js | 4 +- .../pipeline_wizard/components/commit_spec.js | 2 +- .../pipeline_wizard/components/editor_spec.js | 11 +- .../components/input_wrapper_spec.js | 2 +- .../pipeline_wizard/components/wrapper_spec.js | 125 +++++ spec/frontend/pipeline_wizard/mock/yaml.js | 2 + .../pipeline_wizard/pipeline_wizard_spec.js | 1 + .../components/dag/dag_annotations_spec.js | 2 +- spec/frontend/pipelines/components/dag/dag_spec.js | 10 +- .../linked_pipelines_mini_list_spec.js | 176 +++++++ .../linked_pipelines_mock_data.js | 407 ++++++++++++++++ .../pipeline_mini_graph_spec.js | 149 ++++++ .../pipeline_mini_graph/pipeline_stage_spec.js | 260 ++++++++++ .../pipeline_mini_graph/pipeline_stages_spec.js | 83 ++++ .../components/pipelines_filtered_search_spec.js | 20 +- .../pipelines_list/pipeline_mini_graph_spec.js | 83 ---- .../pipelines_list/pipeline_stage_spec.js | 259 ---------- .../pipelines/graph/action_component_spec.js | 2 +- .../pipelines/graph/graph_component_spec.js | 8 +- .../graph/graph_component_wrapper_spec.js | 24 +- .../pipelines/graph/graph_view_selector_spec.js | 61 +-- spec/frontend/pipelines/graph/job_item_spec.js | 6 +- .../pipelines/graph/job_name_component_spec.js | 2 +- .../pipelines/graph/linked_pipeline_spec.js | 16 +- .../graph/linked_pipelines_column_spec.js | 4 +- spec/frontend/pipelines/graph/mock_data.js | 242 ---------- .../pipelines/graph/stage_column_component_spec.js | 10 +- .../pipelines/graph_shared/links_layer_spec.js | 2 +- spec/frontend/pipelines/header_component_spec.js | 6 +- .../pipelines/performance_insights_modal_spec.js | 131 ------ .../pipeline_graph/pipeline_graph_spec.js | 2 +- .../pipelines/pipeline_multi_actions_spec.js | 20 + spec/frontend/pipelines/pipeline_url_spec.js | 71 ++- spec/frontend/pipelines/pipelines_actions_spec.js | 24 +- .../frontend/pipelines/pipelines_artifacts_spec.js | 5 +- spec/frontend/pipelines/pipelines_spec.js | 18 +- spec/frontend/pipelines/pipelines_table_spec.js | 72 ++- .../test_reports/test_suite_table_spec.js | 8 +- .../test_reports/test_summary_table_spec.js | 2 +- spec/frontend/pipelines/time_ago_spec.js | 4 +- .../tokens/pipeline_branch_name_token_spec.js | 7 +- .../pipelines/tokens/pipeline_source_token_spec.js | 5 +- .../pipelines/tokens/pipeline_status_token_spec.js | 7 +- .../tokens/pipeline_tag_name_token_spec.js | 7 +- .../tokens/pipeline_trigger_author_token_spec.js | 7 +- spec/frontend/pipelines/utils_spec.js | 44 +- spec/frontend/popovers/components/popovers_spec.js | 10 +- .../components/delete_account_modal_spec.js | 2 +- .../account/components/update_username_spec.js | 6 +- .../components/integration_view_spec.js | 2 +- .../components/profile_preferences_spec.js | 4 +- .../projects/commit/components/form_modal_spec.js | 8 +- .../projects/commit/store/mutations_spec.js | 2 +- .../commits/components/author_select_spec.js | 10 +- .../projects/compare/components/app_spec.js | 4 +- .../compare/components/repo_dropdown_spec.js | 4 +- .../compare/components/revision_card_spec.js | 4 +- .../components/revision_dropdown_legacy_spec.js | 4 +- .../compare/components/revision_dropdown_spec.js | 6 +- .../components/project_delete_button_spec.js | 2 +- .../components/shared/delete_button_spec.js | 2 +- .../projects/details/upload_button_spec.js | 6 +- .../pipelines/charts/components/app_spec.js | 17 +- .../components/ci_cd_analytics_charts_spec.js | 4 +- .../charts/components/pipeline_charts_spec.js | 10 +- .../components/new_access_dropdown_spec.js | 8 +- .../components/shared_runners_toggle_spec.js | 6 +- .../settings/repository/branch_rules/app_spec.js | 49 +- .../branch_rules/components/branch_rule_spec.js | 58 +++ .../settings/repository/branch_rules/mock_data.js | 25 + .../components/service_desk_root_spec.js | 40 +- .../components/service_desk_setting_spec.js | 37 +- .../service_desk_template_dropdown_spec.js | 6 +- spec/frontend/ref/components/ref_selector_spec.js | 17 +- .../components/related_issuable_input_spec.js | 8 +- .../releases/components/app_edit_new_spec.js | 4 +- spec/frontend/releases/components/app_show_spec.js | 4 +- .../releases/components/asset_links_form_spec.js | 14 +- .../releases/components/evidence_block_spec.js | 14 +- .../releases/components/issuable_stats_spec.js | 8 +- .../components/release_block_assets_spec.js | 2 +- .../components/release_block_footer_spec.js | 12 +- .../components/release_block_header_spec.js | 2 +- .../release_block_milestone_info_spec.js | 8 +- .../releases/components/release_block_spec.js | 6 +- .../components/release_skeleton_loader_spec.js | 2 +- .../releases/components/tag_field_exsting_spec.js | 2 +- .../releases/components/tag_field_new_spec.js | 6 +- .../frontend/releases/components/tag_field_spec.js | 4 +- .../releases/stores/modules/detail/actions_spec.js | 26 + .../releases/stores/modules/detail/getters_spec.js | 19 +- .../stores/modules/detail/mutations_spec.js | 7 + .../components/accessibility_issue_body_spec.js | 8 +- .../grouped_accessibility_reports_app_spec.js | 2 +- .../components/codequality_issue_body_spec.js | 2 +- .../grouped_codequality_reports_app_spec.js | 2 +- .../reports/components/grouped_issues_list_spec.js | 8 +- .../reports/components/report_item_spec.js | 4 +- .../grouped_test_report/components/modal_spec.js | 4 +- .../grouped_test_report/store/actions_spec.js | 4 +- .../new_failures_with_null_files_report.json | 40 ++ .../__snapshots__/last_commit_spec.js.snap | 3 +- .../components/blob_button_group_spec.js | 10 +- .../components/blob_content_viewer_spec.js | 49 +- .../components/blob_viewers/csv_viewer_spec.js | 2 +- .../repository/components/breadcrumbs_spec.js | 20 +- .../components/delete_blob_modal_spec.js | 2 +- .../repository/components/last_commit_spec.js | 9 +- .../components/new_directory_modal_spec.js | 2 +- .../repository/components/preview/index_spec.js | 2 +- .../repository/components/table/index_spec.js | 39 +- .../components/upload_blob_modal_spec.js | 2 +- spec/frontend/repository/log_tree_spec.js | 8 +- spec/frontend/repository/mock_data.js | 1 + spec/frontend/repository/utils/commit_spec.js | 2 +- .../admin_runner_edit_app_spec.js | 113 ----- .../admin_runner_show_app_spec.js | 2 +- .../runner/admin_runners/admin_runners_app_spec.js | 30 +- .../cells/runner_stacked_summary_cell_spec.js | 164 +++++++ .../components/cells/runner_status_cell_spec.js | 21 +- .../components/cells/runner_summary_cell_spec.js | 91 ---- .../components/cells/runner_summary_field_spec.js | 49 ++ .../runner/components/runner_details_spec.js | 30 +- .../runner/components/runner_header_spec.js | 6 +- .../frontend/runner/components/runner_list_spec.js | 75 +-- .../runner/components/runner_paused_badge_spec.js | 5 +- .../runner/components/runner_projects_spec.js | 65 ++- .../runner_stacked_layout_banner_spec.js | 39 ++ .../runner/components/runner_status_badge_spec.js | 20 +- spec/frontend/runner/components/runner_tag_spec.js | 4 +- .../frontend/runner/components/runner_tags_spec.js | 4 +- .../runner/components/runner_type_badge_spec.js | 17 +- .../runner/components/runner_type_tabs_spec.js | 2 +- .../runner/components/runner_update_form_spec.js | 7 + .../runner/components/stat/runner_stats_spec.js | 33 +- .../runner/group_runners/group_runners_app_spec.js | 55 ++- .../runner/runner_edit/runner_edit_app_spec.js | 114 +++++ spec/frontend/runner/utils_spec.js | 13 +- .../search/sidebar/components/radio_filter_spec.js | 2 +- spec/frontend/search/sort/components/app_spec.js | 2 +- .../set_status_modal/set_status_form_spec.js | 167 +++++++ .../set_status_modal_wrapper_spec.js | 45 +- .../user_profile_set_status_wrapper_spec.js | 156 ++++++ spec/frontend/set_status_modal/utils_spec.js | 3 +- spec/frontend/sidebar/assignee_title_spec.js | 4 +- .../assignees/assignee_avatar_link_spec.js | 2 +- .../assignees/collapsed_assignee_list_spec.js | 2 +- .../assignees/sidebar_assignees_widget_spec.js | 12 + .../assignees/uncollapsed_assignee_list_spec.js | 8 +- .../assignees/user_name_with_status_spec.js | 2 +- .../sidebar_confidentiality_form_spec.js | 25 +- .../sidebar_confidentiality_widget_spec.js | 5 +- .../components/date/sidebar_inherit_date_spec.js | 12 +- .../reviewers/uncollapsed_reviewer_list_spec.js | 4 +- .../components/severity/sidebar_severity_spec.js | 2 +- .../components/sidebar_dropdown_widget_spec.js | 18 + .../sidebar_subscriptions_widget_spec.js | 2 +- .../components/time_tracking/report_spec.js | 2 - spec/frontend/sidebar/issuable_assignees_spec.js | 23 +- .../sidebar/lock/issuable_lock_form_spec.js | 2 +- spec/frontend/sidebar/mock_data.js | 10 + spec/frontend/sidebar/sidebar_mediator_spec.js | 2 +- spec/frontend/sidebar/sidebar_move_issue_spec.js | 11 +- spec/frontend/sidebar/todo_spec.js | 2 +- spec/frontend/snippets/components/edit_spec.js | 38 +- spec/frontend/snippets/components/show_spec.js | 22 +- .../components/snippet_blob_actions_edit_spec.js | 2 +- .../snippets/components/snippet_blob_view_spec.js | 4 +- .../components/snippet_visibility_edit_spec.js | 20 +- .../surveys/merge_request_performance/app_spec.js | 74 ++- .../terraform/components/states_table_spec.js | 2 +- spec/frontend/token_access/mock_data.js | 13 - spec/frontend/token_access/token_access_spec.js | 28 +- spec/frontend/tooltips/components/tooltips_spec.js | 4 +- .../user_lists/store/index/actions_spec.js | 4 +- .../components/added_commit_message_spec.js | 13 +- .../components/artifacts_list_spec.js | 6 +- .../components/mr_widget_pipeline_spec.js | 28 +- .../components/mr_widget_rebase_spec.js | 75 ++- .../components/mr_widget_status_icon_spec.js | 40 +- .../components/mr_widget_suggest_pipeline_spec.js | 2 +- .../mr_widget_auto_merge_enabled_spec.js.snap | 468 +++++++++++------- .../mr_widget_pipeline_failed_spec.js.snap | 24 - .../components/states/mr_widget_archived_spec.js | 21 +- .../components/states/mr_widget_checking_spec.js | 22 +- .../components/states/mr_widget_closed_spec.js | 65 ++- .../mr_widget_commit_message_dropdown_spec.js | 2 +- .../states/mr_widget_failed_to_merge_spec.js | 15 +- .../states/mr_widget_not_allowed_spec.js | 20 +- .../states/mr_widget_pipeline_blocked_spec.js | 19 +- .../states/mr_widget_pipeline_failed_spec.js | 17 +- .../states/mr_widget_ready_to_merge_spec.js | 4 +- .../mr_widget_terraform_container_spec.js | 3 +- .../components/widget/app_spec.js | 4 +- .../widget/widget_content_section_spec.js | 39 ++ .../components/widget/widget_spec.js | 174 ++++++- .../deployment/deployment_actions_spec.js | 2 +- .../extensions/test_report/index_spec.js | 10 + .../mr_widget_options_spec.js | 4 +- .../stores/artifacts_list/actions_spec.js | 4 +- .../vue_shared/alert_details/alert_details_spec.js | 2 +- .../vue_shared/alert_details/alert_metrics_spec.js | 2 +- .../__snapshots__/code_block_spec.js.snap | 26 - .../vue_shared/components/ci_badge_link_spec.js | 22 +- .../components/code_block_highlighted_spec.js | 65 +++ .../vue_shared/components/code_block_spec.js | 82 +++- .../components/diff_stats_dropdown_spec.js | 4 +- .../vue_shared/components/gl_modal_vuex_spec.js | 4 +- .../vue_shared/components/paginated_list_spec.js | 2 +- .../components/registry/registry_search_spec.js | 2 +- .../sidebar/labels_select_widget/mock_data.js | 2 +- .../components/source_viewer/source_viewer_spec.js | 9 +- .../__snapshots__/upload_dropzone_spec.js.snap | 14 +- .../components/user_callout_dismisser_spec.js | 24 +- .../components/user_popover/user_popover_spec.js | 41 +- .../show/components/issuable_edit_form_spec.js | 6 +- .../components/manage_via_mr_spec.js | 4 +- .../work_items/components/item_title_spec.js | 2 +- .../components/work_item_actions_spec.js | 48 +- .../components/work_item_assignees_spec.js | 79 +++- .../components/work_item_description_spec.js | 14 +- .../components/work_item_detail_modal_spec.js | 2 +- .../work_items/components/work_item_detail_spec.js | 522 +++++++++++++++++++++ .../components/work_item_due_date_spec.js | 346 ++++++++++++++ .../components/work_item_information_spec.js | 9 +- .../work_items/components/work_item_labels_spec.js | 6 +- .../work_item_links/work_item_link_child_spec.js | 122 +++++ .../work_item_links/work_item_links_spec.js | 98 ++-- .../work_items/components/work_item_state_spec.js | 5 +- .../work_items/components/work_item_title_spec.js | 6 +- .../components/work_item_type_icon_spec.js | 39 +- .../work_items/components/work_item_weight_spec.js | 214 --------- spec/frontend/work_items/mock_data.js | 258 ++++++++-- .../work_items/pages/create_work_item_spec.js | 4 +- .../work_items/pages/work_item_detail_spec.js | 484 ------------------- spec/frontend/work_items/router_spec.js | 39 +- .../components/hierarchy_spec.js | 2 +- 721 files changed, 17226 insertions(+), 9955 deletions(-) create mode 100644 spec/frontend/admin/topics/components/topic_select_spec.js create mode 100644 spec/frontend/api/harbor_registry_spec.js create mode 100644 spec/frontend/boards/components/board_card_move_to_position_spec.js create mode 100644 spec/frontend/ci_variable_list/components/ci_project_variables_spec.js create mode 100644 spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js create mode 100644 spec/frontend/clusters/agents/components/integration_status_spec.js create mode 100644 spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js create mode 100644 spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js delete mode 100644 spec/frontend/content_editor/components/bubble_menus/code_block_spec.js create mode 100644 spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js delete mode 100644 spec/frontend/content_editor/components/bubble_menus/formatting_spec.js create mode 100644 spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js delete mode 100644 spec/frontend/content_editor/components/bubble_menus/link_spec.js create mode 100644 spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js delete mode 100644 spec/frontend/content_editor/components/bubble_menus/media_spec.js create mode 100644 spec/frontend/diffs/components/diff_line_spec.js create mode 100644 spec/frontend/groups/components/overview_tabs_spec.js delete mode 100644 spec/frontend/groups/components/visibility_level_dropdown_spec.js create mode 100644 spec/frontend/ide/init_gitlab_web_ide_spec.js create mode 100644 spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js delete mode 100644 spec/frontend/jobs/components/artifacts_block_spec.js delete mode 100644 spec/frontend/jobs/components/commit_block_spec.js delete mode 100644 spec/frontend/jobs/components/empty_state_spec.js delete mode 100644 spec/frontend/jobs/components/environments_block_spec.js delete mode 100644 spec/frontend/jobs/components/erased_block_spec.js create mode 100644 spec/frontend/jobs/components/filtered_search/utils_spec.js create mode 100644 spec/frontend/jobs/components/job/artifacts_block_spec.js create mode 100644 spec/frontend/jobs/components/job/commit_block_spec.js create mode 100644 spec/frontend/jobs/components/job/empty_state_spec.js create mode 100644 spec/frontend/jobs/components/job/environments_block_spec.js create mode 100644 spec/frontend/jobs/components/job/erased_block_spec.js create mode 100644 spec/frontend/jobs/components/job/job_app_spec.js create mode 100644 spec/frontend/jobs/components/job/job_container_item_spec.js create mode 100644 spec/frontend/jobs/components/job/job_log_controllers_spec.js create mode 100644 spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js create mode 100644 spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js create mode 100644 spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js create mode 100644 spec/frontend/jobs/components/job/jobs_container_spec.js create mode 100644 spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js create mode 100644 spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js create mode 100644 spec/frontend/jobs/components/job/manual_variables_form_spec.js create mode 100644 spec/frontend/jobs/components/job/sidebar_detail_row_spec.js create mode 100644 spec/frontend/jobs/components/job/sidebar_header_spec.js create mode 100644 spec/frontend/jobs/components/job/sidebar_spec.js create mode 100644 spec/frontend/jobs/components/job/stages_dropdown_spec.js create mode 100644 spec/frontend/jobs/components/job/stuck_block_spec.js create mode 100644 spec/frontend/jobs/components/job/trigger_block_spec.js create mode 100644 spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js delete mode 100644 spec/frontend/jobs/components/job_app_spec.js delete mode 100644 spec/frontend/jobs/components/job_container_item_spec.js delete mode 100644 spec/frontend/jobs/components/job_log_controllers_spec.js delete mode 100644 spec/frontend/jobs/components/job_retry_forward_deployment_modal_spec.js delete mode 100644 spec/frontend/jobs/components/job_sidebar_details_container_spec.js delete mode 100644 spec/frontend/jobs/components/job_sidebar_retry_button_spec.js delete mode 100644 spec/frontend/jobs/components/jobs_container_spec.js delete mode 100644 spec/frontend/jobs/components/manual_variables_form_spec.js delete mode 100644 spec/frontend/jobs/components/sidebar_detail_row_spec.js delete mode 100644 spec/frontend/jobs/components/sidebar_spec.js delete mode 100644 spec/frontend/jobs/components/stages_dropdown_spec.js delete mode 100644 spec/frontend/jobs/components/stuck_block_spec.js delete mode 100644 spec/frontend/jobs/components/trigger_block_spec.js delete mode 100644 spec/frontend/jobs/components/unmet_prerequisites_block_spec.js delete mode 100644 spec/frontend/notebook/lib/highlight_spec.js create mode 100644 spec/frontend/notes/components/note_actions/timeline_event_button_spec.js delete mode 100644 spec/frontend/notes/components/sort_discussion_spec.js delete mode 100644 spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js create mode 100644 spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js delete mode 100644 spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap delete mode 100644 spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js create mode 100644 spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js delete mode 100644 spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js delete mode 100644 spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js delete mode 100644 spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js create mode 100644 spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js create mode 100644 spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js delete mode 100644 spec/frontend/pages/profiles/show/emoji_menu_spec.js create mode 100644 spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js create mode 100644 spec/frontend/pages/projects/merge_requests/edit/update_form_spec.js create mode 100644 spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js create mode 100644 spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js create mode 100644 spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js create mode 100644 spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js create mode 100644 spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js create mode 100644 spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js create mode 100644 spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js delete mode 100644 spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js delete mode 100644 spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js delete mode 100644 spec/frontend/pipelines/performance_insights_modal_spec.js create mode 100644 spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js create mode 100644 spec/frontend/projects/settings/repository/branch_rules/mock_data.js create mode 100644 spec/frontend/reports/mock_data/new_failures_with_null_files_report.json delete mode 100644 spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js create mode 100644 spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js delete mode 100644 spec/frontend/runner/components/cells/runner_summary_cell_spec.js create mode 100644 spec/frontend/runner/components/cells/runner_summary_field_spec.js create mode 100644 spec/frontend/runner/components/runner_stacked_layout_banner_spec.js create mode 100644 spec/frontend/runner/runner_edit/runner_edit_app_spec.js create mode 100644 spec/frontend/set_status_modal/set_status_form_spec.js create mode 100644 spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js delete mode 100644 spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap create mode 100644 spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js delete mode 100644 spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap create mode 100644 spec/frontend/vue_shared/components/code_block_highlighted_spec.js create mode 100644 spec/frontend/work_items/components/work_item_detail_spec.js create mode 100644 spec/frontend/work_items/components/work_item_due_date_spec.js create mode 100644 spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js delete mode 100644 spec/frontend/work_items/components/work_item_weight_spec.js delete mode 100644 spec/frontend/work_items/pages/work_item_detail_spec.js (limited to 'spec/frontend') diff --git a/spec/frontend/__helpers__/datetime_helpers.js b/spec/frontend/__helpers__/datetime_helpers.js index 25dbd1d477d..cbe627b7968 100644 --- a/spec/frontend/__helpers__/datetime_helpers.js +++ b/spec/frontend/__helpers__/datetime_helpers.js @@ -1,4 +1,4 @@ -import dateFormat from 'dateformat'; +import dateFormat from '~/lib/dateformat'; /** * Returns a date object corresponding to the given date string. diff --git a/spec/frontend/__helpers__/dl_locator_helper.js b/spec/frontend/__helpers__/dl_locator_helper.js index b507dcd599d..591c034be9b 100644 --- a/spec/frontend/__helpers__/dl_locator_helper.js +++ b/spec/frontend/__helpers__/dl_locator_helper.js @@ -19,10 +19,13 @@ import { createWrapper, ErrorWrapper } from '@vue/test-utils'; * @returns Wrapper */ export const findDd = (dtLabel, wrapper) => { - const dt = wrapper.findByText(dtLabel).element; - const dd = dt.nextElementSibling; - if (dt.tagName === 'DT' && dd.tagName === 'DD') { - return createWrapper(dd, {}); + const dtw = wrapper.findByText(dtLabel); + if (dtw.exists()) { + const dt = dtw.element; + const dd = dt.nextElementSibling; + if (dt.tagName === 'DT' && dd.tagName === 'DD') { + return createWrapper(dd, {}); + } } - return ErrorWrapper(dtLabel); + return new ErrorWrapper(dtLabel); }; diff --git a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js index dcccc14f396..54d397d0997 100644 --- a/spec/frontend/__helpers__/keep_alive_component_helper_spec.js +++ b/spec/frontend/__helpers__/keep_alive_component_helper_spec.js @@ -17,16 +17,16 @@ describe('keepAlive', () => { }); it('converts a component to a keep-alive component', async () => { - const { element } = wrapper.find(component); + const { element } = wrapper.findComponent(component); await wrapper.vm.deactivate(); - expect(wrapper.find(component).exists()).toBe(false); + expect(wrapper.findComponent(component).exists()).toBe(false); await wrapper.vm.activate(); // assert that when the component is destroyed and re-rendered, the // newly rendered component has the reference to the old component // (i.e. the old component was deactivated and activated) - expect(wrapper.find(component).element).toBe(element); + expect(wrapper.findComponent(component).element).toBe(element); }); }); diff --git a/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js b/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js index fd42c710c65..e6096221528 100644 --- a/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js +++ b/spec/frontend/__helpers__/matchers/to_validate_json_schema_spec.js @@ -38,7 +38,7 @@ describe('custom matcher toValidateJsonSchema', () => { }); it('throws if not matching', () => { - expect(() => expect(null).toValidateJsonSchema(schema)).toThrowError( + expect(() => expect(null).toValidateJsonSchema(schema)).toThrow( `Expected the given data to pass the schema validation, but found that it was considered invalid. Errors: Error with item : must be object`, ); @@ -57,7 +57,7 @@ Error with item : must be object`, }); it('throws if matching', () => { - expect(() => expect({ fruit: 'apple' }).not.toValidateJsonSchema(schema)).toThrowError( + expect(() => expect({ fruit: 'apple' }).not.toValidateJsonSchema(schema)).toThrow( 'Expected the given data not to pass the schema validation, but found that it was considered valid.', ); }); diff --git a/spec/frontend/__helpers__/shared_test_setup.js b/spec/frontend/__helpers__/shared_test_setup.js index 4d6486544ca..45a7b8e0352 100644 --- a/spec/frontend/__helpers__/shared_test_setup.js +++ b/spec/frontend/__helpers__/shared_test_setup.js @@ -48,9 +48,6 @@ testUtilsConfig.deprecationWarningHandler = (method, message) => { const ALLOWED_DEPRECATED_METHODS = [ // https://gitlab.com/gitlab-org/gitlab/-/issues/295679 'finding components with `find` or `get`', - - // https://gitlab.com/gitlab-org/gitlab/-/issues/295680 - 'finding components with `findAll`', ]; if (!ALLOWED_DEPRECATED_METHODS.includes(method)) { global.console.error(message); diff --git a/spec/frontend/__mocks__/sortablejs/index.js b/spec/frontend/__mocks__/sortablejs/index.js index 5039af54542..d8bc8ae9bda 100644 --- a/spec/frontend/__mocks__/sortablejs/index.js +++ b/spec/frontend/__mocks__/sortablejs/index.js @@ -1,4 +1,4 @@ -const Sortablejs = jest.genMockFromModule('sortablejs'); +const Sortablejs = jest.createMockFromModule('sortablejs'); export default Sortablejs; export const Sortable = Sortablejs; diff --git a/spec/frontend/access_tokens/components/access_token_table_app_spec.js b/spec/frontend/access_tokens/components/access_token_table_app_spec.js index 6013fa3ec39..aed3db4aa4c 100644 --- a/spec/frontend/access_tokens/components/access_token_table_app_spec.js +++ b/spec/frontend/access_tokens/components/access_token_table_app_spec.js @@ -190,6 +190,21 @@ describe('~/access_tokens/components/access_token_table_app', () => { expect(button.props('category')).toBe('tertiary'); }); + describe('revoke path', () => { + beforeEach(() => { + createComponent({ showRole: true }); + }); + + it.each([{ revoke_path: null }, { revoke_path: undefined }])( + 'with %p, does not show revoke button', + async (input) => { + await triggerSuccess(defaultActiveAccessTokens.map((data) => ({ ...data, ...input }))); + + expect(findCells().at(6).findComponent(GlButton).exists()).toBe(false); + }, + ); + }); + it('sorts rows alphabetically', async () => { createComponent({ showRole: true }); await triggerSuccess(); 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 646dc0d703f..491d2a0e323 100644 --- a/spec/frontend/access_tokens/components/expires_at_field_spec.js +++ b/spec/frontend/access_tokens/components/expires_at_field_spec.js @@ -58,4 +58,20 @@ describe('~/access_tokens/components/expires_at_field', () => { expect(findDatepicker().props('defaultDate')).toStrictEqual(future); }); + + it('should set the default expiration date to be 365 days', () => { + const offset = 365; + const today = new Date(); + const future = getDateInFuture(today, offset); + createComponent({ defaultDateOffset: offset }); + + expect(findDatepicker().props('defaultDate')).toStrictEqual(future); + }); + + it('should set the default expiration date to maxDate, ignoring defaultDateOffset', () => { + const maxDate = new Date(); + createComponent({ maxDate, defaultDateOffset: 2 }); + + expect(findDatepicker().props('defaultDate')).toStrictEqual(maxDate); + }); }); 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 9ccadbebf7a..d12d200d214 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 @@ -23,18 +23,27 @@ describe('~/access_tokens/components/new_access_token_app', () => { }; const triggerSuccess = async (newToken = 'new token') => { - wrapper.find(DomElementListener).vm.$emit(EVENT_SUCCESS, { detail: [{ new_token: newToken }] }); + wrapper + .findComponent(DomElementListener) + .vm.$emit(EVENT_SUCCESS, { detail: [{ new_token: newToken }] }); await nextTick(); }; const triggerError = async (errors = ['1', '2']) => { - wrapper.find(DomElementListener).vm.$emit(EVENT_ERROR, { detail: [{ errors }] }); + wrapper.findComponent(DomElementListener).vm.$emit(EVENT_ERROR, { detail: [{ errors }] }); await nextTick(); }; beforeEach(() => { // NewAccessTokenApp observes a form element - setHTMLFixture(`
`); + setHTMLFixture( + `
+ + + + +
`, + ); createComponent(); }); @@ -78,7 +87,6 @@ describe('~/access_tokens/components/new_access_token_app', () => { .findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType })) .attributes(); expect(inputAttributes).toMatchObject({ - class: expect.stringContaining('qa-created-access-token'), 'data-qa-selector': 'created_access_token_field', }); }); @@ -94,12 +102,29 @@ describe('~/access_tokens/components/new_access_token_app', () => { }); }); - it('should reset the form', async () => { - const resetSpy = jest.spyOn(wrapper.vm.form, 'reset'); + describe('when resetting the form', () => { + it('should reset selectively some input fields', async () => { + expect(document.querySelector('input[type=text]:not([id$=expires_at])').value).toBe('1'); + expect(document.querySelector('input[type=checkbox]').checked).toBe(true); + await triggerSuccess(); - await triggerSuccess(); + expect(document.querySelector('input[type=text]:not([id$=expires_at])').value).toBe(''); + expect(document.querySelector('input[type=checkbox]').checked).toBe(false); + }); - expect(resetSpy).toHaveBeenCalled(); + it('should not reset the date field', async () => { + expect(document.querySelector('input[type=text][id$=expires_at]').value).toBe('2022-01-01'); + await triggerSuccess(); + + expect(document.querySelector('input[type=text][id$=expires_at]').value).toBe('2022-01-01'); + }); + + it('should not reset the submit button value', async () => { + expect(document.querySelector('input[type=submit]').value).toBe('Create'); + await triggerSuccess(); + + expect(document.querySelector('input[type=submit]').value).toBe('Create'); + }); }); }); diff --git a/spec/frontend/access_tokens/index_spec.js b/spec/frontend/access_tokens/index_spec.js index 0c611a4a512..55575ab25fc 100644 --- a/spec/frontend/access_tokens/index_spec.js +++ b/spec/frontend/access_tokens/index_spec.js @@ -182,7 +182,7 @@ describe('access tokens', () => { }); describe('initTokensApp', () => { - it('mounts the component and provides`tokenTypes` ', () => { + it('mounts the component and provides`tokenTypes`', () => { const tokensData = { [FEED_TOKEN]: FEED_TOKEN, [INCOMING_EMAIL_TOKEN]: INCOMING_EMAIL_TOKEN, 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 bffadbde087..1d57473943b 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 @@ -48,8 +48,8 @@ describe('AddContextCommitsModal', () => { return wrapper; }; - const findModal = () => wrapper.find(GlModal); - const findSearch = () => wrapper.find(GlSearchBoxByType); + const findModal = () => wrapper.findComponent(GlModal); + const findSearch = () => wrapper.findComponent(GlSearchBoxByType); beforeEach(() => { wrapper = createWrapper(); @@ -75,7 +75,7 @@ describe('AddContextCommitsModal', () => { 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.toBeCalled(); + expect(searchCommits).not.toHaveBeenCalled(); jest.advanceTimersByTime(500); expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText); }); @@ -107,12 +107,12 @@ describe('AddContextCommitsModal', () => { it('a disabled ok button in first tab, when row is selected in second tab', () => { createWrapper({ selectedContextCommits: [commit] }); - expect(wrapper.find(GlModal).attributes('ok-disabled')).toBe('true'); + expect(wrapper.findComponent(GlModal).attributes('ok-disabled')).toBe('true'); }); }); describe('has an ok button when clicked calls action', () => { - it('"createContextCommits" when only new commits to be added ', async () => { + it('"createContextCommits" when only new commits to be added', async () => { wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }]; findModal().vm.$emit('ok'); await nextTick(); @@ -121,7 +121,7 @@ describe('AddContextCommitsModal', () => { forceReload: true, }); }); - it('"removeContextCommits" when only added commits are to be removed ', async () => { + it('"removeContextCommits" when only added commits are to be removed', async () => { wrapper.vm.$store.state.toRemoveCommits = [commit.short_id]; findModal().vm.$emit('ok'); await nextTick(); 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 85ecb4313c2..f679576182f 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 @@ -32,7 +32,7 @@ describe('ReviewTabContainer', () => { it('shows loading icon when commits are being loaded', () => { createWrapper({ isLoading: true }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('shows loading error text when API call fails', () => { @@ -46,6 +46,6 @@ describe('ReviewTabContainer', () => { it('renders all passed commits as list', () => { createWrapper({ commits: [commit] }); - expect(wrapper.findAll(CommitItem).length).toBe(1); + expect(wrapper.findAllComponents(CommitItem).length).toBe(1); }); }); diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js index 534af2a3033..de56e843eb9 100644 --- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js +++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js @@ -34,7 +34,7 @@ describe('DevopsScore', () => { createComponent({ devopsScoreMetrics: {} }); }); - it('includes the DevopsScoreCallout component ', () => { + it('includes the DevopsScoreCallout component', () => { expect(bannerExists()).toBe(true); }); @@ -67,7 +67,7 @@ describe('DevopsScore', () => { createComponent(); }); - it('includes the DevopsScoreCallout component ', () => { + it('includes the DevopsScoreCallout component', () => { expect(bannerExists()).toBe(true); }); diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js new file mode 100644 index 00000000000..f61af6203f0 --- /dev/null +++ b/spec/frontend/admin/topics/components/topic_select_spec.js @@ -0,0 +1,91 @@ +import { GlAvatarLabeled, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import TopicSelect from '~/admin/topics/components/topic_select.vue'; + +const mockTopics = [ + { id: 1, name: 'topic1', title: 'Topic 1', avatarUrl: 'avatar.com/topic1.png' }, + { id: 2, name: 'GitLab', title: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' }, +]; + +describe('TopicSelect', () => { + let wrapper; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + + function createComponent(props = {}) { + wrapper = shallowMount(TopicSelect, { + propsData: props, + data() { + return { + topics: mockTopics, + search: '', + }; + }, + mocks: { + $apollo: { + queries: { + topics: { loading: false }, + }, + }, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('mounts', () => { + createComponent(); + + expect(wrapper.exists()).toBe(true); + }); + + it('`selectedTopic` prop defaults to `{}`', () => { + createComponent(); + + expect(wrapper.props('selectedTopic')).toEqual({}); + }); + + it('`labelText` prop defaults to `null`', () => { + createComponent(); + + expect(wrapper.props('labelText')).toBe(null); + }); + + it('renders default text if no selected topic', () => { + createComponent(); + + expect(findDropdown().props('text')).toBe('Select a topic'); + }); + + it('renders selected topic', () => { + createComponent({ selectedTopic: mockTopics[0] }); + + expect(findDropdown().props('text')).toBe('topic1'); + }); + + it('renders label', () => { + createComponent({ labelText: 'my label' }); + + expect(wrapper.find('label').text()).toBe('my label'); + }); + + it('renders dropdown items', () => { + createComponent(); + + const dropdownItems = findAllDropdownItems(); + + expect(dropdownItems.at(0).findComponent(GlAvatarLabeled).props('label')).toBe('Topic 1'); + expect(dropdownItems.at(1).findComponent(GlAvatarLabeled).props('label')).toBe('GitLab'); + }); + + it('emits `click` event when topic selected', () => { + createComponent(); + + findAllDropdownItems().at(0).vm.$emit('click'); + + expect(wrapper.emitted('click')).toEqual([[mockTopics[0]]]); + }); +}); 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 c2bf90e7635..0d6bc1b74fb 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 @@ -25,7 +25,7 @@ describe('AlertManagementEmptyState', () => { } }); - const EmptyState = () => wrapper.find(GlEmptyState); + const EmptyState = () => wrapper.findComponent(GlEmptyState); describe('Empty state', () => { it('shows 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 bba5fcbbf08..3a5fb99fdf1 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 @@ -28,8 +28,8 @@ describe('AlertManagementList', () => { describe('Alert List Wrapper', () => { it('should show the empty state when alerts are not enabled', () => { - expect(wrapper.find(AlertManagementEmptyState).exists()).toBe(true); - expect(wrapper.find(AlertManagementTable).exists()).toBe(false); + expect(wrapper.findComponent(AlertManagementEmptyState).exists()).toBe(true); + expect(wrapper.findComponent(AlertManagementTable).exists()).toBe(false); }); it('should show the alerts table when alerts are enabled', () => { @@ -39,8 +39,8 @@ describe('AlertManagementList', () => { }, }); - expect(wrapper.find(AlertManagementEmptyState).exists()).toBe(false); - expect(wrapper.find(AlertManagementTable).exists()).toBe(true); + expect(wrapper.findComponent(AlertManagementEmptyState).exists()).toBe(false); + expect(wrapper.findComponent(AlertManagementTable).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 5b823694b99..3e1438c37d6 100644 --- a/spec/frontend/alert_management/components/alert_management_table_spec.js +++ b/spec/frontend/alert_management/components/alert_management_table_spec.js @@ -172,8 +172,8 @@ describe('AlertManagementTable', () => { await nextTick(); - expect(wrapper.find(GlTable).exists()).toBe(true); - expect(findAlertsTable().find(GlIcon).classes('icon-critical')).toBe(true); + expect(wrapper.findComponent(GlTable).exists()).toBe(true); + expect(findAlertsTable().findComponent(GlIcon).classes('icon-critical')).toBe(true); }); it('renders severity text', () => { @@ -200,7 +200,7 @@ describe('AlertManagementTable', () => { loading: false, }); - const avatar = findAssignees().at(1).find(GlAvatar); + const avatar = findAssignees().at(1).findComponent(GlAvatar); const { src, label } = avatar.attributes(); const { name, avatarUrl } = mockAlerts[1].assignees.nodes[0]; 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 dba9c8be669..1e125bdfd3a 100644 --- a/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js +++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js @@ -47,7 +47,7 @@ describe('AlertMappingBuilder', () => { expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle); expect(findColumnInRow(0, 3).text()).toContain(i18n.columns.fallbackKeyTitle); - const fallbackColumnIcon = findColumnInRow(0, 3).find(GlIcon); + const fallbackColumnIcon = findColumnInRow(0, 3).findComponent(GlIcon); expect(fallbackColumnIcon.exists()).toBe(true); expect(fallbackColumnIcon.attributes('name')).toBe('question'); expect(fallbackColumnIcon.attributes('title')).toBe(i18n.fallbackTooltip); @@ -55,7 +55,7 @@ describe('AlertMappingBuilder', () => { it('renders disabled form input for each mapped field', () => { alertFields.forEach((field, index) => { - const input = findColumnInRow(index + 1, 0).find(GlFormInput); + const input = findColumnInRow(index + 1, 0).findComponent(GlFormInput); const types = field.types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or '); expect(input.attributes('value')).toBe(`${field.label} (${types})`); expect(input.attributes('disabled')).toBe(''); @@ -71,7 +71,7 @@ describe('AlertMappingBuilder', () => { it('renders mapping dropdown for each field', () => { alertFields.forEach(({ types }, index) => { - const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown); + const dropdown = findColumnInRow(index + 1, 2).findComponent(GlDropdown); const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types); expect(dropdown.exists()).toBe(true); @@ -82,7 +82,7 @@ describe('AlertMappingBuilder', () => { it('renders fallback dropdown only for the fields that have fallback', () => { alertFields.forEach(({ types, numberOfFallbacks }, index) => { - const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown); + const dropdown = findColumnInRow(index + 1, 3).findComponent(GlDropdown); expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks)); if (numberOfFallbacks) { @@ -96,8 +96,8 @@ describe('AlertMappingBuilder', () => { it('emits event with selected mapping', () => { const mappingToSave = { fieldName: 'TITLE', mapping: 'PARSED_TITLE' }; jest.spyOn(transformationUtils, 'transformForSave').mockReturnValue(mappingToSave); - const dropdown = findColumnInRow(1, 2).find(GlDropdown); - const option = dropdown.find(GlDropdownItem); + const dropdown = findColumnInRow(1, 2).findComponent(GlDropdown); + const option = dropdown.findComponent(GlDropdownItem); option.vm.$emit('click'); expect(wrapper.emitted('onMappingUpdate')[0]).toEqual([mappingToSave]); }); diff --git a/spec/frontend/alerts_settings/components/alerts_form_spec.js b/spec/frontend/alerts_settings/components/alerts_form_spec.js index a045954dfb8..33098282bf8 100644 --- a/spec/frontend/alerts_settings/components/alerts_form_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_form_spec.js @@ -5,7 +5,7 @@ describe('Alert integration settings form', () => { let wrapper; const service = { updateSettings: jest.fn().mockResolvedValue() }; - const findForm = () => wrapper.find({ ref: 'settingsForm' }); + const findForm = () => wrapper.findComponent({ ref: 'settingsForm' }); beforeEach(() => { wrapper = shallowMount(AlertsSettingsForm, { 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 3ffbb7ab60a..9983af873c2 100644 --- a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js @@ -53,8 +53,8 @@ describe('AlertIntegrationsList', () => { mountComponent(); }); - const findTableComponent = () => wrapper.find(GlTable); - const findTableComponentRows = () => wrapper.find(GlTable).findAll('table tbody tr'); + const findTableComponent = () => wrapper.findComponent(GlTable); + const findTableComponentRows = () => wrapper.findComponent(GlTable).findAll('table tbody tr'); const finsStatusCell = () => wrapper.findAll('[data-testid="integration-activated-status"]'); it('renders a table', () => { @@ -67,7 +67,7 @@ describe('AlertIntegrationsList', () => { }); it('renders an an edit and delete button for each integration', () => { - expect(findTableComponent().findAll(GlButton).length).toBe(4); + expect(findTableComponent().findAllComponents(GlButton).length).toBe(4); }); it('renders an highlighted row when a current integration is selected to edit', () => { @@ -78,7 +78,7 @@ describe('AlertIntegrationsList', () => { describe('integration status', () => { it('enabled', () => { const cell = finsStatusCell().at(0); - const activatedIcon = cell.find(GlIcon); + const activatedIcon = cell.findComponent(GlIcon); expect(cell.text()).toBe(i18n.status.enabled.name); expect(activatedIcon.attributes('name')).toBe('check'); expect(activatedIcon.attributes('title')).toBe(i18n.status.enabled.tooltip); @@ -86,7 +86,7 @@ describe('AlertIntegrationsList', () => { it('disabled', () => { const cell = finsStatusCell().at(1); - const notActivatedIcon = cell.find(GlIcon); + const notActivatedIcon = cell.findComponent(GlIcon); expect(cell.text()).toBe(i18n.status.disabled.name); expect(notActivatedIcon.attributes('name')).toBe('warning-solid'); expect(notActivatedIcon.attributes('title')).toBe(i18n.status.disabled.tooltip); 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 7d9d2875cf8..fb9e97e7505 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js @@ -325,9 +325,9 @@ describe('AlertsSettingsForm', () => { }); await nextTick(); - expect(findSamplePayloadSection().find(GlFormTextarea).attributes('disabled')).toBe( - disabled, - ); + expect( + findSamplePayloadSection().findComponent(GlFormTextarea).attributes('disabled'), + ).toBe(disabled); }); }); 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 ed185c11732..0266adeb6c7 100644 --- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js +++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js @@ -63,14 +63,14 @@ describe('AlertsSettingsWrapper', () => { const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon); const findIntegrationsList = () => wrapper.findComponent(IntegrationsList); - const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr'); + const findIntegrations = () => wrapper.findComponent(IntegrationsList).findAll('table tbody tr'); const findAddIntegrationBtn = () => wrapper.findByTestId('add-integration-btn'); const findAlertsSettingsForm = () => wrapper.findComponent(AlertsSettingsForm); const findAlert = () => wrapper.findComponent(GlAlert); function destroyHttpIntegration(localWrapper) { localWrapper - .find(IntegrationsList) + .findComponent(IntegrationsList) .vm.$emit('delete-integration', { id: integrationToDestroy.id }); } @@ -148,7 +148,7 @@ describe('AlertsSettingsWrapper', () => { expect(findIntegrations()).toHaveLength(mockIntegrations.length); }); - it('renders `Add new integration` button when multiple integrations are supported ', () => { + it('renders `Add new integration` button when multiple integrations are supported', () => { createComponent({ data: { integrations: mockIntegrations, @@ -189,7 +189,7 @@ describe('AlertsSettingsWrapper', () => { data: { integrations: [] }, loading: true, }); - expect(wrapper.find(IntegrationsList).exists()).toBe(true); + expect(wrapper.findComponent(IntegrationsList).exists()).toBe(true); expect(findLoader().exists()).toBe(true); }); }); @@ -321,7 +321,7 @@ describe('AlertsSettingsWrapper', () => { }); }); - it('shows an error alert when integration creation fails ', async () => { + it('shows an error alert when integration creation fails', async () => { jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(ADD_INTEGRATION_ERROR); findAlertsSettingsForm().vm.$emit('create-new-integration', {}); @@ -330,7 +330,7 @@ describe('AlertsSettingsWrapper', () => { expect(createFlash).toHaveBeenCalledWith({ message: ADD_INTEGRATION_ERROR }); }); - it('shows an error alert when integration token reset fails ', async () => { + it('shows an error alert when integration token reset fails', async () => { jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(RESET_INTEGRATION_TOKEN_ERROR); findAlertsSettingsForm().vm.$emit('reset-token', {}); @@ -339,7 +339,7 @@ describe('AlertsSettingsWrapper', () => { expect(createFlash).toHaveBeenCalledWith({ message: RESET_INTEGRATION_TOKEN_ERROR }); }); - it('shows an error alert when integration update fails ', async () => { + it('shows an error alert when integration update fails', async () => { jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg); findAlertsSettingsForm().vm.$emit('update-integration', {}); @@ -357,14 +357,14 @@ describe('AlertsSettingsWrapper', () => { mock.restore(); }); - it('shows an error alert when integration test payload is invalid ', async () => { + it('shows an error alert when integration test payload is invalid', async () => { mock.onPost(/(.*)/).replyOnce(httpStatusCodes.UNPROCESSABLE_ENTITY); await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' }); expect(createFlash).toHaveBeenCalledWith({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); expect(createFlash).toHaveBeenCalledTimes(1); }); - it('shows an error alert when integration is not activated ', async () => { + it('shows an error alert when integration is not activated', async () => { mock.onPost(/(.*)/).replyOnce(httpStatusCodes.FORBIDDEN); await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' }); expect(createFlash).toHaveBeenCalledWith({ diff --git a/spec/frontend/analytics/components/activity_chart_spec.js b/spec/frontend/analytics/components/activity_chart_spec.js index a6b45ffe20f..c26407f5c1d 100644 --- a/spec/frontend/analytics/components/activity_chart_spec.js +++ b/spec/frontend/analytics/components/activity_chart_spec.js @@ -18,7 +18,7 @@ describe('Activity Chart Bundle', () => { wrapper = null; }); - const findChart = () => wrapper.find(GlColumnChart); + const findChart = () => wrapper.findComponent(GlColumnChart); const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]'); describe('Activity Chart', () => { diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js index a38df274243..7a09fe3319d 100644 --- a/spec/frontend/analytics/shared/components/daterange_spec.js +++ b/spec/frontend/analytics/shared/components/daterange_spec.js @@ -1,5 +1,5 @@ -import { GlDaterangePicker, GlSprintf } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { GlDaterangePicker } from '@gitlab/ui'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import { useFakeDate } from 'helpers/fake_date'; import Daterange from '~/analytics/shared/components/daterange.vue'; @@ -13,13 +13,12 @@ describe('Daterange component', () => { let wrapper; - const factory = (props = defaultProps, mountFn = shallowMount) => { + const factory = (props = defaultProps, mountFn = shallowMountExtended) => { wrapper = mountFn(Daterange, { propsData: { ...defaultProps, ...props, }, - stubs: { GlSprintf }, }); }; @@ -28,7 +27,7 @@ describe('Daterange component', () => { }); const findDaterangePicker = () => wrapper.findComponent(GlDaterangePicker); - const findDateRangeIndicator = () => wrapper.findComponent(GlSprintf); + const findDateRangeIndicator = () => wrapper.findByTestId('daterange-picker-indicator'); describe('template', () => { describe('when show is false', () => { @@ -52,7 +51,7 @@ describe('Daterange component', () => { const endDate = new Date('2019-09-30'); const minDate = new Date('2019-06-01'); - factory({ show: true, startDate, endDate, minDate }, mount); + factory({ show: true, startDate, endDate, minDate }, mountExtended); const input = findDaterangePicker().find('input'); input.setValue('2019-01-01'); @@ -64,7 +63,7 @@ describe('Daterange component', () => { describe('with a maxDateRange being set', () => { beforeEach(() => { - factory({ maxDateRange: 30 }); + factory({ maxDateRange: 30 }, mountExtended); }); it('displays the max date range indicator', () => { @@ -72,7 +71,7 @@ describe('Daterange component', () => { }); it('displays the correct number of selected days in the indicator', () => { - expect(findDateRangeIndicator().text()).toMatchInterpolatedText('10 days selected'); + expect(findDateRangeIndicator().text()).toBe('10 days selected'); }); it('sets the tooltip', () => { diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js index ffec77c2708..6a58f8c6d29 100644 --- a/spec/frontend/analytics/shared/components/metric_popover_spec.js +++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js @@ -30,7 +30,7 @@ describe('MetricPopover', () => { const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]'); const findMetricDescription = () => wrapper.findByTestId('metric-description'); const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link'); - const findMetricDocsLinkIcon = () => findMetricDocsLink().find(GlIcon); + const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon); afterEach(() => { wrapper.destroy(); @@ -83,7 +83,9 @@ describe('MetricPopover', () => { const allLinkContainers = findAllMetricLinks(); expect(allLinkContainers.at(idx).text()).toContain(link.name); - expect(allLinkContainers.at(idx).find(GlLink).attributes('href')).toBe(link.url); + expect(allLinkContainers.at(idx).findComponent(GlLink).attributes('href')).toBe( + link.url, + ); }); }); 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 69918c1db65..3871fd530d8 100644 --- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js +++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js @@ -79,11 +79,11 @@ describe('ProjectsDropdownFilter component', () => { const findClearAllButton = () => wrapper.findByText('Clear all'); const findSelectedProjectsLabel = () => wrapper.findComponent(GlTruncate); - const findDropdown = () => wrapper.find(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDropdown); const findDropdownItems = () => findDropdown() - .findAll(GlDropdownItem) + .findAllComponents(GlDropdownItem) .filter((w) => w.text() !== 'No matching results'); const findDropdownAtIndex = (index) => findDropdownItems().at(index); @@ -106,7 +106,7 @@ describe('ProjectsDropdownFilter component', () => { }; // NOTE: Selected items are now visually separated from unselected items - const findSelectedDropdownItems = () => findHighlightedItems().findAll(GlDropdownItem); + const findSelectedDropdownItems = () => findHighlightedItems().findAllComponents(GlDropdownItem); const findSelectedDropdownAtIndex = (index) => findSelectedDropdownItems().at(index); const findSelectedButtonIdentIconAtIndex = (index) => diff --git a/spec/frontend/analytics/usage_trends/components/app_spec.js b/spec/frontend/analytics/usage_trends/components/app_spec.js index 156be26f895..c732dc22322 100644 --- a/spec/frontend/analytics/usage_trends/components/app_spec.js +++ b/spec/frontend/analytics/usage_trends/components/app_spec.js @@ -21,13 +21,13 @@ describe('UsageTrendsApp', () => { }); it('displays the usage counts component', () => { - expect(wrapper.find(UsageCounts).exists()).toBe(true); + expect(wrapper.findComponent(UsageCounts).exists()).toBe(true); }); ['Total projects & groups', 'Pipelines', 'Issues & merge requests'].forEach((usage) => { it(`displays the ${usage} chart`, () => { const chartTitles = wrapper - .findAll(UsageTrendsCountChart) + .findAllComponents(UsageTrendsCountChart) .wrappers.map((chartComponent) => chartComponent.props('chartTitle')); expect(chartTitles).toContain(usage); @@ -35,6 +35,6 @@ describe('UsageTrendsApp', () => { }); it('displays the users chart component', () => { - expect(wrapper.find(UsersChart).exists()).toBe(true); + expect(wrapper.findComponent(UsersChart).exists()).toBe(true); }); }); 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 02cf7f42a0b..ad6089f74b5 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 @@ -50,9 +50,9 @@ describe('UsageTrendsCountChart', () => { wrapper = null; }); - const findLoader = () => wrapper.find(ChartSkeletonLoader); - const findChart = () => wrapper.find(GlLineChart); - const findAlert = () => wrapper.find(GlAlert); + const findLoader = () => wrapper.findComponent(ChartSkeletonLoader); + const findChart = () => wrapper.findComponent(GlLineChart); + const findAlert = () => wrapper.findComponent(GlAlert); describe('while loading', () => { beforeEach(() => { @@ -61,7 +61,7 @@ describe('UsageTrendsCountChart', () => { }); it('requests data', () => { - expect(queryHandler).toBeCalledTimes(1); + expect(queryHandler).toHaveBeenCalledTimes(1); }); it('displays the skeleton loader', () => { @@ -105,7 +105,7 @@ describe('UsageTrendsCountChart', () => { }); it('requests data', () => { - expect(queryHandler).toBeCalledTimes(1); + expect(queryHandler).toHaveBeenCalledTimes(1); }); it('hides the skeleton loader', () => { @@ -141,7 +141,7 @@ describe('UsageTrendsCountChart', () => { }); it('requests data twice', () => { - expect(queryHandler).toBeCalledTimes(2); + expect(queryHandler).toHaveBeenCalledTimes(2); }); it('passes the data to the line chart', () => { 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 32a664a5026..e7abd4d4323 100644 --- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js +++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js @@ -47,9 +47,9 @@ describe('UsersChart', () => { wrapper = null; }); - const findLoader = () => wrapper.find(ChartSkeletonLoader); - const findAlert = () => wrapper.find(GlAlert); - const findChart = () => wrapper.find(GlAreaChart); + const findLoader = () => wrapper.findComponent(ChartSkeletonLoader); + const findAlert = () => wrapper.findComponent(GlAlert); + const findChart = () => wrapper.findComponent(GlAreaChart); describe('while loading', () => { beforeEach(() => { @@ -139,7 +139,7 @@ describe('UsersChart', () => { }); it('requests data twice', () => { - expect(queryHandler).toBeCalledTimes(2); + expect(queryHandler).toHaveBeenCalledTimes(2); }); it('calls fetchMore', () => { diff --git a/spec/frontend/analytics/usage_trends/utils_spec.js b/spec/frontend/analytics/usage_trends/utils_spec.js index 656f310dda7..9982e96735e 100644 --- a/spec/frontend/analytics/usage_trends/utils_spec.js +++ b/spec/frontend/analytics/usage_trends/utils_spec.js @@ -16,17 +16,17 @@ describe('getAverageByMonth', () => { expect(getAverageByMonth(mockCountsData2)).toStrictEqual(countsMonthlyChartData2); }); - it('it transforms a data point to the first of the month', () => { + it('transforms a data point to the first of the month', () => { const item = mockCountsData1[0]; const firstOfTheMonth = item.recordedAt.replace(/-[0-9]{2}$/, '-01'); expect(getAverageByMonth([item])).toStrictEqual([[firstOfTheMonth, item.count]]); }); - it('it uses sane defaults', () => { + it('uses sane defaults', () => { expect(getAverageByMonth()).toStrictEqual([]); }); - it('it errors when passing null', () => { + it('errors when passing null', () => { expect(() => { getAverageByMonth(null); }).toThrow(); diff --git a/spec/frontend/api/harbor_registry_spec.js b/spec/frontend/api/harbor_registry_spec.js new file mode 100644 index 00000000000..8a4c377ebd1 --- /dev/null +++ b/spec/frontend/api/harbor_registry_spec.js @@ -0,0 +1,107 @@ +import MockAdapter from 'axios-mock-adapter'; +import * as harborRegistryApi from '~/api/harbor_registry'; +import axios from '~/lib/utils/axios_utils'; +import httpStatus from '~/lib/utils/http_status'; + +describe('~/api/harbor_registry', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + jest.spyOn(axios, 'get'); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('getHarborRepositoriesList', () => { + it('fetches the harbor repositories of the configured harbor project', () => { + const requestPath = '/flightjs/Flight/-/harbor/repositories'; + const expectedUrl = `${requestPath}.json`; + const expectedParams = { + limit: 10, + page: 1, + sort: 'update_time desc', + requestPath, + }; + const expectResponse = [ + { + harbor_id: 1, + name: 'test-project/image-1', + artifact_count: 1, + creation_time: '2022-07-16T08:20:34.851Z', + update_time: '2022-07-16T08:20:34.851Z', + harbor_project_id: 2, + pull_count: 0, + location: 'http://demo.harbor.com/harbor/projects/2/repositories/image-1', + }, + ]; + mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse); + + return harborRegistryApi.getHarborRepositoriesList(expectedParams).then(({ data }) => { + expect(data).toEqual(expectResponse); + }); + }); + }); + + describe('getHarborArtifacts', () => { + it('fetches the artifacts of a particular harbor repository', () => { + const requestPath = '/flightjs/Flight/-/harbor/repositories'; + const repoName = 'image-1'; + const expectedUrl = `${requestPath}/${repoName}/artifacts.json`; + const expectedParams = { + limit: 10, + page: 1, + sort: 'name asc', + repoName, + requestPath, + }; + const expectResponse = [ + { + harbor_id: 1, + digest: 'sha256:dcdf379c574e1773d703f0c0d56d67594e7a91d6b84d11ff46799f60fb081c52', + size: 775241, + push_time: '2022-07-16T08:20:34.867Z', + tags: ['v2', 'v1', 'latest'], + }, + ]; + mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse); + + return harborRegistryApi.getHarborArtifacts(expectedParams).then(({ data }) => { + expect(data).toEqual(expectResponse); + }); + }); + }); + + describe('getHarborTags', () => { + it('fetches the tags of a particular artifact', () => { + const requestPath = '/flightjs/Flight/-/harbor/repositories'; + const repoName = 'image-1'; + const digest = 'sha256:5d98daa36cdc8d6c7ed6579ce17230f0f9fd893a9012fc069cb7d714c0e3df35'; + const expectedUrl = `${requestPath}/${repoName}/artifacts/${digest}/tags.json`; + const expectedParams = { + requestPath, + digest, + repoName, + }; + const expectResponse = [ + { + repositoryId: 4, + artifactId: 5, + id: 4, + name: 'latest', + pullTime: '0001-01-01T00:00:00.000Z', + pushTime: '2022-05-27T18:21:27.903Z', + signed: false, + immutable: false, + }, + ]; + mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse); + + return harborRegistryApi.getHarborTags(expectedParams).then(({ data }) => { + expect(data).toEqual(expectResponse); + }); + }); + }); +}); 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 2f3ff2b22f2..ca94acfa444 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 @@ -39,8 +39,8 @@ describe('Keep latest artifact checkbox', () => { const fullPath = 'gitlab-org/gitlab'; const helpPagePath = '/help/ci/pipelines/job_artifacts'; - const findCheckbox = () => wrapper.find(GlFormCheckbox); - const findHelpLink = () => wrapper.find(GlLink); + const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); + const findHelpLink = () => wrapper.findComponent(GlLink); const createComponent = (handlers) => { requestHandlers = { diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js index 2dcc537809f..0d9196b88ed 100644 --- a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js +++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js @@ -31,11 +31,13 @@ describe('RecoveryCodes', () => { }; const queryByText = (text, options) => within(wrapper.element).queryByText(text, options); - const findAlert = () => wrapper.find(GlAlert); + const findAlert = () => wrapper.findComponent(GlAlert); const findRecoveryCodes = () => wrapper.findByTestId('recovery-codes'); - const findCopyButton = () => wrapper.find(ClipboardButton); + const findCopyButton = () => wrapper.findComponent(ClipboardButton); const findButtonByText = (text) => - wrapper.findAll(GlButton).wrappers.find((buttonWrapper) => buttonWrapper.text() === text); + wrapper + .findAllComponents(GlButton) + .wrappers.find((buttonWrapper) => buttonWrapper.text() === text); const findDownloadButton = () => findButtonByText('Download codes'); const findPrintButton = () => findButtonByText('Print codes'); const findProceedButton = () => findButtonByText('Proceed'); diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js index f9a6b2df662..427159bacd9 100644 --- a/spec/frontend/authentication/two_factor_auth/index_spec.js +++ b/spec/frontend/authentication/two_factor_auth/index_spec.js @@ -10,7 +10,7 @@ describe('initRecoveryCodes', () => { let el; let wrapper; - const findRecoveryCodesComponent = () => wrapper.find(RecoveryCodes); + const findRecoveryCodesComponent = () => wrapper.findComponent(RecoveryCodes); beforeEach(() => { el = document.createElement('div'); diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js index c881e0f9794..7a9262cd004 100644 --- a/spec/frontend/autosave_spec.js +++ b/spec/frontend/autosave_spec.js @@ -8,6 +8,7 @@ describe('Autosave', () => { let autosave; const field = $(''); + const checkbox = $(''); const key = 'key'; const fallbackKey = 'fallbackKey'; const lockVersionKey = 'lockVersionKey'; @@ -90,6 +91,24 @@ describe('Autosave', () => { expect(eventHandler).toHaveBeenCalledTimes(1); fieldElement.removeEventListener('change', eventHandler); }); + + describe('if field type is checkbox', () => { + beforeEach(() => { + autosave = { + field: checkbox, + key, + isLocalStorageAvailable: true, + type: 'checkbox', + }; + }); + + it('should restore', () => { + window.localStorage.setItem(key, true); + expect(checkbox.is(':checked')).toBe(false); + Autosave.prototype.restore.call(autosave); + expect(checkbox.is(':checked')).toBe(true); + }); + }); }); describe('if field gets deleted from DOM', () => { @@ -169,6 +188,31 @@ describe('Autosave', () => { expect(window.localStorage.setItem).toHaveBeenCalled(); }); }); + + describe('if field type is checkbox', () => { + beforeEach(() => { + autosave = { + field: checkbox, + key, + isLocalStorageAvailable: true, + type: 'checkbox', + }; + }); + + it('should save true when checkbox on', () => { + checkbox.prop('checked', true); + Autosave.prototype.save.call(autosave); + expect(window.localStorage.setItem).toHaveBeenCalledWith(key, true); + }); + + it('should call reset when checkbox off', () => { + autosave.reset = jest.fn(); + checkbox.prop('checked', false); + Autosave.prototype.save.call(autosave); + expect(autosave.reset).toHaveBeenCalled(); + expect(window.localStorage.setItem).not.toHaveBeenCalled(); + }); + }); }); describe('save with lockVersion', () => { diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js index 79cf5f3e4ff..bddb6d3801c 100644 --- a/spec/frontend/badges/components/badge_settings_spec.js +++ b/spec/frontend/badges/components/badge_settings_spec.js @@ -42,7 +42,7 @@ describe('BadgeSettings component', () => { button.vm.$emit('click'); await nextTick(); - const modal = wrapper.find(GlModal); + const modal = wrapper.findComponent(GlModal); expect(modal.isVisible()).toBe(true); }); @@ -51,7 +51,7 @@ describe('BadgeSettings component', () => { }); it('displays badge list', () => { - expect(wrapper.find(BadgeList).isVisible()).toBe(true); + expect(wrapper.findComponent(BadgeList).isVisible()).toBe(true); }); describe('when editing', () => { @@ -64,7 +64,7 @@ describe('BadgeSettings component', () => { }); it('displays no badge list', () => { - expect(wrapper.find(BadgeList).isVisible()).toBe(false); + expect(wrapper.findComponent(BadgeList).isVisible()).toBe(false); }); }); }); 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 6a5ff1af7c9..c922d6a9809 100644 --- a/spec/frontend/batch_comments/components/diff_file_drafts_spec.js +++ b/spec/frontend/batch_comments/components/diff_file_drafts_spec.js @@ -35,13 +35,13 @@ describe('Batch comments diff file drafts component', () => { it('renders list of draft notes', () => { factory(); - expect(vm.findAll(DraftNote).length).toEqual(2); + expect(vm.findAllComponents(DraftNote).length).toEqual(2); }); it('renders index of draft note', () => { factory(); - const elements = vm.findAll(DesignNotePin); + const elements = vm.findAllComponents(DesignNotePin); expect(elements.length).toEqual(2); diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js index ccca4a2c3e9..03ecbc01a56 100644 --- a/spec/frontend/batch_comments/components/draft_note_spec.js +++ b/spec/frontend/batch_comments/components/draft_note_spec.js @@ -61,7 +61,7 @@ describe('Batch comments draft note component', () => { createComponent(); expect(wrapper.findComponent(GlBadge).exists()).toBe(true); - const note = wrapper.find(NoteableNote); + const note = wrapper.findComponent(NoteableNote); expect(note.exists()).toBe(true); expect(note.props().note).toEqual(draft); @@ -115,14 +115,14 @@ describe('Batch comments draft note component', () => { await nextTick(); const publishNowButton = findSubmitReviewButton(); - expect(publishNowButton.attributes().disabled).toBeTruthy(); + expect(publishNowButton.attributes().disabled).toBe('true'); }); }); describe('update', () => { it('dispatches updateDraft', async () => { createComponent(); - const note = wrapper.find(NoteableNote); + const note = wrapper.findComponent(NoteableNote); note.vm.$emit('handleEdit'); @@ -147,7 +147,7 @@ describe('Batch comments draft note component', () => { createComponent(); jest.spyOn(window, 'confirm').mockImplementation(() => true); - const note = wrapper.find(NoteableNote); + const note = wrapper.findComponent(NoteableNote); note.vm.$emit('handleDeleteNote', draft); diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js index 079b64225e4..283632cb560 100644 --- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js @@ -53,7 +53,7 @@ describe('Batch comments preview dropdown', () => { }); describe('clicking draft', () => { - it('it toggles active file when viewDiffsFileByFile is true', async () => { + it('toggles active file when viewDiffsFileByFile is true', async () => { factory({ viewDiffsFileByFile: true, sortedDrafts: [{ id: 1, file_hash: 'hash' }], diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js index cb71edd1238..91e6b84a216 100644 --- a/spec/frontend/batch_comments/components/preview_item_spec.js +++ b/spec/frontend/batch_comments/components/preview_item_spec.js @@ -118,7 +118,7 @@ describe('Batch comments draft preview item component', () => { ); }); - it('it renders thread resolved text', () => { + it('renders thread resolved text', () => { expect(vm.$el.querySelector('.draft-note-resolution').textContent).toContain( 'Thread will be resolved', ); diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js index a3168931f1f..d1b7160d231 100644 --- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js @@ -28,12 +28,12 @@ describe('Batch comments publish dropdown component', () => { it('renders list of drafts', () => { createComponent(); - expect(wrapper.findAll(GlDropdownItem).length).toBe(2); + expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(2); }); it('renders draft count in dropdown title', () => { createComponent(); - expect(wrapper.find(GlDropdown).props('headerText')).toEqual('2 pending comments'); + expect(wrapper.findComponent(GlDropdown).props('headerText')).toEqual('2 pending comments'); }); }); diff --git a/spec/frontend/batch_comments/components/review_bar_spec.js b/spec/frontend/batch_comments/components/review_bar_spec.js index f50db6ab210..0a4c9ff62e4 100644 --- a/spec/frontend/batch_comments/components/review_bar_spec.js +++ b/spec/frontend/batch_comments/components/review_bar_spec.js @@ -24,7 +24,7 @@ describe('Batch comments review bar component', () => { wrapper.destroy(); }); - it('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', async () => { expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(false); createComponent(); @@ -32,7 +32,7 @@ describe('Batch comments review bar component', () => { expect(document.body.classList.contains(REVIEW_BAR_VISIBLE_CLASS_NAME)).toBe(true); }); - it('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', async () => { 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 4f5ff797230..462ef7e7280 100644 --- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js @@ -8,7 +8,7 @@ Vue.use(Vuex); let wrapper; let publishReview; -function factory() { +function factory({ canApprove = true } = {}) { publishReview = jest.fn(); const store = new Vuex.Store({ @@ -17,8 +17,13 @@ function factory() { markdownDocsPath: '/markdown/docs', quickActionsDocsPath: '/quickactions/docs', }), - getNoteableData: () => ({ id: 1, preview_note_path: '/preview' }), + getNoteableData: () => ({ + id: 1, + preview_note_path: '/preview', + current_user: { can_approve: canApprove }, + }), noteableType: () => 'merge_request', + getCurrentUserLastNote: () => ({ id: 1 }), }, modules: { batchComments: { @@ -41,6 +46,7 @@ const findForm = () => wrapper.findByTestId('submit-gl-form'); describe('Batch comments submit dropdown', () => { afterEach(() => { wrapper.destroy(); + window.mrTabs = null; }); it('calls publishReview with note data', async () => { @@ -54,9 +60,24 @@ describe('Batch comments submit dropdown', () => { noteable_type: 'merge_request', noteable_id: 1, note: 'Hello world', + approve: false, + approval_password: '', }); }); + it('switches to the overview tab after submit', async () => { + window.mrTabs = { tabShown: jest.fn() }; + + factory(); + + findCommentTextarea().setValue('Hello world'); + + await findForm().vm.$emit('submit', { preventDefault: jest.fn() }); + await Vue.nextTick(); + + expect(window.mrTabs.tabShown).toHaveBeenCalledWith('show'); + }); + it('sets submit dropdown to loading', async () => { factory(); @@ -66,4 +87,14 @@ describe('Batch comments submit dropdown', () => { expect(findSubmitButton().props('loading')).toBe(true); }); + + it.each` + canApprove | exists | existsText + ${true} | ${true} | ${'shows'} + ${false} | ${false} | ${'hides'} + `('$existsText approve checkbox if can_approve is $canApprove', ({ canApprove, exists }) => { + factory({ canApprove }); + + expect(wrapper.findByTestId('approve_merge_request').exists()).toBe(exists); + }); }); 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 9f50b12bac2..6369ea9aa15 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 @@ -180,6 +180,7 @@ describe('Batch comments store actions', () => { }); it('calls service with notes data', () => { + mock.onAny().reply(200); jest.spyOn(axios, 'post'); return actions @@ -192,7 +193,7 @@ describe('Batch comments store actions', () => { it('dispatches error commits', () => { mock.onAny().reply(500); - return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => { + return actions.publishReview({ dispatch, commit, getters, rootGetters }).catch(() => { expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']); expect(commit.mock.calls[1]).toEqual(['RECEIVE_PUBLISH_REVIEW_ERROR']); }); diff --git a/spec/frontend/behaviors/bind_in_out_spec.js b/spec/frontend/behaviors/bind_in_out_spec.js index 49425a9377e..4d958e30b4d 100644 --- a/spec/frontend/behaviors/bind_in_out_spec.js +++ b/spec/frontend/behaviors/bind_in_out_spec.js @@ -33,7 +33,7 @@ describe('BindInOut', () => { testContext.bindInOut = new BindInOut({ tagName: 'INPUT' }); }); - it('should set .eventType to keyup ', () => { + it('should set .eventType to keyup', () => { expect(testContext.bindInOut.eventType).toEqual('keyup'); }); }); @@ -43,7 +43,7 @@ describe('BindInOut', () => { testContext.bindInOut = new BindInOut({ tagName: 'TEXTAREA' }); }); - it('should set .eventType to keyup ', () => { + it('should set .eventType to keyup', () => { expect(testContext.bindInOut.eventType).toEqual('keyup'); }); }); @@ -53,7 +53,7 @@ describe('BindInOut', () => { testContext.bindInOut = new BindInOut({ tagName: 'SELECT' }); }); - it('should set .eventType to change ', () => { + it('should set .eventType to change', () => { expect(testContext.bindInOut.eventType).toEqual('change'); }); }); diff --git a/spec/frontend/blob/sketch/index_spec.js b/spec/frontend/blob/sketch/index_spec.js index e8d1f724c4b..4b6cb79791c 100644 --- a/spec/frontend/blob/sketch/index_spec.js +++ b/spec/frontend/blob/sketch/index_spec.js @@ -2,18 +2,6 @@ import SketchLoader from '~/blob/sketch'; import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import waitForPromises from 'helpers/wait_for_promises'; -jest.mock('jszip', () => { - return { - loadAsync: jest.fn().mockResolvedValue({ - files: { - 'previews/preview.png': { - async: jest.fn().mockResolvedValue('foo'), - }, - }, - }), - }; -}); - describe('Sketch viewer', () => { beforeEach(() => { loadHTMLFixture('static/sketch_viewer.html'); @@ -25,7 +13,7 @@ describe('Sketch viewer', () => { describe('with error message', () => { beforeEach(() => { - jest.spyOn(SketchLoader.prototype, 'getZipFile').mockImplementation( + jest.spyOn(SketchLoader.prototype, 'getZipContents').mockImplementation( () => new Promise((resolve, reject) => { reject(); @@ -50,7 +38,13 @@ describe('Sketch viewer', () => { describe('success', () => { beforeEach(() => { - jest.spyOn(SketchLoader.prototype, 'getZipFile').mockResolvedValue(); + jest.spyOn(SketchLoader.prototype, 'getZipContents').mockResolvedValue({ + files: { + 'previews/preview.png': { + async: jest.fn().mockResolvedValue('foo'), + }, + }, + }); // eslint-disable-next-line no-new new SketchLoader(document.getElementById('js-sketch-viewer')); diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 985902b4a3b..2c3ec69f9ae 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -7,6 +7,8 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue'; import BoardCardInner from '~/boards/components/board_card_inner.vue'; +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; +import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue'; import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import defaultStore from '~/boards/stores'; @@ -47,6 +49,8 @@ describe('Board card component', () => { const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight'); const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content'); const findHiddenIssueIcon = () => wrapper.findByTestId('hidden-icon'); + const findMoveToPositionComponent = () => wrapper.findComponent(BoardCardMoveToPosition); + const findWorkItemIcon = () => wrapper.findComponent(WorkItemTypeIcon); const performSearchMock = jest.fn(); @@ -75,10 +79,12 @@ describe('Board card component', () => { propsData: { list, item: issue, + index: 0, ...props, }, stubs: { GlLoadingIcon: true, + BoardCardMoveToPosition: true, }, directives: { GlTooltip: createMockDirective(), @@ -137,6 +143,20 @@ describe('Board card component', () => { expect(findHiddenIssueIcon().exists()).toBe(false); }); + it('renders the move to position icon', () => { + expect(findMoveToPositionComponent().exists()).toBe(true); + }); + + it('does not render the work type icon by default', () => { + expect(findWorkItemIcon().exists()).toBe(false); + }); + + it('renders the work type icon when props is passed', () => { + createWrapper({ item: issue, list, showWorkItemTypeIcon: true }); + expect(findWorkItemIcon().exists()).toBe(true); + expect(findWorkItemIcon().props('workItemType')).toBe(issue.type); + }); + it('renders issue ID with #', () => { expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.iid}`); }); diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js index 04192489817..65a41c49e7f 100644 --- a/spec/frontend/boards/board_list_helper.js +++ b/spec/frontend/boards/board_list_helper.js @@ -75,6 +75,7 @@ export default function createComponent({ id: 1, iid: 1, confidential: false, + referencePath: 'gitlab-org/test-subgroup/gitlab-test#1', labels: [], assignees: [], ...listIssueProps, diff --git a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap index 3fb0706fd10..34e4f996ff0 100644 --- a/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap +++ b/spec/frontend/boards/components/__snapshots__/board_blocked_icon_spec.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`BoardBlockedIcon on mouseenter on blocked icon with more than three blocking issues matches the snapshot 1`] = ` -"
+"
diff --git a/spec/frontend/boards/components/board_blocked_icon_spec.js b/spec/frontend/boards/components/board_blocked_icon_spec.js index cf4ba07da16..ffdc0a7cecc 100644 --- a/spec/frontend/boards/components/board_blocked_icon_spec.js +++ b/spec/frontend/boards/components/board_blocked_icon_spec.js @@ -10,13 +10,17 @@ import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants'; import { truncate } from '~/lib/utils/text_utility'; import { mockIssue, + mockEpic, mockBlockingIssue1, mockBlockingIssue2, + mockBlockingEpic1, mockBlockingIssuablesResponse1, mockBlockingIssuablesResponse2, mockBlockingIssuablesResponse3, mockBlockedIssue1, mockBlockedIssue2, + mockBlockedEpic1, + mockBlockingEpicIssuablesResponse1, } from '../mock_data'; describe('BoardBlockedIcon', () => { @@ -51,9 +55,11 @@ describe('BoardBlockedIcon', () => { const createWrapperWithApollo = ({ item = mockBlockedIssue1, blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1), + issuableItem = mockIssue, + issuableType = issuableTypes.issue, } = {}) => { mockApollo = createMockApollo([ - [blockingIssuablesQueries[issuableTypes.issue].query, blockingIssuablesSpy], + [blockingIssuablesQueries[issuableType].query, blockingIssuablesSpy], ]); Vue.use(VueApollo); @@ -62,27 +68,34 @@ describe('BoardBlockedIcon', () => { apolloProvider: mockApollo, propsData: { item: { - ...mockIssue, + ...issuableItem, ...item, }, uniqueId: 'uniqueId', - issuableType: issuableTypes.issue, + issuableType, }, attachTo: document.body, }), ); }; - const createWrapper = ({ item = {}, queries = {}, data = {}, loading = false } = {}) => { + const createWrapper = ({ + item = {}, + queries = {}, + data = {}, + loading = false, + mockIssuable = mockIssue, + issuableType = issuableTypes.issue, + } = {}) => { wrapper = extendedWrapper( shallowMount(BoardBlockedIcon, { propsData: { item: { - ...mockIssue, + ...mockIssuable, ...item, }, uniqueId: 'uniqueid', - issuableType: issuableTypes.issue, + issuableType, }, data() { return { @@ -105,11 +118,24 @@ describe('BoardBlockedIcon', () => { ); }; - it('should render blocked icon', () => { - createWrapper(); + it.each` + mockIssuable | issuableType | expectedIcon + ${mockIssue} | ${issuableTypes.issue} | ${'issue-block'} + ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'} + `( + 'should render blocked icon for $issuableType', + ({ mockIssuable, issuableType, expectedIcon }) => { + createWrapper({ + mockIssuable, + issuableType, + }); - expect(findGlIcon().exists()).toBe(true); - }); + expect(findGlIcon().exists()).toBe(true); + const icon = findGlIcon(); + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe(expectedIcon); + }, + ); it('should display a loading spinner while loading', () => { createWrapper({ loading: true }); @@ -124,17 +150,29 @@ describe('BoardBlockedIcon', () => { }); describe('on mouseenter on blocked icon', () => { - it('should query for blocking issuables and render the result', async () => { - createWrapperWithApollo(); + it.each` + item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy + ${mockBlockedIssue1} | ${issuableTypes.issue} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)} + ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)} + `( + 'should query for blocking issuables and render the result for $issuableType', + async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => { + createWrapperWithApollo({ + item, + issuableType, + issuableItem, + blockingIssuablesSpy, + }); - expect(findGlPopover().text()).not.toContain(mockBlockingIssue1.title); + expect(findGlPopover().text()).not.toContain(mockBlockingIssuable.title); - await mouseenter(); + await mouseenter(); - expect(findGlPopover().exists()).toBe(true); - expect(findIssuableTitle().text()).toContain(mockBlockingIssue1.title); - expect(wrapper.vm.skip).toBe(true); - }); + expect(findGlPopover().exists()).toBe(true); + expect(findIssuableTitle().text()).toContain(mockBlockingIssuable.title); + expect(wrapper.vm.skip).toBe(true); + }, + ); it('should emit "blocking-issuables-error" event on query error', async () => { const mockError = new Error('mayday'); diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js new file mode 100644 index 00000000000..7254b9486ef --- /dev/null +++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js @@ -0,0 +1,133 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; + +import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue'; +import { mockList, mockIssue2, mockIssue, mockIssue3, mockIssue4 } from 'jest/boards/mock_data'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; + +Vue.use(Vuex); + +const dropdownOptions = [ + BoardCardMoveToPosition.i18n.moveToStartText, + BoardCardMoveToPosition.i18n.moveToEndText, +]; + +describe('Board Card Move to position', () => { + let wrapper; + let trackingSpy; + let store; + let dispatch; + const itemIndex = 1; + + const createStoreOptions = () => { + const state = { + pageInfoByListId: { + 'gid://gitlab/List/1': {}, + 'gid://gitlab/List/2': { hasNextPage: true }, + }, + }; + const getters = { + getBoardItemsByList: () => () => [mockIssue, mockIssue2, mockIssue3, mockIssue4], + }; + const actions = { + moveItem: jest.fn(), + }; + + return { + state, + getters, + actions, + }; + }; + + const createComponent = (propsData) => { + wrapper = shallowMount(BoardCardMoveToPosition, { + store, + propsData: { + item: mockIssue2, + list: mockList, + index: 0, + ...propsData, + }, + stubs: { + GlDropdown, + GlDropdownItem, + }, + }); + }; + + beforeEach(() => { + store = new Vuex.Store(createStoreOptions()); + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findMoveToPositionDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownItems = () => findMoveToPositionDropdown().findAllComponents(GlDropdownItem); + const findDropdownItemAtIndex = (index) => findDropdownItems().at(index); + + describe('Dropdown', () => { + describe('Dropdown button', () => { + it('has an icon with vertical ellipsis', () => { + expect(findMoveToPositionDropdown().exists()).toBe(true); + expect(findMoveToPositionDropdown().props('icon')).toBe('ellipsis_v'); + }); + + it('is opened on the click of vertical ellipsis and has 2 dropdown items when number of list items < 10', () => { + findMoveToPositionDropdown().vm.$emit('click'); + expect(findDropdownItems()).toHaveLength(dropdownOptions.length); + }); + }); + + describe('Dropdown options', () => { + beforeEach(() => { + createComponent({ index: itemIndex }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + dispatch = jest.spyOn(store, 'dispatch').mockImplementation(() => {}); + }); + + afterEach(() => { + unmockTracking(); + }); + + it.each` + dropdownIndex | dropdownLabel | trackLabel | positionInList + ${0} | ${BoardCardMoveToPosition.i18n.moveToStartText} | ${'move_to_start'} | ${0} + ${1} | ${BoardCardMoveToPosition.i18n.moveToEndText} | ${'move_to_end'} | ${-1} + `( + 'on click of dropdown index $dropdownIndex with label $dropdownLabel should call moveItem action with tracking label $trackLabel', + async ({ dropdownIndex, dropdownLabel, trackLabel, positionInList }) => { + await findMoveToPositionDropdown().vm.$emit('click'); + + expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownLabel); + await findDropdownItemAtIndex(dropdownIndex).vm.$emit('click', { + stopPropagation: () => {}, + }); + + await nextTick(); + + expect(trackingSpy).toHaveBeenCalledWith('boards:list', 'click_toggle_button', { + category: 'boards:list', + label: trackLabel, + property: 'type_card', + }); + expect(dispatch).toHaveBeenCalledWith('moveItem', { + fromListId: mockList.id, + itemId: mockIssue2.id, + itemIid: mockIssue2.iid, + itemPath: mockIssue2.referencePath, + positionInList, + toListId: mockList.id, + allItemsLoadedInList: true, + atIndex: itemIndex, + }); + }, + ); + }); + }); +}); diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index bb1e63a581e..2feaa5dff8c 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -1,5 +1,5 @@ import { GlLabel } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; @@ -45,7 +45,10 @@ describe('Board card', () => { item = mockIssue, } = {}) => { wrapper = mountFn(BoardCard, { - stubs, + stubs: { + ...stubs, + BoardCardInner, + }, store, propsData: { list: mockLabelList, @@ -86,7 +89,7 @@ describe('Board card', () => { describe('when GlLabel is clicked in BoardCardInner', () => { it('doesnt call toggleBoardItem', () => { createStore({ initialState: { isShowingLabels: true } }); - mountComponent({ mountFn: mount, stubs: {} }); + mountComponent(); wrapper.findComponent(GlLabel).trigger('mouseup'); diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js index 8b0100d069a..f097f42476a 100644 --- a/spec/frontend/boards/components/board_new_issue_spec.js +++ b/spec/frontend/boards/components/board_new_issue_spec.js @@ -90,7 +90,7 @@ describe('Issue boards new issue form', () => { }); }); - it('it uses the first issue ID as moveAfterId', async () => { + it('uses the first issue ID as moveAfterId', async () => { findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' }); await nextTick(); diff --git a/spec/frontend/boards/components/issue_due_date_spec.js b/spec/frontend/boards/components/issue_due_date_spec.js index 73340c1b96b..45fa10bf03a 100644 --- a/spec/frontend/boards/components/issue_due_date_spec.js +++ b/spec/frontend/boards/components/issue_due_date_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; -import dateFormat from 'dateformat'; import IssueDueDate from '~/boards/components/issue_due_date.vue'; +import dateFormat from '~/lib/dateformat'; const createComponent = (dueDate = new Date(), closed = false) => shallowMount(IssueDueDate, { diff --git a/spec/frontend/boards/components/item_count_spec.js b/spec/frontend/boards/components/item_count_spec.js index 06cd3910fc0..0c0c7f66933 100644 --- a/spec/frontend/boards/components/item_count_spec.js +++ b/spec/frontend/boards/components/item_count_spec.js @@ -50,7 +50,7 @@ describe('IssueCount', () => { }); it('contains maxIssueCount in the template', () => { - expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount)); + expect(vm.find('.max-issue-size').text()).toContain(String(maxIssueCount)); }); it('does not have text-danger class when issueSize is less than maxIssueCount', () => { @@ -75,7 +75,7 @@ describe('IssueCount', () => { }); it('contains maxIssueCount in the template', () => { - expect(vm.find('.max-issue-size').text()).toEqual(String(maxIssueCount)); + expect(vm.find('.max-issue-size').text()).toContain(String(maxIssueCount)); }); it('has text-danger class', () => { diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 1ee05d81f37..dc1f3246be0 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -262,9 +262,11 @@ export const rawIssue = { epic: { id: 'gid://gitlab/Epic/41', }, + type: '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', @@ -287,6 +289,48 @@ export const mockIssue = { epic: { id: 'gid://gitlab/Epic/41', }, + type: 'ISSUE', +}; + +export const mockEpic = { + id: 'gid://gitlab/Epic/26', + iid: '1', + group: { + id: 'gid://gitlab/Group/33', + fullPath: 'twitter', + __typename: 'Group', + }, + title: 'Eum animi debitis occaecati ad non odio repellat voluptatem similique.', + state: 'opened', + reference: '&1', + referencePath: `${mockEpicFullPath}&1`, + webPath: `/groups/${mockEpicFullPath}/-/epics/1`, + webUrl: `${mockEpicFullPath}/-/epics/1`, + createdAt: '2022-01-18T05:15:15Z', + closedAt: null, + __typename: 'Epic', + relativePosition: null, + confidential: false, + subscribed: true, + blocked: true, + blockedByCount: 1, + labels: { + nodes: [], + __typename: 'LabelConnection', + }, + hasIssues: true, + descendantCounts: { + closedEpics: 0, + closedIssues: 0, + openedEpics: 0, + openedIssues: 2, + __typename: 'EpicDescendantCount', + }, + descendantWeightSum: { + closedIssues: 0, + openedIssues: 0, + __typename: 'EpicDescendantWeights', + }, }; export const mockActiveIssue = { @@ -521,6 +565,15 @@ export const mockBlockingIssue1 = { __typename: 'Issue', }; +export const mockBlockingEpic1 = { + id: 'gid://gitlab/Epic/29', + iid: '4', + title: 'Sint nihil exercitationem aspernatur unde molestiae rem accusantium.', + reference: 'twitter&4', + webUrl: 'http://gdk.test:3000/groups/gitlab-org/test-subgroup/-/epics/4', + __typename: 'Epic', +}; + export const mockBlockingIssue2 = { id: 'gid://gitlab/Issue/524', iid: '5', @@ -562,6 +615,23 @@ export const mockBlockingIssuablesResponse1 = { }, }; +export const mockBlockingEpicIssuablesResponse1 = { + data: { + group: { + __typename: 'Group', + id: 'gid://gitlab/Group/33', + issuable: { + __typename: 'Epic', + id: 'gid://gitlab/Epic/26', + blockingIssuables: { + __typename: 'EpicConnection', + nodes: [mockBlockingEpic1], + }, + }, + }, + }, +}; + export const mockBlockingIssuablesResponse2 = { data: { issuable: { @@ -599,6 +669,12 @@ export const mockBlockedIssue2 = { webUrl: 'http://gdk.test:3000/gitlab-org/my-project-1/-/issues/0', }; +export const mockBlockedEpic1 = { + id: '26', + blockedByCount: 1, + webUrl: 'http://gdk.test:3000/gitlab-org/test-subgroup/-/epics/1', +}; + export const mockMoveIssueParams = { itemId: 1, fromListId: 'gid://gitlab/List/1', diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index e48b946ff1b..e919300228a 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -1056,6 +1056,8 @@ describe('moveIssueCard and undoMoveIssueCard', () => { originalIndex = 0, moveBeforeId = undefined, moveAfterId = undefined, + allItemsLoadedInList = true, + listPosition = undefined, } = {}) => { state = { boardLists: { @@ -1065,12 +1067,28 @@ describe('moveIssueCard and undoMoveIssueCard', () => { boardItems: { [itemId]: originalIssue }, boardItemsByListId: { [fromListId]: [123] }, }; - params = { itemId, fromListId, toListId, moveBeforeId, moveAfterId }; + params = { + itemId, + fromListId, + toListId, + moveBeforeId, + moveAfterId, + listPosition, + allItemsLoadedInList, + }; moveMutations = [ { type: types.REMOVE_BOARD_ITEM_FROM_LIST, payload: { itemId, listId: fromListId } }, { type: types.ADD_BOARD_ITEM_TO_LIST, - payload: { itemId, listId: toListId, moveBeforeId, moveAfterId }, + payload: { + itemId, + listId: toListId, + moveBeforeId, + moveAfterId, + listPosition, + allItemsLoadedInList, + atIndex: originalIndex, + }, }, ]; undoMutations = [ @@ -1365,10 +1383,18 @@ describe('updateIssueOrder', () => { { moveData }, state, [ + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: true, + }, { type: types.MUTATE_ISSUE_SUCCESS, payload: { issue: rawIssue }, }, + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: false, + }, ], [], ); @@ -1389,6 +1415,14 @@ describe('updateIssueOrder', () => { { moveData }, state, [ + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: true, + }, + { + type: types.MUTATE_ISSUE_IN_PROGRESS, + payload: false, + }, { type: types.SET_ERROR, payload: 'An error occurred while moving the issue. Please try again.', diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js index 1606ca09d8f..87a183c0441 100644 --- a/spec/frontend/boards/stores/mutations_spec.js +++ b/spec/frontend/boards/stores/mutations_spec.js @@ -513,6 +513,31 @@ describe('Board Store Mutations', () => { listState: [mockIssue2.id, mockIssue.id], }, ], + [ + 'to the top of the list', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + positionInList: 0, + atIndex: 1, + }, + listState: [mockIssue2.id, mockIssue.id], + }, + ], + [ + 'to the bottom of the list when the list is fully loaded', + { + payload: { + itemId: mockIssue2.id, + listId: mockList.id, + positionInList: -1, + atIndex: 0, + allItemsLoadedInList: true, + }, + listState: [mockIssue.id, mockIssue2.id], + }, + ], ])(`inserts an item into a list %s`, (_, { payload, listState }) => { mutations.ADD_BOARD_ITEM_TO_LIST(state, payload); diff --git a/spec/frontend/branches/components/divergence_graph_spec.js b/spec/frontend/branches/components/divergence_graph_spec.js index 3b565539f87..9429a6e982c 100644 --- a/spec/frontend/branches/components/divergence_graph_spec.js +++ b/spec/frontend/branches/components/divergence_graph_spec.js @@ -21,7 +21,7 @@ describe('Branch divergence graph component', () => { maxCommits: 100, }); - expect(vm.findAll(GraphBar).length).toBe(2); + expect(vm.findAllComponents(GraphBar).length).toBe(2); expect(vm.element).toMatchSnapshot(); }); @@ -45,7 +45,7 @@ describe('Branch divergence graph component', () => { maxCommits: 100, }); - expect(vm.findAll(GraphBar).length).toBe(1); + expect(vm.findAllComponents(GraphBar).length).toBe(1); expect(vm.element).toMatchSnapshot(); }); diff --git a/spec/frontend/captcha/captcha_modal_spec.js b/spec/frontend/captcha/captcha_modal_spec.js index b8448f9ff0a..20e69b5a834 100644 --- a/spec/frontend/captcha/captcha_modal_spec.js +++ b/spec/frontend/captcha/captcha_modal_spec.js @@ -40,7 +40,7 @@ describe('Captcha Modal', () => { }); const findGlModal = () => { - const glModal = wrapper.find(GlModal); + const glModal = wrapper.findComponent(GlModal); jest.spyOn(glModal.vm, 'show').mockImplementation(() => glModal.vm.$emit('shown')); jest diff --git a/spec/frontend/cascading_settings/components/lock_popovers_spec.js b/spec/frontend/cascading_settings/components/lock_popovers_spec.js index 182e3c1c8ff..9d3275a1ff2 100644 --- a/spec/frontend/cascading_settings/components/lock_popovers_spec.js +++ b/spec/frontend/cascading_settings/components/lock_popovers_spec.js @@ -39,7 +39,7 @@ describe('LockPopovers', () => { wrapper = mountExtended(LockPopovers); }; - const findPopover = () => extendedWrapper(wrapper.find(GlPopover)); + const findPopover = () => extendedWrapper(wrapper.findComponent(GlPopover)); const findByTextInPopover = (text, options) => findPopover().findByText((_, element) => element.textContent === text, options); @@ -143,7 +143,7 @@ describe('LockPopovers', () => { }); it('mounts multiple popovers', () => { - const popovers = wrapper.findAll(GlPopover).wrappers; + const popovers = wrapper.findAllComponents(GlPopover).wrappers; expectCorrectPopoverTarget(popoverMountEl1, popovers[0]); expectCorrectPopoverTarget(popoverMountEl2, popovers[1]); diff --git a/spec/frontend/chronic_duration_spec.js b/spec/frontend/chronic_duration_spec.js index 32652e13dfc..b063110782a 100644 --- a/spec/frontend/chronic_duration_spec.js +++ b/spec/frontend/chronic_duration_spec.js @@ -86,7 +86,7 @@ describe('parseChronicDuration', () => { describe('when .raiseExceptions set to true', () => { it('raises with DurationParseError', () => { - expect(() => parseChronicDuration('23 gobblygoos', { raiseExceptions: true })).toThrowError( + expect(() => parseChronicDuration('23 gobblygoos', { raiseExceptions: true })).toThrow( DurationParseError, ); }); diff --git a/spec/frontend/ci_lint/components/ci_lint_spec.js b/spec/frontend/ci_lint/components/ci_lint_spec.js index 0ad6ed56b0e..ea69a80274e 100644 --- a/spec/frontend/ci_lint/components/ci_lint_spec.js +++ b/spec/frontend/ci_lint/components/ci_lint_spec.js @@ -36,9 +36,9 @@ describe('CI Lint', () => { }); }; - const findEditor = () => wrapper.find(SourceEditor); - const findAlert = () => wrapper.find(GlAlert); - const findCiLintResults = () => wrapper.find(CiLintResults); + const findEditor = () => wrapper.findComponent(SourceEditor); + const findAlert = () => wrapper.findComponent(GlAlert); + const findCiLintResults = () => wrapper.findComponent(CiLintResults); const findValidateBtn = () => wrapper.find('[data-testid="ci-lint-validate"]'); const findClearBtn = () => wrapper.find('[data-testid="ci-lint-clear"]'); 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 04d38a3281a..5273aafbb04 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 @@ -83,7 +83,9 @@ describe('SecureFilesList', () => { const [secureFile] = secureFiles; expect(findCell(0, 0).text()).toBe(secureFile.name); - expect(findCell(0, 1).find(TimeAgoTooltip).props('time')).toBe(secureFile.created_at); + expect(findCell(0, 1).findComponent(TimeAgoTooltip).props('time')).toBe( + secureFile.created_at, + ); }); describe('event tracking', () => { diff --git a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js index 6bf28a67300..01eb08f4ece 100644 --- a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js +++ b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js @@ -16,13 +16,13 @@ describe('TriggersList', () => { }); }; - const findTable = () => wrapper.find(GlTable); + const findTable = () => wrapper.findComponent(GlTable); const findHeaderAt = (i) => wrapper.findAll('thead th').at(i); const findRows = () => wrapper.findAll('tbody tr'); const findRowAt = (i) => findRows().at(i); const findCell = (i, col) => findRowAt(i).findAll('td').at(col); - const findClipboardBtn = (i) => findCell(i, 0).find(ClipboardButton); - const findInvalidBadge = (i) => findCell(i, 0).find(GlBadge); + const findClipboardBtn = (i) => findCell(i, 0).findComponent(ClipboardButton); + const findInvalidBadge = (i) => findCell(i, 0).findComponent(GlBadge); const findEditBtn = (i) => findRowAt(i).find('[data-testid="edit-btn"]'); const findRevokeBtn = (i) => findRowAt(i).find('[data-testid="trigger_revoke_button"]'); @@ -65,17 +65,19 @@ describe('TriggersList', () => { it('displays a time ago label when last used', () => { expect(findCell(0, 3).text()).toBe('Never'); - expect(findCell(1, 3).find(TimeAgoTooltip).props('time')).toBe(triggers[1].lastUsed); + expect(findCell(1, 3).findComponent(TimeAgoTooltip).props('time')).toBe(triggers[1].lastUsed); }); it('displays actions in a rows', () => { const [data] = triggers; + const confirmWarning = + 'By revoking a trigger you will break any processes making use of it. Are you sure?'; expect(findEditBtn(0).attributes('href')).toBe(data.editProjectTriggerPath); expect(findRevokeBtn(0).attributes('href')).toBe(data.projectTriggerPath); expect(findRevokeBtn(0).attributes('data-method')).toBe('delete'); - expect(findRevokeBtn(0).attributes('data-confirm')).toBeTruthy(); + expect(findRevokeBtn(0).attributes('data-confirm')).toBe(confirmWarning); }); describe('when there are no triggers set', () => { diff --git a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js new file mode 100644 index 00000000000..867f8e0cf8f --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js @@ -0,0 +1,215 @@ +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import { resolvers } from '~/ci_variable_list/graphql/resolvers'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; + +import ciProjectVariables from '~/ci_variable_list/components/ci_project_variables.vue'; +import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue'; +import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue'; +import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql'; +import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql'; + +import addProjectVariable from '~/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql'; +import deleteProjectVariable from '~/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql'; +import updateProjectVariable from '~/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql'; + +import { + environmentFetchErrorText, + genericMutationErrorText, + variableFetchErrorText, +} from '~/ci_variable_list/constants'; + +import { + devName, + mockProjectEnvironments, + mockProjectVariables, + newVariable, + prodName, +} from '../mocks'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); + +const mockProvide = { + endpoint: '/variables', + projectFullPath: '/namespace/project', + projectId: 1, +}; + +describe('Ci Project Variable list', () => { + let wrapper; + + let mockApollo; + let mockEnvironments; + let mockVariables; + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findCiTable = () => wrapper.findComponent(GlTable); + const findCiSettings = () => wrapper.findComponent(ciVariableSettings); + + // eslint-disable-next-line consistent-return + const createComponentWithApollo = async ({ isLoading = false } = {}) => { + const handlers = [ + [getProjectEnvironments, mockEnvironments], + [getProjectVariables, mockVariables], + ]; + + mockApollo = createMockApollo(handlers, resolvers); + + wrapper = shallowMount(ciProjectVariables, { + provide: mockProvide, + apolloProvider: mockApollo, + stubs: { ciVariableSettings, ciVariableTable }, + }); + + if (!isLoading) { + return waitForPromises(); + } + }; + + beforeEach(() => { + mockEnvironments = jest.fn(); + mockVariables = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('while queries are being fetch', () => { + beforeEach(() => { + createComponentWithApollo({ isLoading: true }); + }); + + it('shows a loading icon', () => { + expect(findLoadingIcon().exists()).toBe(true); + expect(findCiTable().exists()).toBe(false); + }); + }); + + describe('when queries are resolved', () => { + describe('successfuly', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockResolvedValue(mockProjectVariables); + + await createComponentWithApollo(); + }); + + 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('createFlash was not called', () => { + expect(createFlash).not.toHaveBeenCalled(); + }); + }); + + describe('with an error for variables', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockRejectedValue(); + + await createComponentWithApollo(); + }); + + it('calls createFlash with the expected error message', () => { + expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText }); + }); + }); + + describe('with an error for environments', () => { + beforeEach(async () => { + mockEnvironments.mockRejectedValue(); + mockVariables.mockResolvedValue(mockProjectVariables); + + await createComponentWithApollo(); + }); + + it('calls createFlash with the expected error message', () => { + expect(createFlash).toHaveBeenCalledWith({ message: environmentFetchErrorText }); + }); + }); + }); + + describe('mutations', () => { + beforeEach(async () => { + mockEnvironments.mockResolvedValue(mockProjectEnvironments); + mockVariables.mockResolvedValue(mockProjectVariables); + + await createComponentWithApollo(); + }); + it.each` + actionName | mutation | event + ${'add'} | ${addProjectVariable} | ${'add-variable'} + ${'update'} | ${updateProjectVariable} | ${'update-variable'} + ${'delete'} | ${deleteProjectVariable} | ${'delete-variable'} + `( + 'calls the right mutation 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: mockProvide.projectFullPath, + projectId: convertToGraphQLId('Project', mockProvide.projectId), + variable: newVariable, + }, + }); + }, + ); + + it.each` + actionName | event | mutationName + ${'add'} | ${'add-variable'} | ${'addProjectVariable'} + ${'update'} | ${'update-variable'} | ${'updateProjectVariable'} + ${'delete'} | ${'delete-variable'} | ${'deleteProjectVariable'} + `( + 'throws with the specific graphql error if present when user performs $actionName variable', + async ({ event, mutationName }) => { + const graphQLErrorMessage = 'There is a problem with this graphQL action'; + jest + .spyOn(wrapper.vm.$apollo, 'mutate') + .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } }); + await findCiSettings().vm.$emit(event, newVariable); + await nextTick(); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage }); + }, + ); + + it.each` + actionName | event + ${'add'} | ${'add-variable'} + ${'update'} | ${'update-variable'} + ${'delete'} | ${'delete-variable'} + `( + 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable', + async ({ event }) => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => { + throw new Error(); + }); + await findCiSettings().vm.$emit(event, newVariable); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText }); + }, + ); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js index e5019e3261e..1ea4e4f833b 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -11,6 +11,7 @@ import { EVENT_ACTION, ENVIRONMENT_SCOPE_LINK_TITLE, instanceString, + variableOptions, } from '~/ci_variable_list/constants'; import { mockVariablesWithScopes } from '../mocks'; import ModalStub from '../stubs'; @@ -57,21 +58,23 @@ describe('Ci variable modal', () => { }); }; - const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown); + const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown); const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference'); - const findModal = () => wrapper.find(ModalStub); + const findModal = () => wrapper.findComponent(ModalStub); const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip'); const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn'); const deleteVariableButton = () => findModal() - .findAll(GlButton) + .findAllComponents(GlButton) .wrappers.find((button) => button.props('variant') === 'danger'); const findProtectedVariableCheckbox = () => wrapper.findByTestId('ci-variable-protected-checkbox'); const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox'); const findValueField = () => wrapper.find('#ci-variable-value'); const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link'); - const findEnvScopeInput = () => wrapper.findByTestId('environment-scope').find(GlFormInput); + const findEnvScopeInput = () => + wrapper.findByTestId('environment-scope').findComponent(GlFormInput); + const findVariableTypeDropdown = () => wrapper.find('#ci-variable-type'); afterEach(() => { wrapper.destroy(); @@ -83,7 +86,7 @@ describe('Ci variable modal', () => { createComponent(); }); - it('shows the submit button as disabled ', () => { + it('shows the submit button as disabled', () => { expect(findAddorUpdateButton().attributes('disabled')).toBe('true'); }); }); @@ -93,7 +96,7 @@ describe('Ci variable modal', () => { createComponent({ props: { selectedVariable: mockVariables[0] } }); }); - it('shows the submit button as enabled ', () => { + it('shows the submit button as enabled', () => { expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined(); }); }); @@ -284,6 +287,21 @@ describe('Ci variable modal', () => { }); }); + describe('variable type dropdown', () => { + describe('default behaviour', () => { + beforeEach(() => { + createComponent({ mountFn: mountExtended }); + }); + + it('adds each option as a dropdown item', () => { + expect(findVariableTypeDropdown().findAll('option')).toHaveLength(variableOptions.length); + variableOptions.forEach((v) => { + expect(findVariableTypeDropdown().text()).toContain(v.text); + }); + }); + }); + }); + describe('Validations', () => { const maskError = 'This variable can not be masked.'; diff --git a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js index b43153d3d7c..4d0c378d10e 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js @@ -18,7 +18,7 @@ describe('Ci Variable Popover', () => { }); }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); beforeEach(() => { createComponent(); diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js index 6681ab91a4a..b607232907b 100644 --- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js @@ -40,12 +40,12 @@ describe('Ci variable modal', () => { }); }; - const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown); - const findModal = () => wrapper.find(ModalStub); + const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown); + const findModal = () => wrapper.findComponent(ModalStub); const findAddorUpdateButton = () => findModal().find('[data-testid="ciUpdateOrAddVariableBtn"]'); const deleteVariableButton = () => findModal() - .findAll(GlButton) + .findAllComponents(GlButton) .wrappers.find((button) => button.props('variant') === 'danger'); afterEach(() => { @@ -213,7 +213,7 @@ describe('Ci variable modal', () => { const environmentScopeInput = wrapper .find('[data-testid="environment-scope"]') - .find(GlFormInput); + .findComponent(GlFormInput); expect(findCiEnvironmentsDropdown().exists()).toBe(false); expect(environmentScopeInput.attributes('readonly')).toBe('readonly'); }); diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js index 89ba77858dc..6d633c8b740 100644 --- a/spec/frontend/ci_variable_list/mocks.js +++ b/spec/frontend/ci_variable_list/mocks.js @@ -1,4 +1,9 @@ -import { variableTypes, groupString, instanceString } from '~/ci_variable_list/constants'; +import { + variableTypes, + groupString, + instanceString, + projectString, +} from '~/ci_variable_list/constants'; export const devName = 'dev'; export const prodName = 'prod'; @@ -11,8 +16,8 @@ export const mockVariables = (kind) => { key: 'my-var', masked: false, protected: true, - value: 'env_val', - variableType: variableTypes.variableType, + value: 'variable_value', + variableType: variableTypes.envType, }, { __typename: `Ci${kind}Variable`, @@ -20,7 +25,7 @@ export const mockVariables = (kind) => { key: 'secret', masked: true, protected: false, - value: 'the_secret_value', + value: 'another_value', variableType: variableTypes.fileType, }, ]; @@ -77,7 +82,7 @@ export const mockProjectVariables = { project: { __typename: 'Project', id: 1, - ciVariables: createDefaultVars(), + ciVariables: createDefaultVars({ kind: projectString }), }, }, }; diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js index ae750ff426d..c7d07ead09b 100644 --- a/spec/frontend/ci_variable_list/store/mutations_spec.js +++ b/spec/frontend/ci_variable_list/store/mutations_spec.js @@ -36,7 +36,7 @@ describe('CI variable list mutations', () => { }); describe('CLEAR_MODAL', () => { - it('should clear modal state ', () => { + it('should clear modal state', () => { const modalState = { variable_type: 'Variable', key: '', 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 new file mode 100644 index 00000000000..2af64191a88 --- /dev/null +++ b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js @@ -0,0 +1,96 @@ +import { GlLink, GlIcon, GlBadge } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import AgentIntegrationStatusRow from '~/clusters/agents/components/agent_integration_status_row.vue'; + +const defaultProps = { + text: 'Default integration status', +}; + +describe('IntegrationStatus', () => { + let wrapper; + + const createWrapper = ({ props = {}, glFeatures = {} } = {}) => { + wrapper = shallowMount(AgentIntegrationStatusRow, { + propsData: { + ...defaultProps, + ...props, + }, + provide: { + glFeatures, + }, + }); + }; + + const findLink = () => wrapper.findComponent(GlLink); + const findIcon = () => wrapper.findComponent(GlIcon); + const findBadge = () => wrapper.findComponent(GlBadge); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('icon', () => { + const icon = 'status-success'; + const iconClass = 'text-success-500'; + it.each` + props | iconName | iconClassName + ${{ icon, iconClass }} | ${icon} | ${iconClass} + ${{ icon }} | ${icon} | ${'text-info'} + ${{ iconClass }} | ${'information'} | ${iconClass} + ${null} | ${'information'} | ${'text-info'} + `('displays correct icon when props are $props', ({ props, iconName, iconClassName }) => { + createWrapper({ props }); + + expect(findIcon().props('name')).toBe(iconName); + expect(findIcon().attributes('class')).toContain(iconClassName); + }); + }); + + describe('helpUrl', () => { + it('displays a link with the correct help url when provided in props', () => { + const props = { + helpUrl: 'help-page-path', + }; + createWrapper({ props }); + + expect(findLink().attributes('href')).toBe(props.helpUrl); + expect(findLink().text()).toBe(defaultProps.text); + }); + + it("displays the text without a link when it's not provided", () => { + createWrapper(); + + expect(findLink().exists()).toBe(false); + expect(wrapper.text()).toBe(defaultProps.text); + }); + }); + + describe('badge', () => { + it('does not display premium feature badge when featureName is not provided', () => { + createWrapper(); + + expect(findBadge().exists()).toBe(false); + }); + + it('does not display premium feature badge when featureName is provided and is available for the project', () => { + const props = { featureName: 'feature' }; + const glFeatures = { feature: true }; + createWrapper({ props, glFeatures }); + + expect(findBadge().exists()).toBe(false); + }); + + it('displays premium feature badge when featureName is provided and is not available for the project', () => { + const props = { featureName: 'feature' }; + const glFeatures = { feature: false }; + createWrapper({ props, glFeatures }); + + expect(findBadge().props()).toMatchObject({ + icon: 'license', + variant: 'tier', + size: 'md', + }); + expect(findBadge().text()).toBe(wrapper.vm.$options.i18n.premiumTitle); + }); + }); +}); diff --git a/spec/frontend/clusters/agents/components/integration_status_spec.js b/spec/frontend/clusters/agents/components/integration_status_spec.js new file mode 100644 index 00000000000..36f0e622452 --- /dev/null +++ b/spec/frontend/clusters/agents/components/integration_status_spec.js @@ -0,0 +1,111 @@ +import { GlCollapse, GlButton, GlIcon } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import IntegrationStatus from '~/clusters/agents/components/integration_status.vue'; +import AgentIntegrationStatusRow from '~/clusters/agents/components/agent_integration_status_row.vue'; +import { ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants'; +import { + INTEGRATION_STATUS_VALID_TOKEN, + INTEGRATION_STATUS_NO_TOKEN, + INTEGRATION_STATUS_RESTRICTED_CI_CD, +} from '~/clusters/agents/constants'; + +const connectedTimeNow = new Date(); +const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME); + +describe('IntegrationStatus', () => { + let wrapper; + + const createWrapper = (tokens = []) => { + wrapper = shallowMountExtended(IntegrationStatus, { + propsData: { tokens }, + }); + }; + + const findCollapseButton = () => wrapper.findComponent(GlButton); + const findCollapse = () => wrapper.findComponent(GlCollapse); + const findStatusIcon = () => wrapper.findComponent(GlIcon); + const findAgentStatus = () => wrapper.findByTestId('agent-status'); + const findAgentIntegrationStatusRows = () => wrapper.findAllComponents(AgentIntegrationStatusRow); + + afterEach(() => { + wrapper.destroy(); + }); + + it.each` + lastUsedAt | status | iconName + ${null} | ${'Never connected'} | ${'status-neutral'} + ${connectedTimeNow} | ${'Connected'} | ${'status-success'} + ${connectedTimeInactive} | ${'Not connected'} | ${'status-alert'} + `( + 'displays correct text and icon when agent connection status is "$status"', + ({ lastUsedAt, status, iconName }) => { + const tokens = [{ lastUsedAt }]; + createWrapper(tokens); + + expect(findStatusIcon().props('name')).toBe(iconName); + expect(findAgentStatus().text()).toBe(status); + }, + ); + + describe('default', () => { + beforeEach(() => { + createWrapper(); + }); + + it('shows the collapse toggle button', () => { + expect(findCollapseButton().text()).toBe(wrapper.vm.$options.i18n.title); + expect(findCollapseButton().attributes()).toMatchObject({ + variant: 'link', + icon: 'chevron-right', + size: 'small', + }); + }); + + it('sets collapse component as invisible by default', () => { + expect(findCollapse().props('visible')).toBeUndefined(); + }); + }); + + describe('when user clicks collapse toggle', () => { + beforeEach(() => { + createWrapper(); + findCollapseButton().vm.$emit('click'); + }); + + it('changes the collapse button icon', () => { + expect(findCollapseButton().props('icon')).toBe('chevron-down'); + }); + + it('sets collapse component as visible', () => { + expect(findCollapse().attributes('visible')).toBe('true'); + }); + }); + + describe('integration status details', () => { + it.each` + agentStatus | tokens | integrationStatuses + ${'active'} | ${[{ lastUsedAt: connectedTimeNow }]} | ${[INTEGRATION_STATUS_VALID_TOKEN, INTEGRATION_STATUS_RESTRICTED_CI_CD]} + ${'inactive'} | ${[{ lastUsedAt: connectedTimeInactive }]} | ${[INTEGRATION_STATUS_RESTRICTED_CI_CD]} + ${'inactive'} | ${[]} | ${[INTEGRATION_STATUS_NO_TOKEN, INTEGRATION_STATUS_RESTRICTED_CI_CD]} + ${'unused'} | ${[{ lastUsedAt: null }]} | ${[INTEGRATION_STATUS_RESTRICTED_CI_CD]} + ${'unused'} | ${[]} | ${[INTEGRATION_STATUS_NO_TOKEN, INTEGRATION_STATUS_RESTRICTED_CI_CD]} + `( + 'displays AgentIntegrationStatusRow component with correct properties when agent status is $agentStatus and agent has $tokens.length tokens', + ({ tokens, integrationStatuses }) => { + createWrapper(tokens); + + expect(findAgentIntegrationStatusRows().length).toBe(integrationStatuses.length); + + integrationStatuses.forEach((integrationStatus, index) => { + expect(findAgentIntegrationStatusRows().at(index).props()).toMatchObject({ + icon: integrationStatus.icon, + iconClass: integrationStatus.iconClass, + text: integrationStatus.text, + helpUrl: integrationStatus.helpUrl || null, + featureName: integrationStatus.featureName || null, + }); + }); + }, + ); + }); +}); diff --git a/spec/frontend/clusters/agents/components/show_spec.js b/spec/frontend/clusters/agents/components/show_spec.js index f2f073544e3..efa85136b17 100644 --- a/spec/frontend/clusters/agents/components/show_spec.js +++ b/spec/frontend/clusters/agents/components/show_spec.js @@ -6,6 +6,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import ClusterAgentShow from '~/clusters/agents/components/show.vue'; import TokenTable from '~/clusters/agents/components/token_table.vue'; import ActivityEvents from '~/clusters/agents/components/activity_events_list.vue'; +import IntegrationStatus from '~/clusters/agents/components/integration_status.vue'; import getAgentQuery from '~/clusters/agents/graphql/queries/get_cluster_agent.query.graphql'; import { useFakeDate } from 'helpers/fake_date'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -76,6 +77,7 @@ describe('ClusterAgentShow', () => { const findTokenCount = () => wrapper.findByTestId('cluster-agent-token-count').text(); const findEESecurityTabSlot = () => wrapper.findByTestId('ee-security-tab'); const findActivity = () => wrapper.findComponent(ActivityEvents); + const findIntegrationStatus = () => wrapper.findComponent(IntegrationStatus); afterEach(() => { wrapper.destroy(); @@ -107,6 +109,10 @@ describe('ClusterAgentShow', () => { expect(findCreatedText()).toMatchInterpolatedText('Created by user-1 2 days ago'); }); + it('displays agent integration status section', () => { + expect(findIntegrationStatus().exists()).toBe(true); + }); + it('displays token count', () => { expect(findTokenCount()).toMatchInterpolatedText( `${ClusterAgentShow.i18n.tokens} ${defaultClusterAgent.tokens.count}`, diff --git a/spec/frontend/clusters_list/components/agent_table_spec.js b/spec/frontend/clusters_list/components/agent_table_spec.js index b78f0a3686c..9cbb83eedd2 100644 --- a/spec/frontend/clusters_list/components/agent_table_spec.js +++ b/spec/frontend/clusters_list/components/agent_table_spec.js @@ -36,7 +36,7 @@ describe('AgentTable', () => { const findAgentLink = (at) => wrapper.findAllByTestId('cluster-agent-name-link').at(at); const findStatusText = (at) => wrapper.findAllByTestId('cluster-agent-connection-status').at(at); - const findStatusIcon = (at) => findStatusText(at).find(GlIcon); + 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 findConfiguration = (at) => @@ -113,7 +113,7 @@ describe('AgentTable', () => { texts, lineNumber, }) => { - const findIcon = () => findVersionText(lineNumber).find(GlIcon); + const findIcon = () => findVersionText(lineNumber).findComponent(GlIcon); const findPopover = () => wrapper.findByTestId(`popover-${agent}`); const versionWarning = versionMismatch || versionOutdated; @@ -151,7 +151,7 @@ describe('AgentTable', () => { `( 'displays config file path as "$agentPath" at line $lineNumber', ({ agentConfig, link, lineNumber }) => { - const findLink = findConfiguration(lineNumber).find(GlLink); + const findLink = findConfiguration(lineNumber).findComponent(GlLink); expect(findLink.attributes('href')).toBe(link); expect(findConfiguration(lineNumber).text()).toBe(agentConfig); diff --git a/spec/frontend/clusters_list/components/agents_spec.js b/spec/frontend/clusters_list/components/agents_spec.js index 92cfff7d490..bff1e573dbd 100644 --- a/spec/frontend/clusters_list/components/agents_spec.js +++ b/spec/frontend/clusters_list/components/agents_spec.js @@ -334,7 +334,7 @@ describe('Agents', () => { }); it('displays a loading icon', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js index a9f11e6fdf8..758f6586e1a 100644 --- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js +++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js @@ -46,7 +46,7 @@ describe('ClustersAncestorNotice', () => { }); it('displays link', () => { - expect(wrapper.find(GlLink).exists()).toBe(true); + expect(wrapper.findComponent(GlLink).exists()).toBe(true); }); }); }); 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 218463b9adf..6f23ed47d2a 100644 --- a/spec/frontend/clusters_list/components/clusters_main_view_spec.js +++ b/spec/frontend/clusters_list/components/clusters_main_view_spec.js @@ -142,7 +142,7 @@ describe('ClustersMainViewComponent', () => { createWrapper({ certificateBasedClustersEnabled: false }); }); - it('it displays only the Agent tab', () => { + it('displays only the Agent tab', () => { expect(findAllTabs()).toHaveLength(1); const agentTab = findGlTabAtIndex(0); diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js index 5c7635c1617..a3f42c1f161 100644 --- a/spec/frontend/clusters_list/components/clusters_spec.js +++ b/spec/frontend/clusters_list/components/clusters_spec.js @@ -142,7 +142,7 @@ describe('Clusters', () => { ({ lineNumber, result }) => { const statuses = findStatuses(); const status = statuses.at(lineNumber); - expect(status.find(GlLoadingIcon).exists()).toBe(result); + expect(status.findComponent(GlLoadingIcon).exists()).toBe(result); }, ); }); 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 964dd005a27..10264d6a011 100644 --- a/spec/frontend/clusters_list/components/install_agent_modal_spec.js +++ b/spec/frontend/clusters_list/components/install_agent_modal_spec.js @@ -65,7 +65,7 @@ describe('InstallAgentModal', () => { const findAgentInstructions = () => findModal().findComponent(AgentToken); const findButtonByVariant = (variant) => findModal() - .findAll(GlButton) + .findAllComponents(GlButton) .wrappers.find((button) => button.props('variant') === variant); const findActionButton = () => findButtonByVariant('confirm'); const findCancelButton = () => findButtonByVariant('default'); 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 8187ab75c58..3211ba44eff 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 @@ -11,7 +11,7 @@ describe('NodeErrorHelpText', () => { await nextTick(); }; - const findPopover = () => wrapper.find(GlPopover); + const findPopover = () => wrapper.findComponent(GlPopover); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js index b85047dc816..b9be262efd0 100644 --- a/spec/frontend/code_navigation/components/app_spec.js +++ b/spec/frontend/code_navigation/components/app_spec.js @@ -63,7 +63,7 @@ describe('Code navigation app component', () => { it('hides popover when no definition set', () => { factory(); - expect(wrapper.find(Popover).exists()).toBe(false); + expect(wrapper.findComponent(Popover).exists()).toBe(false); }); it('renders popover when definition set', () => { @@ -73,7 +73,7 @@ describe('Code navigation app component', () => { currentBlobPath: 'index.js', }); - expect(wrapper.find(Popover).exists()).toBe(true); + expect(wrapper.findComponent(Popover).exists()).toBe(true); }); it('calls showDefinition when clicking blob viewer', () => { diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js index c038c04a0f8..874263e046a 100644 --- a/spec/frontend/code_navigation/components/popover_spec.js +++ b/spec/frontend/code_navigation/components/popover_spec.js @@ -115,8 +115,8 @@ describe('Code navigation popover component', () => { definitionPathPrefix: DEFINITION_PATH_PREFIX, }); - expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true); - expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'code-output' }).exists()).toBe(true); + expect(wrapper.findComponent({ ref: 'doc-output' }).exists()).toBe(false); }); }); @@ -128,8 +128,8 @@ describe('Code navigation popover component', () => { definitionPathPrefix: DEFINITION_PATH_PREFIX, }); - expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false); - expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true); + expect(wrapper.findComponent({ ref: 'code-output' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'doc-output' }).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/code_navigation/utils/index_spec.js b/spec/frontend/code_navigation/utils/index_spec.js index b8448709f0b..700c912029c 100644 --- a/spec/frontend/code_navigation/utils/index_spec.js +++ b/spec/frontend/code_navigation/utils/index_spec.js @@ -17,7 +17,7 @@ describe('getCurrentHoverElement', () => { value ${'test'} ${undefined} - `('it returns cached current key', ({ value }) => { + `('returns cached current key', ({ value }) => { if (value) { cachedData.set('current', value); } @@ -52,7 +52,7 @@ describe('addInteractionClass', () => { ${1} | ${0} | ${0} ${1} | ${0} | ${0} `( - 'it sets code navigation attributes for line $line and character $char', + 'sets code navigation attributes for line $line and character $char', ({ line, char, index }) => { addInteractionClass({ path: 'index.js', d: { start_line: line, start_char: char } }); 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 b1c8ba48475..fddc767953a 100644 --- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js +++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js @@ -1,14 +1,24 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import { GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import createMockApollo from 'helpers/mock_apollo_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; 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 { mockPipelineStagesQueryResponse, mockStages } from './mock_data'; +import * as graphQlUtils from '~/pipelines/components/graph/utils'; +import { + mockDownstreamQueryResponse, + mockPipelineStagesQueryResponse, + mockStages, + mockUpstreamDownstreamQueryResponse, + mockUpstreamQueryResponse, +} from './mock_data'; jest.mock('~/flash'); @@ -17,61 +27,219 @@ Vue.use(VueApollo); describe('Commit box pipeline mini graph', () => { let wrapper; - const findMiniGraph = () => wrapper.findByTestId('commit-box-mini-graph'); - const findUpstream = () => wrapper.findByTestId('commit-box-mini-graph-upstream'); - const findDownstream = () => wrapper.findByTestId('commit-box-mini-graph-downstream'); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); + const downstreamHandler = jest.fn().mockResolvedValue(mockDownstreamQueryResponse); + const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); const stagesHandler = jest.fn().mockResolvedValue(mockPipelineStagesQueryResponse); + const upstreamDownstreamHandler = jest + .fn() + .mockResolvedValue(mockUpstreamDownstreamQueryResponse); + const upstreamHandler = jest.fn().mockResolvedValue(mockUpstreamQueryResponse); + const advanceToNextFetch = () => { + jest.advanceTimersByTime(COMMIT_BOX_POLL_INTERVAL); + }; - const createComponent = ({ props = {} } = {}) => { - const handlers = [ - [getLinkedPipelinesQuery, {}], + const fullPath = 'gitlab-org/gitlab'; + const iid = '315'; + const createMockApolloProvider = (handler = downstreamHandler) => { + const requestHandlers = [ + [getLinkedPipelinesQuery, handler], [getPipelineStagesQuery, stagesHandler], ]; + return createMockApollo(requestHandlers); + }; + + const createComponent = (handler) => { wrapper = extendedWrapper( shallowMount(CommitBoxPipelineMiniGraph, { propsData: { stages: mockStages, - ...props, }, - apolloProvider: createMockApollo(handlers), + provide: { + fullPath, + iid, + dataMethod: 'graphql', + graphqlResourceEtag: '/api/graphql:pipelines/id/320', + }, + apolloProvider: createMockApolloProvider(handler), }), ); - - return waitForPromises(); }; afterEach(() => { wrapper.destroy(); }); - describe('linked pipelines', () => { + describe('loading state', () => { + it('should display loading state when loading', () => { + createComponent(); + + expect(findLoadingIcon().exists()).toBe(true); + expect(findPipelineMiniGraph().exists()).toBe(false); + }); + }); + + describe('loaded state', () => { beforeEach(async () => { await createComponent(); }); - it('should display the mini pipeine graph', () => { - expect(findMiniGraph().exists()).toBe(true); + it('should not display loading state after the query is resolved', async () => { + expect(findLoadingIcon().exists()).toBe(false); + expect(findPipelineMiniGraph().exists()).toBe(true); }); - it('should not display linked pipelines', () => { - expect(findUpstream().exists()).toBe(false); - expect(findDownstream().exists()).toBe(false); + it('should display the pipeline mini graph', () => { + expect(findPipelineMiniGraph().exists()).toBe(true); }); }); - describe('when data is mismatched', () => { - beforeEach(async () => { - await createComponent({ props: { stages: [] } }); + describe('load upstream/downstream', () => { + const samplePipeline = { + __typename: expect.any(String), + id: expect.any(String), + path: expect.any(String), + project: expect.any(Object), + detailedStatus: expect.any(Object), + }; + + it('formatted stages should be passed to the pipeline mini graph', async () => { + const stage = mockStages[0]; + const expectedStages = [ + { + name: stage.name, + status: { + __typename: 'DetailedStatus', + id: stage.status.id, + icon: stage.status.icon, + group: stage.status.group, + }, + dropdown_path: stage.dropdown_path, + title: stage.title, + }, + ]; + + createComponent(); + + await waitForPromises(); + + expect(findPipelineMiniGraph().props('stages')).toEqual(expectedStages); + }); + + it('should render a downstream pipeline only', async () => { + createComponent(downstreamHandler); + + await waitForPromises(); + + const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines'); + const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline'); + + expect(downstreamPipelines).toEqual(expect.any(Array)); + expect(upstreamPipeline).toEqual(null); + }); + + it('should pass the pipeline path prop for the counter badge', async () => { + createComponent(downstreamHandler); + + await waitForPromises(); + + const expectedPath = mockDownstreamQueryResponse.data.project.pipeline.path; + const pipelinePath = findPipelineMiniGraph().props('pipelinePath'); + + expect(pipelinePath).toBe(expectedPath); + }); + + it('should render an upstream pipeline only', async () => { + createComponent(upstreamHandler); + + await waitForPromises(); + + const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines'); + const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline'); + + expect(upstreamPipeline).toEqual(samplePipeline); + expect(downstreamPipelines).toHaveLength(0); }); - it('calls create flash with expected arguments', () => { + it('should render downstream and upstream pipelines', async () => { + createComponent(upstreamDownstreamHandler); + + await waitForPromises(); + + const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines'); + const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline'); + + expect(upstreamPipeline).toEqual(samplePipeline); + expect(downstreamPipelines).toEqual(expect.arrayContaining([samplePipeline])); + }); + }); + + describe('error state', () => { + it('createFlash should show if there is an error fetching the data', async () => { + createComponent({ handler: failedHandler }); + + await waitForPromises(); + expect(createFlash).toHaveBeenCalledWith({ - message: 'There was a problem handling the pipeline data.', - captureError: true, - error: new Error('Rest stages and graphQl stages must be the same length'), + message: 'There was a problem fetching linked pipelines.', }); }); }); + + describe('polling', () => { + it('polling interval is set for linked pipelines', () => { + createComponent(); + + const expectedInterval = wrapper.vm.$apollo.queries.pipeline.options.pollInterval; + + expect(expectedInterval).toBe(COMMIT_BOX_POLL_INTERVAL); + }); + + it('polling interval is set for pipeline stages', () => { + createComponent(); + + const expectedInterval = wrapper.vm.$apollo.queries.pipelineStages.options.pollInterval; + + expect(expectedInterval).toBe(COMMIT_BOX_POLL_INTERVAL); + }); + + it('polls for stages and linked pipelines', async () => { + createComponent(); + + await waitForPromises(); + + expect(stagesHandler).toHaveBeenCalledTimes(1); + expect(downstreamHandler).toHaveBeenCalledTimes(1); + + advanceToNextFetch(); + await waitForPromises(); + + expect(stagesHandler).toHaveBeenCalledTimes(2); + expect(downstreamHandler).toHaveBeenCalledTimes(2); + + advanceToNextFetch(); + await waitForPromises(); + + expect(stagesHandler).toHaveBeenCalledTimes(3); + expect(downstreamHandler).toHaveBeenCalledTimes(3); + }); + + it('toggles query polling with visibility check', async () => { + jest.spyOn(graphQlUtils, 'toggleQueryPollingByVisibility'); + + createComponent(); + + await waitForPromises(); + + expect(graphQlUtils.toggleQueryPollingByVisibility).toHaveBeenCalledWith( + wrapper.vm.$apollo.queries.pipelineStages, + ); + expect(graphQlUtils.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 43db6db00c1..73720c1cc88 100644 --- a/spec/frontend/commit/commit_pipeline_status_component_spec.js +++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js @@ -37,9 +37,9 @@ describe('Commit pipeline status component', () => { }); }; - const findLoader = () => wrapper.find(GlLoadingIcon); + const findLoader = () => wrapper.findComponent(GlLoadingIcon); const findLink = () => wrapper.find('a'); - const findCiIcon = () => findLink().find(CiIcon); + const findCiIcon = () => findLink().findComponent(CiIcon); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js index 8db162c07c2..aef137e6fa5 100644 --- a/spec/frontend/commit/mock_data.js +++ b/spec/frontend/commit/mock_data.js @@ -3,116 +3,21 @@ export const mockStages = [ name: 'build', title: 'build: passed', status: { + __typename: 'DetailedStatus', + id: 'success-409-409', icon: 'status_success', text: 'passed', label: 'passed', group: 'success', tooltip: 'passed', has_details: true, - details_path: '/root/ci-project/-/pipelines/611#build', + details_path: '/root/ci-project/-/pipelines/318#build', illustration: null, favicon: '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', }, - path: '/root/ci-project/-/pipelines/611#build', - dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=build', - }, - { - name: 'test', - title: 'test: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/ci-project/-/pipelines/611#test', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/ci-project/-/pipelines/611#test', - dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test', - }, - { - name: 'test_two', - title: 'test_two: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/ci-project/-/pipelines/611#test_two', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/ci-project/-/pipelines/611#test_two', - dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=test_two', - }, - { - name: 'manual', - title: 'manual: skipped', - status: { - icon: 'status_skipped', - text: 'skipped', - label: 'skipped', - group: 'skipped', - tooltip: 'skipped', - has_details: true, - details_path: '/root/ci-project/-/pipelines/611#manual', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', - action: { - icon: 'play', - title: 'Play all manual', - path: '/root/ci-project/-/pipelines/611/stages/manual/play_manual', - method: 'post', - button_title: 'Play all manual', - }, - }, - path: '/root/ci-project/-/pipelines/611#manual', - dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=manual', - }, - { - name: 'deploy', - title: 'deploy: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/ci-project/-/pipelines/611#deploy', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/ci-project/-/pipelines/611#deploy', - dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=deploy', - }, - { - name: 'qa', - title: 'qa: passed', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - tooltip: 'passed', - has_details: true, - details_path: '/root/ci-project/-/pipelines/611#qa', - illustration: null, - favicon: - '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', - }, - path: '/root/ci-project/-/pipelines/611#qa', - dropdown_path: '/root/ci-project/-/pipelines/611/stage.json?stage=qa', + path: '/root/ci-project/-/pipelines/318#build', + dropdown_path: '/root/ci-project/-/pipelines/318/stage.json?stage=build', }, ]; @@ -161,3 +66,109 @@ export const mockPipelineStatusResponse = { }, }, }; + +export const mockDownstreamQueryResponse = { + data: { + project: { + id: '1', + pipeline: { + path: '/root/ci-project/-/pipelines/790', + id: 'pipeline-1', + downstream: { + nodes: [ + { + id: 'gid://gitlab/Ci::Pipeline/612', + path: '/root/job-log-sections/-/pipelines/612', + project: { id: '1', name: 'job-log-sections', __typename: 'Project' }, + detailedStatus: { + id: 'status-1', + group: 'success', + icon: 'status_success', + label: 'passed', + __typename: 'DetailedStatus', + }, + __typename: 'Pipeline', + }, + ], + __typename: 'PipelineConnection', + }, + upstream: null, + }, + __typename: 'Project', + }, + }, +}; + +export const mockUpstreamDownstreamQueryResponse = { + data: { + project: { + id: '1', + pipeline: { + id: 'pipeline-1', + path: '/root/ci-project/-/pipelines/790', + downstream: { + nodes: [ + { + id: 'gid://gitlab/Ci::Pipeline/612', + path: '/root/job-log-sections/-/pipelines/612', + project: { id: '1', name: 'job-log-sections', __typename: 'Project' }, + detailedStatus: { + id: 'status-1', + group: 'success', + icon: 'status_success', + label: 'passed', + __typename: 'DetailedStatus', + }, + __typename: 'Pipeline', + }, + ], + __typename: 'PipelineConnection', + }, + upstream: { + id: 'gid://gitlab/Ci::Pipeline/610', + path: '/root/trigger-downstream/-/pipelines/610', + project: { id: '1', name: 'trigger-downstream', __typename: 'Project' }, + detailedStatus: { + id: 'status-1', + group: 'success', + icon: 'status_success', + label: 'passed', + __typename: 'DetailedStatus', + }, + __typename: 'Pipeline', + }, + }, + __typename: 'Project', + }, + }, +}; + +export const mockUpstreamQueryResponse = { + data: { + project: { + id: '1', + pipeline: { + id: 'pipeline-1', + path: '/root/ci-project/-/pipelines/790', + downstream: { + nodes: [], + __typename: 'PipelineConnection', + }, + upstream: { + id: 'gid://gitlab/Ci::Pipeline/610', + path: '/root/trigger-downstream/-/pipelines/610', + project: { id: '1', name: 'trigger-downstream', __typename: 'Project' }, + detailedStatus: { + id: 'status-1', + group: 'success', + icon: 'status_success', + label: 'passed', + __typename: 'DetailedStatus', + }, + __typename: 'Pipeline', + }, + }, + __typename: 'Project', + }, + }, +}; diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js index 71ee12cf02d..d89a238105b 100644 --- a/spec/frontend/commit/pipelines/pipelines_table_spec.js +++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js @@ -302,6 +302,33 @@ describe('Pipelines table in Commits and Merge requests', () => { expect(findModal()).not.toBeNull(); }); }); + + describe('when no pipelines were created on a forked merge request', () => { + beforeEach(async () => { + mock.onGet('endpoint.json').reply(200, []); + + createComponent({ + projectId: '5', + mergeRequestId: 3, + canCreatePipelineInTargetProject: true, + sourceProjectFullPath: 'test/parent-project', + targetProjectFullPath: 'test/fork-project', + }); + + jest.spyOn(findModal().vm, 'show').mockReturnValue(); + + await waitForPromises(); + }); + + it('should show security modal from empty state run pipeline button', () => { + expect(findEmptyState().exists()).toBe(true); + expect(findModal().exists()).toBe(true); + + findRunPipelineBtn().trigger('click'); + + expect(findModal().vm.show).toHaveBeenCalled(); + }); + }); }); describe('unsuccessfull request', () => { diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js index 14a0b98a0d5..770f2636648 100644 --- a/spec/frontend/confidential_merge_request/components/dropdown_spec.js +++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js @@ -30,18 +30,18 @@ describe('Confidential merge request project dropdown component', () => { }, ]); - expect(vm.findAll(GlDropdownItem).length).toBe(2); + expect(vm.findAllComponents(GlDropdownItem).length).toBe(2); }); it('shows lock icon', () => { factory(); - expect(vm.find(GlDropdown).props('icon')).toBe('lock'); + expect(vm.findComponent(GlDropdown).props('icon')).toBe('lock'); }); it('has dropdown text', () => { factory(); - expect(vm.find(GlDropdown).props('text')).toBe('Select private project'); + expect(vm.findComponent(GlDropdown).props('text')).toBe('Select private project'); }); }); 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 index b54f7cf17c8..6ad8a9de8d3 100644 --- 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 @@ -1,49 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`content_editor/components/toolbar_link_button renders dropdown component 1`] = ` -"
- -
    -
    +"
    +
  • +
    +
    + + +
    + +
    +
    +
  • +
  • +
    +
  • +
  • - -
    - - -
  • -
    -
  • -
-
- +
" `; 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 new file mode 100644 index 00000000000..0700cf5d529 --- /dev/null +++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js @@ -0,0 +1,126 @@ +import { BubbleMenuPlugin } from '@tiptap/extension-bubble-menu'; +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue'; +import { createTestEditor } from '../../test_utils'; + +jest.mock('@tiptap/extension-bubble-menu'); + +describe('content_editor/components/bubble_menus/bubble_menu', () => { + let wrapper; + let tiptapEditor; + const pluginKey = 'key'; + const shouldShow = jest.fn(); + const tippyOptions = { placement: 'bottom' }; + const pluginInitializationResult = {}; + + const buildEditor = () => { + tiptapEditor = createTestEditor(); + }; + + const createWrapper = (propsData = {}) => { + wrapper = shallowMountExtended(BubbleMenu, { + provide: { + tiptapEditor, + }, + propsData: { + pluginKey, + shouldShow, + tippyOptions, + ...propsData, + }, + slots: { + default: '
menu content
', + }, + }); + }; + + const setupMocks = () => { + BubbleMenuPlugin.mockReturnValueOnce(pluginInitializationResult); + jest.spyOn(tiptapEditor, 'registerPlugin').mockImplementationOnce(() => true); + }; + + const invokeTippyEvent = (eventName, eventArgs) => { + const pluginConfig = BubbleMenuPlugin.mock.calls[0][0]; + + pluginConfig.tippyOptions[eventName](eventArgs); + }; + + beforeEach(() => { + buildEditor(); + setupMocks(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('initializes BubbleMenuPlugin', async () => { + createWrapper({}); + + await nextTick(); + + expect(BubbleMenuPlugin).toHaveBeenCalledWith({ + pluginKey, + editor: tiptapEditor, + shouldShow, + element: wrapper.vm.$el, + tippyOptions: expect.objectContaining({ + onHidden: expect.any(Function), + onShow: expect.any(Function), + ...tippyOptions, + }), + }); + + expect(tiptapEditor.registerPlugin).toHaveBeenCalledWith(pluginInitializationResult); + }); + + it('does not render default slot by default', async () => { + createWrapper({}); + + await nextTick(); + + expect(wrapper.text()).not.toContain('menu content'); + }); + + describe('when onShow event handler is invoked', () => { + const onShowArgs = {}; + + beforeEach(async () => { + createWrapper({}); + + await nextTick(); + + invokeTippyEvent('onShow', onShowArgs); + }); + + it('displays the menu content', () => { + expect(wrapper.text()).toContain('menu content'); + }); + + it('emits show event', () => { + expect(wrapper.emitted('show')).toEqual([[onShowArgs]]); + }); + }); + + describe('when onHidden event handler is invoked', () => { + const onHiddenArgs = {}; + + beforeEach(async () => { + createWrapper({}); + + await nextTick(); + + invokeTippyEvent('onShow', onHiddenArgs); + invokeTippyEvent('onHidden', onHiddenArgs); + }); + + it('displays the menu content', () => { + expect(wrapper.text()).not.toContain('menu content'); + }); + + it('emits show event', () => { + expect(wrapper.emitted('hidden')).toEqual([[onHiddenArgs]]); + }); + }); +}); 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 new file mode 100644 index 00000000000..378b11f4ae9 --- /dev/null +++ b/spec/frontend/content_editor/components/bubble_menus/code_block_bubble_menu_spec.js @@ -0,0 +1,296 @@ +import { + GlDropdown, + GlDropdownForm, + GlDropdownItem, + GlSearchBoxByType, + GlFormInput, +} from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { stubComponent } from 'helpers/stub_component'; +import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block_bubble_menu.vue'; +import eventHubFactory from '~/helpers/event_hub_factory'; +import BubbleMenu from '~/content_editor/components/bubble_menus/bubble_menu.vue'; +import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; +import Diagram from '~/content_editor/extensions/diagram'; +import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader'; +import { createTestEditor, emitEditorEvent } from '../../test_utils'; + +const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); + +describe('content_editor/components/bubble_menus/code_block_bubble_menu', () => { + let wrapper; + let tiptapEditor; + let contentEditor; + let bubbleMenu; + let eventHub; + + const buildEditor = () => { + tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] }); + contentEditor = { renderDiagram: jest.fn() }; + eventHub = eventHubFactory(); + }; + + const buildWrapper = () => { + wrapper = mountExtended(CodeBlockBubbleMenu, { + provide: { + tiptapEditor, + contentEditor, + eventHub, + }, + stubs: { + GlDropdownItem: stubComponent(GlDropdownItem), + BubbleMenu: stubComponent(BubbleMenu), + }, + }); + }; + + const preTag = ({ language, content = 'test' } = {}) => { + const languageAttr = language ? ` lang="${language}"` : ''; + + return `
${content}
`; + }; + + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemsData = () => + findDropdownItems().wrappers.map((x) => ({ + text: x.text(), + visible: x.isVisible(), + checked: x.props('isChecked'), + })); + + beforeEach(async () => { + buildEditor(); + buildWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders bubble menu component', async () => { + tiptapEditor.commands.insertContent(preTag()); + bubbleMenu = wrapper.findComponent(BubbleMenu); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + + expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); + }); + + it('selects plaintext language by default', async () => { + tiptapEditor.commands.insertContent(preTag()); + bubbleMenu = wrapper.findComponent(BubbleMenu); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Plain text'); + }); + + it('selects appropriate language based on the code block', async () => { + tiptapEditor.commands.insertContent(preTag({ language: 'javascript' })); + bubbleMenu = wrapper.findComponent(BubbleMenu); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Javascript'); + }); + + it('selects diagram sytnax for mermaid', async () => { + tiptapEditor.commands.insertContent(preTag({ language: 'mermaid' })); + bubbleMenu = wrapper.findComponent(BubbleMenu); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Diagram (mermaid)'); + }); + + it("selects Custom (syntax) if the language doesn't exist in the list", async () => { + tiptapEditor.commands.insertContent(preTag({ language: 'nomnoml' })); + bubbleMenu = wrapper.findComponent(BubbleMenu); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Custom (nomnoml)'); + }); + + describe('copy button', () => { + it('copies the text of the code block', async () => { + const content = 'var a = Math.PI / 2;'; + jest.spyOn(navigator.clipboard, 'writeText'); + + tiptapEditor.commands.insertContent(preTag({ language: 'javascript', content })); + + await wrapper.findByTestId('copy-code-block').vm.$emit('click'); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(content); + }); + }); + + describe('delete button', () => { + it('deletes the code block', async () => { + tiptapEditor.commands.insertContent(preTag({ language: 'javascript' })); + + await wrapper.findByTestId('delete-code-block').vm.$emit('click'); + + expect(tiptapEditor.getText()).toBe(''); + }); + }); + + describe('preview button', () => { + it('does not appear for a regular code block', async () => { + tiptapEditor.commands.insertContent('
var a = 2;
'); + + expect(wrapper.findByTestId('preview-diagram').exists()).toBe(false); + }); + + it.each` + diagramType | diagramCode + ${'mermaid'} | ${'
graph TD;\n    A-->B;
'} + ${'nomnoml'} | ${''} + `('toggles preview for a $diagramType diagram', async ({ diagramType, diagramCode }) => { + tiptapEditor.commands.insertContent(diagramCode); + + await nextTick(); + await wrapper.findByTestId('preview-diagram').vm.$emit('click'); + + expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({ + isDiagram: true, + language: diagramType, + showPreview: false, + }); + + await wrapper.findByTestId('preview-diagram').vm.$emit('click'); + + expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({ + isDiagram: true, + language: diagramType, + showPreview: true, + }); + }); + }); + + describe('when opened and search is changed', () => { + beforeEach(async () => { + tiptapEditor.commands.insertContent(preTag({ language: 'javascript' })); + + wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'js'); + + await nextTick(); + }); + + it('shows dropdown items', () => { + expect(findDropdownItemsData()).toEqual( + expect.arrayContaining([ + { text: 'Javascript', visible: true, checked: true }, + { text: 'Java', visible: true, checked: false }, + { text: 'Javascript', visible: false, checked: false }, + { text: 'JSON', visible: true, checked: false }, + ]), + ); + }); + + describe('when dropdown item is clicked', () => { + beforeEach(async () => { + jest.spyOn(codeBlockLanguageLoader, 'loadLanguage').mockResolvedValue(); + + findDropdownItems().at(1).vm.$emit('click'); + + await nextTick(); + }); + + it('loads language', () => { + expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith('java'); + }); + + it('sets code block', () => { + expect(tiptapEditor.getJSON()).toMatchObject({ + content: [ + { + type: 'codeBlock', + attrs: { + language: 'java', + }, + }, + ], + }); + }); + + it('updates selected dropdown', () => { + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Java'); + }); + }); + + describe('Create custom type', () => { + beforeEach(async () => { + tiptapEditor.commands.insertContent('
var a = 2;
'); + + await wrapper.findComponent(GlDropdown).vm.show(); + await wrapper.findByTestId('create-custom-type').trigger('click'); + }); + + it('shows custom language input form and hides dropdown items', () => { + expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false); + expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(true); + }); + + describe('on clicking back', () => { + it('hides the custom language input form and shows dropdown items', async () => { + await wrapper.findByRole('button', { name: 'Go back' }).trigger('click'); + + expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); + expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false); + }); + }); + + describe('on clicking cancel', () => { + it('hides the custom language input form and shows dropdown items', async () => { + await wrapper.findByRole('button', { name: 'Cancel' }).trigger('click'); + + expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); + expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false); + }); + }); + + describe('on dropdown hide', () => { + it('hides the form', async () => { + wrapper.findComponent(GlFormInput).setValue('foobar'); + await wrapper.findComponent(GlDropdown).vm.$emit('hide'); + + expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); + expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false); + }); + }); + + describe('on clicking apply', () => { + beforeEach(async () => { + wrapper.findComponent(GlFormInput).setValue('foobar'); + await wrapper.findComponent(GlDropdownForm).vm.$emit('submit', createFakeEvent()); + + await emitEditorEvent({ event: 'transaction', tiptapEditor }); + }); + + it('hides the custom language input form and shows dropdown items', async () => { + expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); + expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false); + }); + + it('updates dropdown value to the custom language type', () => { + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Custom (foobar)'); + }); + + it('updates tiptap editor to the custom language type', () => { + expect(tiptapEditor.getAttributes(CodeBlockHighlight.name)).toEqual( + expect.objectContaining({ + language: 'foobar', + }), + ); + }); + }); + }); + }); +}); diff --git a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js b/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js deleted file mode 100644 index 154035a46ed..00000000000 --- a/spec/frontend/content_editor/components/bubble_menus/code_block_spec.js +++ /dev/null @@ -1,296 +0,0 @@ -import { BubbleMenu } from '@tiptap/vue-2'; -import { - GlDropdown, - GlDropdownForm, - GlDropdownItem, - GlSearchBoxByType, - GlFormInput, -} from '@gitlab/ui'; -import { nextTick } from 'vue'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { stubComponent } from 'helpers/stub_component'; -import CodeBlockBubbleMenu from '~/content_editor/components/bubble_menus/code_block.vue'; -import eventHubFactory from '~/helpers/event_hub_factory'; -import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; -import Diagram from '~/content_editor/extensions/diagram'; -import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader'; -import { createTestEditor, emitEditorEvent } from '../../test_utils'; - -const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); - -describe('content_editor/components/bubble_menus/code_block', () => { - let wrapper; - let tiptapEditor; - let contentEditor; - let bubbleMenu; - let eventHub; - - const buildEditor = () => { - tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] }); - contentEditor = { renderDiagram: jest.fn() }; - eventHub = eventHubFactory(); - }; - - const buildWrapper = () => { - wrapper = mountExtended(CodeBlockBubbleMenu, { - provide: { - tiptapEditor, - contentEditor, - eventHub, - }, - stubs: { - GlDropdownItem: stubComponent(GlDropdownItem), - }, - }); - }; - - const preTag = ({ language, content = 'test' } = {}) => { - const languageAttr = language ? ` lang="${language}"` : ''; - - return `
${content}
`; - }; - - const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findDropdownItemsData = () => - findDropdownItems().wrappers.map((x) => ({ - text: x.text(), - visible: x.isVisible(), - checked: x.props('isChecked'), - })); - - beforeEach(async () => { - buildEditor(); - buildWrapper(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders bubble menu component', async () => { - tiptapEditor.commands.insertContent(preTag()); - bubbleMenu = wrapper.findComponent(BubbleMenu); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - expect(bubbleMenu.props('editor')).toBe(tiptapEditor); - expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); - }); - - it('selects plaintext language by default', async () => { - tiptapEditor.commands.insertContent(preTag()); - bubbleMenu = wrapper.findComponent(BubbleMenu); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Plain text'); - }); - - it('selects appropriate language based on the code block', async () => { - tiptapEditor.commands.insertContent(preTag({ language: 'javascript' })); - bubbleMenu = wrapper.findComponent(BubbleMenu); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Javascript'); - }); - - it('selects diagram sytnax for mermaid', async () => { - tiptapEditor.commands.insertContent(preTag({ language: 'mermaid' })); - bubbleMenu = wrapper.findComponent(BubbleMenu); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Diagram (mermaid)'); - }); - - it("selects Custom (syntax) if the language doesn't exist in the list", async () => { - tiptapEditor.commands.insertContent(preTag({ language: 'nomnoml' })); - bubbleMenu = wrapper.findComponent(BubbleMenu); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Custom (nomnoml)'); - }); - - describe('copy button', () => { - it('copies the text of the code block', async () => { - const content = 'var a = Math.PI / 2;'; - jest.spyOn(navigator.clipboard, 'writeText'); - - tiptapEditor.commands.insertContent(preTag({ language: 'javascript', content })); - - await wrapper.findByTestId('copy-code-block').vm.$emit('click'); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(content); - }); - }); - - describe('delete button', () => { - it('deletes the code block', async () => { - tiptapEditor.commands.insertContent(preTag({ language: 'javascript' })); - - await wrapper.findByTestId('delete-code-block').vm.$emit('click'); - - expect(tiptapEditor.getText()).toBe(''); - }); - }); - - describe('preview button', () => { - it('does not appear for a regular code block', async () => { - tiptapEditor.commands.insertContent('
var a = 2;
'); - - expect(wrapper.findByTestId('preview-diagram').exists()).toBe(false); - }); - - it.each` - diagramType | diagramCode - ${'mermaid'} | ${'
graph TD;\n    A-->B;
'} - ${'nomnoml'} | ${''} - `('toggles preview for a $diagramType diagram', async ({ diagramType, diagramCode }) => { - tiptapEditor.commands.insertContent(diagramCode); - - await nextTick(); - await wrapper.findByTestId('preview-diagram').vm.$emit('click'); - - expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({ - isDiagram: true, - language: diagramType, - showPreview: false, - }); - - await wrapper.findByTestId('preview-diagram').vm.$emit('click'); - - expect(tiptapEditor.getAttributes(Diagram.name)).toEqual({ - isDiagram: true, - language: diagramType, - showPreview: true, - }); - }); - }); - - describe('when opened and search is changed', () => { - beforeEach(async () => { - tiptapEditor.commands.insertContent(preTag({ language: 'javascript' })); - - wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'js'); - - await nextTick(); - }); - - it('shows dropdown items', () => { - expect(findDropdownItemsData()).toEqual( - expect.arrayContaining([ - { text: 'Javascript', visible: true, checked: true }, - { text: 'Java', visible: true, checked: false }, - { text: 'Javascript', visible: false, checked: false }, - { text: 'JSON', visible: true, checked: false }, - ]), - ); - }); - - describe('when dropdown item is clicked', () => { - beforeEach(async () => { - jest.spyOn(codeBlockLanguageLoader, 'loadLanguage').mockResolvedValue(); - - findDropdownItems().at(1).vm.$emit('click'); - - await nextTick(); - }); - - it('loads language', () => { - expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith('java'); - }); - - it('sets code block', () => { - expect(tiptapEditor.getJSON()).toMatchObject({ - content: [ - { - type: 'codeBlock', - attrs: { - language: 'java', - }, - }, - ], - }); - }); - - it('updates selected dropdown', () => { - expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Java'); - }); - }); - - describe('Create custom type', () => { - beforeEach(async () => { - tiptapEditor.commands.insertContent('
var a = 2;
'); - - await wrapper.findComponent(GlDropdown).vm.show(); - await wrapper.findByTestId('create-custom-type').trigger('click'); - }); - - it('shows custom language input form and hides dropdown items', () => { - expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false); - expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(false); - expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(true); - }); - - describe('on clicking back', () => { - it('hides the custom language input form and shows dropdown items', async () => { - await wrapper.findByRole('button', { name: 'Go back' }).trigger('click'); - - expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); - expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true); - expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false); - }); - }); - - describe('on clicking cancel', () => { - it('hides the custom language input form and shows dropdown items', async () => { - await wrapper.findByRole('button', { name: 'Cancel' }).trigger('click'); - - expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); - expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true); - expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false); - }); - }); - - describe('on dropdown hide', () => { - it('hides the form', async () => { - wrapper.findComponent(GlFormInput).setValue('foobar'); - await wrapper.findComponent(GlDropdown).vm.$emit('hide'); - - expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); - expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true); - expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false); - }); - }); - - describe('on clicking apply', () => { - beforeEach(async () => { - wrapper.findComponent(GlFormInput).setValue('foobar'); - await wrapper.findComponent(GlDropdownForm).vm.$emit('submit', createFakeEvent()); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - }); - - it('hides the custom language input form and shows dropdown items', async () => { - expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); - expect(wrapper.findComponent(GlSearchBoxByType).exists()).toBe(true); - expect(wrapper.findComponent(GlDropdownForm).exists()).toBe(false); - }); - - it('updates dropdown value to the custom language type', () => { - expect(wrapper.findComponent(GlDropdown).props('text')).toBe('Custom (foobar)'); - }); - - it('updates tiptap editor to the custom language type', () => { - expect(tiptapEditor.getAttributes(CodeBlockHighlight.name)).toEqual( - expect.objectContaining({ - language: 'foobar', - }), - ); - }); - }); - }); - }); -}); 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 new file mode 100644 index 00000000000..cce17176129 --- /dev/null +++ b/spec/frontend/content_editor/components/bubble_menus/formatting_bubble_menu_spec.js @@ -0,0 +1,90 @@ +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' }} + ${'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/formatting_spec.js b/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js deleted file mode 100644 index 1e2f58d9e40..00000000000 --- a/spec/frontend/content_editor/components/bubble_menus/formatting_spec.js +++ /dev/null @@ -1,87 +0,0 @@ -import { BubbleMenu } from '@tiptap/vue-2'; -import { mockTracking } from 'helpers/tracking_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import FormattingBubbleMenu from '~/content_editor/components/bubble_menus/formatting.vue'; - -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', () => { - let wrapper; - let trackingSpy; - let tiptapEditor; - - const buildEditor = () => { - tiptapEditor = createTestEditor(); - - jest.spyOn(tiptapEditor, 'isActive'); - }; - - const buildWrapper = () => { - wrapper = shallowMountExtended(FormattingBubbleMenu, { - provide: { - tiptapEditor, - }, - }); - }; - - beforeEach(() => { - trackingSpy = mockTracking(undefined, null, jest.spyOn); - buildEditor(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders bubble menu component', () => { - buildWrapper(); - const bubbleMenu = wrapper.findComponent(BubbleMenu); - - expect(bubbleMenu.props().editor).toBe(tiptapEditor); - 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' }} - ${'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 new file mode 100644 index 00000000000..9aa9c6483f4 --- /dev/null +++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js @@ -0,0 +1,305 @@ +import { GlLink, GlForm } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link_bubble_menu.vue'; +import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; +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'; + +const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); + +describe('content_editor/components/bubble_menus/link_bubble_menu', () => { + let wrapper; + let tiptapEditor; + let contentEditor; + let eventHub; + + const buildEditor = () => { + tiptapEditor = createTestEditor({ extensions: [Link] }); + contentEditor = { resolveUrl: jest.fn() }; + eventHub = eventHubFactory(); + }; + + const buildWrapper = () => { + wrapper = mountExtended(LinkBubbleMenu, { + provide: { + tiptapEditor, + contentEditor, + eventHub, + }, + stubs: { + BubbleMenu: stubComponent(BubbleMenu), + }, + }); + }; + + const showMenu = () => { + wrapper.findComponent(BubbleMenu).vm.$emit('show'); + return nextTick(); + }; + + const buildWrapperAndDisplayMenu = () => { + buildWrapper(); + + return showMenu(); + }; + + const findBubbleMenu = () => wrapper.findComponent(BubbleMenu); + const findLink = () => wrapper.findComponent(GlLink); + const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); + const findEditLinkButton = () => wrapper.findByTestId('edit-link'); + + const expectLinkButtonsToExist = (exist = true) => { + expect(wrapper.findComponent(GlLink).exists()).toBe(exist); + expect(wrapper.findByTestId('copy-link-url').exists()).toBe(exist); + expect(wrapper.findByTestId('edit-link').exists()).toBe(exist); + expect(wrapper.findByTestId('remove-link').exists()).toBe(exist); + }; + + beforeEach(async () => { + buildEditor(); + + tiptapEditor + .chain() + .insertContent( + 'Download PDF File', + ) + .setTextSelection(14) // put cursor in the middle of the link + .run(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders bubble menu component', async () => { + await buildWrapperAndDisplayMenu(); + + expect(findBubbleMenu().classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); + }); + + it('shows a clickable link to the URL in the link node', async () => { + await buildWrapperAndDisplayMenu(); + + expect(findLink().attributes()).toEqual( + 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('updates the bubble menu state when @selectionUpdate event is triggered', async () => { + const linkUrl = 'https://gitlab.com'; + + await buildWrapperAndDisplayMenu(); + + expect(findLink().attributes()).toEqual( + expect.objectContaining({ + href: '/path/to/project/-/wikis/uploads/my_file.pdf', + }), + ); + + tiptapEditor + .chain() + .setContent( + `Link to GitLab`, + ) + .setTextSelection(11) + .run(); + + findEditorStateObserver().vm.$emit('selectionUpdate'); + + await nextTick(); + + expect(findLink().attributes()).toEqual( + expect.objectContaining({ + href: linkUrl, + }), + ); + }); + + describe('when the selection changes within the same link', () => { + it('does not update the bubble menu state', async () => { + await buildWrapperAndDisplayMenu(); + + await findEditLinkButton().trigger('click'); + + expect(wrapper.findComponent(GlForm).exists()).toBe(true); + + tiptapEditor.commands.setTextSelection(13); + + findEditorStateObserver().vm.$emit('selectionUpdate'); + + await nextTick(); + + expect(wrapper.findComponent(GlForm).exists()).toBe(true); + }); + }); + + it('cleans bubble menu state when hidden event is triggered', async () => { + await buildWrapperAndDisplayMenu(); + + expect(findLink().attributes()).toEqual( + expect.objectContaining({ + href: '/path/to/project/-/wikis/uploads/my_file.pdf', + }), + ); + + findBubbleMenu().vm.$emit('hidden'); + + await nextTick(); + + expect(findLink().attributes()).toEqual( + expect.objectContaining({ + href: '#', + }), + ); + expect(findLink().text()).toEqual(''); + }); + + describe('copy button', () => { + it('copies the canonical link to clipboard', async () => { + await buildWrapperAndDisplayMenu(); + + jest.spyOn(navigator.clipboard, 'writeText'); + + await wrapper.findByTestId('copy-link-url').vm.$emit('click'); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith('uploads/my_file.pdf'); + }); + }); + + describe('remove link button', () => { + it('removes the link', async () => { + await buildWrapperAndDisplayMenu(); + await wrapper.findByTestId('remove-link').vm.$emit('click'); + + expect(tiptapEditor.getHTML()).toBe('

Download PDF File

'); + }); + }); + + 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('

Dummy link

'); + }); + + it('removes the link on clicking cancel', async () => { + await wrapper.findByTestId('cancel-link').vm.$emit('click'); + + expect(tiptapEditor.getHTML()).toBe('

Dummy link

'); + }); + }); + + 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 () => { + expectLinkButtonsToExist(false); + }); + + it('shows a form to edit the link', () => { + 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', () => { + const { from, to } = tiptapEditor.state.selection; + + expect(from).toBe(10); + expect(to).toBe(18); + }); + + 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( + '

Download PDF File

', + ); + }); + + 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', + }), + ); + expect(link.text()).toBe('https://google.com'); + }); + }); + + 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'); + }); + + 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/link_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_spec.js deleted file mode 100644 index 93204deb68c..00000000000 --- a/spec/frontend/content_editor/components/bubble_menus/link_spec.js +++ /dev/null @@ -1,227 +0,0 @@ -import { GlLink, GlForm } from '@gitlab/ui'; -import { BubbleMenu } from '@tiptap/vue-2'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import LinkBubbleMenu from '~/content_editor/components/bubble_menus/link.vue'; -import eventHubFactory from '~/helpers/event_hub_factory'; -import Link from '~/content_editor/extensions/link'; -import { createTestEditor, emitEditorEvent } from '../../test_utils'; - -const createFakeEvent = () => ({ preventDefault: jest.fn(), stopPropagation: jest.fn() }); - -describe('content_editor/components/bubble_menus/link', () => { - let wrapper; - let tiptapEditor; - let contentEditor; - let bubbleMenu; - let eventHub; - - const buildEditor = () => { - tiptapEditor = createTestEditor({ extensions: [Link] }); - contentEditor = { resolveUrl: jest.fn() }; - eventHub = eventHubFactory(); - }; - - const buildWrapper = () => { - wrapper = mountExtended(LinkBubbleMenu, { - provide: { - tiptapEditor, - contentEditor, - eventHub, - }, - }); - }; - - const expectLinkButtonsToExist = (exist = true) => { - expect(wrapper.findComponent(GlLink).exists()).toBe(exist); - expect(wrapper.findByTestId('copy-link-url').exists()).toBe(exist); - expect(wrapper.findByTestId('edit-link').exists()).toBe(exist); - expect(wrapper.findByTestId('remove-link').exists()).toBe(exist); - }; - - beforeEach(async () => { - buildEditor(); - buildWrapper(); - - tiptapEditor - .chain() - .insertContent( - 'Download PDF File', - ) - .setTextSelection(14) // put cursor in the middle of the link - .run(); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - bubbleMenu = wrapper.findComponent(BubbleMenu); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders bubble menu component', async () => { - expect(bubbleMenu.props('editor')).toBe(tiptapEditor); - expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); - }); - - it('shows a clickable link to the URL in the link node', async () => { - const link = wrapper.findComponent(GlLink); - expect(link.attributes()).toEqual( - 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(link.text()).toBe('uploads/my_file.pdf'); - }); - - describe('copy button', () => { - it('copies the canonical link to clipboard', async () => { - jest.spyOn(navigator.clipboard, 'writeText'); - - await wrapper.findByTestId('copy-link-url').vm.$emit('click'); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith('uploads/my_file.pdf'); - }); - }); - - describe('remove link button', () => { - it('removes the link', async () => { - await wrapper.findByTestId('remove-link').vm.$emit('click'); - - expect(tiptapEditor.getHTML()).toBe('

Download PDF File

'); - }); - }); - - describe('for a placeholder link', () => { - beforeEach(async () => { - tiptapEditor - .chain() - .clearContent() - .insertContent('Dummy link') - .selectAll() - .setLink({ href: '' }) - .setTextSelection(4) - .run(); - - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - }); - - 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('

Dummy link

'); - }); - - it('removes the link on clicking cancel', async () => { - await wrapper.findByTestId('cancel-link').vm.$emit('click'); - - expect(tiptapEditor.getHTML()).toBe('

Dummy link

'); - }); - }); - - describe('edit button', () => { - let linkHrefInput; - let linkTitleInput; - - beforeEach(async () => { - 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 () => { - expectLinkButtonsToExist(false); - }); - - it('shows a form to edit the link', () => { - 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', () => { - const { from, to } = tiptapEditor.state.selection; - - expect(from).toBe(10); - expect(to).toBe(18); - }); - - it('shows the copy/edit/remove link buttons again if selection changes to another non-link and then back again to a link', async () => { - expectLinkButtonsToExist(false); - - tiptapEditor.commands.setTextSelection(3); - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - tiptapEditor.commands.setTextSelection(14); - await emitEditorEvent({ event: 'transaction', tiptapEditor }); - - expectLinkButtonsToExist(true); - expect(wrapper.findComponent(GlForm).exists()).toBe(false); - }); - - 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( - '

Download PDF File

', - ); - }); - - 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', - }), - ); - expect(link.text()).toBe('https://google.com'); - }); - }); - - 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'); - }); - - 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 new file mode 100644 index 00000000000..13c6495ac41 --- /dev/null +++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js @@ -0,0 +1,237 @@ +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 Video from '~/content_editor/extensions/video'; +import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils'; +import { + PROJECT_WIKI_ATTACHMENT_IMAGE_HTML, + PROJECT_WIKI_ATTACHMENT_AUDIO_HTML, + PROJECT_WIKI_ATTACHMENT_VIDEO_HTML, +} from '../../test_constants'; + +const TIPTAP_IMAGE_HTML = `

+ gitlab favicon +

`; + +const TIPTAP_AUDIO_HTML = `

+ gitlab favicon +

`; + +const TIPTAP_VIDEO_HTML = `

+ gitlab favicon +

`; + +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} +`( + '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] }); + contentEditor = { resolveUrl: jest.fn() }; + eventHub = eventHubFactory(); + }; + + const buildWrapper = () => { + wrapper = mountExtended(MediaBubbleMenu, { + provide: { + tiptapEditor, + contentEditor, + eventHub, + }, + stubs: { + BubbleMenu: stubComponent(BubbleMenu), + }, + }); + }; + + 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'); + }; + + const expectLinkButtonsToExist = (exist = true) => { + expect(wrapper.findComponent(GlLink).exists()).toBe(exist); + expect(wrapper.findByTestId('copy-media-src').exists()).toBe(exist); + expect(wrapper.findByTestId('edit-media').exists()).toBe(exist); + expect(wrapper.findByTestId('delete-media').exists()).toBe(exist); + }; + + beforeEach(async () => { + buildEditor(); + buildWrapper(); + + tiptapEditor + .chain() + .insertContent(mediaHTML) + .setNodeSelection(4) // select the media + .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']); + }); + + it('shows a clickable link to the image', async () => { + const link = wrapper.findComponent(GlLink); + expect(link.attributes()).toEqual( + expect.objectContaining({ + href: `/group1/project1/-/wikis/${filePath}`, + 'aria-label': filePath, + title: filePath, + target: '_blank', + }), + ); + expect(link.text()).toBe(filePath); + }); + + describe('copy button', () => { + it(`copies the canonical link to the ${mediaType} to clipboard`, async () => { + jest.spyOn(navigator.clipboard, 'writeText'); + + await wrapper.findByTestId('copy-media-src').vm.$emit('click'); + + expect(navigator.clipboard.writeText).toHaveBeenCalledWith(filePath); + }); + }); + + describe(`remove ${mediaType} button`, () => { + it(`removes the ${mediaType}`, async () => { + await wrapper.findByTestId('delete-media').vm.$emit('click'); + + expect(tiptapEditor.getHTML()).toBe('

\n \n

'); + }); + }); + + 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(); + }); + }); + + describe('edit button', () => { + let mediaSrcInput; + let mediaTitleInput; + let mediaAltInput; + + beforeEach(async () => { + await wrapper.findByTestId('edit-media').vm.$emit('click'); + + mediaSrcInput = wrapper.findByTestId('media-src'); + mediaTitleInput = wrapper.findByTestId('media-title'); + mediaAltInput = wrapper.findByTestId('media-alt'); + }); + + it('hides the link and copy/edit/remove link buttons', async () => { + expectLinkButtonsToExist(false); + }); + + it(`shows a form to edit the ${mediaType} src/title/alt`, () => { + expect(wrapper.findComponent(GlForm).exists()).toBe(true); + + expect(mediaSrcInput.element.value).toBe(filePath); + expect(mediaTitleInput.element.value).toBe(''); + expect(mediaAltInput.element.value).toBe('test-file'); + }); + + describe('after making changes in the form and clicking apply', () => { + beforeEach(async () => { + mediaSrcInput.setValue('https://gitlab.com/favicon.png'); + mediaAltInput.setValue('gitlab favicon'); + mediaTitleInput.setValue('gitlab favicon'); + + contentEditor.resolveUrl.mockResolvedValue('https://gitlab.com/favicon.png'); + + await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent()); + }); + + it(`updates prosemirror doc with new src to the ${mediaType}`, async () => { + expect(tiptapEditor.getHTML()).toBe(mediaOutputHTML); + }); + + it(`updates the link to the ${mediaType} in the bubble menu`, () => { + const link = wrapper.findComponent(GlLink); + expect(link.attributes()).toEqual( + expect.objectContaining({ + href: 'https://gitlab.com/favicon.png', + 'aria-label': 'https://gitlab.com/favicon.png', + title: 'https://gitlab.com/favicon.png', + target: '_blank', + }), + ); + expect(link.text()).toBe('https://gitlab.com/favicon.png'); + }); + }); + + describe('after making changes in the form and clicking cancel', () => { + beforeEach(async () => { + mediaSrcInput.setValue('https://gitlab.com/favicon.png'); + mediaAltInput.setValue('gitlab favicon'); + mediaTitleInput.setValue('gitlab favicon'); + + await wrapper.findByTestId('cancel-editing-media').vm.$emit('click'); + }); + + it('hides the form and shows the copy/edit/remove link buttons', () => { + expectLinkButtonsToExist(); + }); + + it(`resets the form with old values of the ${mediaType} from prosemirror`, async () => { + // click edit once again to show the form back + await wrapper.findByTestId('edit-media').vm.$emit('click'); + + mediaSrcInput = wrapper.findByTestId('media-src'); + mediaTitleInput = wrapper.findByTestId('media-title'); + mediaAltInput = wrapper.findByTestId('media-alt'); + + expect(mediaSrcInput.element.value).toBe(filePath); + expect(mediaAltInput.element.value).toBe('test-file'); + expect(mediaTitleInput.element.value).toBe(''); + }); + }); + }); + }, +); diff --git a/spec/frontend/content_editor/components/bubble_menus/media_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_spec.js deleted file mode 100644 index fada4f06743..00000000000 --- a/spec/frontend/content_editor/components/bubble_menus/media_spec.js +++ /dev/null @@ -1,234 +0,0 @@ -import { GlLink, GlForm } from '@gitlab/ui'; -import { BubbleMenu } from '@tiptap/vue-2'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import MediaBubbleMenu from '~/content_editor/components/bubble_menus/media.vue'; -import eventHubFactory from '~/helpers/event_hub_factory'; -import Image from '~/content_editor/extensions/image'; -import Audio from '~/content_editor/extensions/audio'; -import Video from '~/content_editor/extensions/video'; -import { createTestEditor, emitEditorEvent, mockChainedCommands } from '../../test_utils'; -import { - PROJECT_WIKI_ATTACHMENT_IMAGE_HTML, - PROJECT_WIKI_ATTACHMENT_AUDIO_HTML, - PROJECT_WIKI_ATTACHMENT_VIDEO_HTML, -} from '../../test_constants'; - -const TIPTAP_IMAGE_HTML = `

- gitlab favicon -

`; - -const TIPTAP_AUDIO_HTML = `

- gitlab favicon -

`; - -const TIPTAP_VIDEO_HTML = `

- gitlab favicon -

`; - -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} -`( - 'content_editor/components/bubble_menus/media ($mediaType)', - ({ mediaType, mediaHTML, filePath, mediaOutputHTML }) => { - let wrapper; - let tiptapEditor; - let contentEditor; - let bubbleMenu; - let eventHub; - - const buildEditor = () => { - tiptapEditor = createTestEditor({ extensions: [Image, Audio, Video] }); - contentEditor = { resolveUrl: jest.fn() }; - eventHub = eventHubFactory(); - }; - - const buildWrapper = () => { - wrapper = mountExtended(MediaBubbleMenu, { - provide: { - tiptapEditor, - contentEditor, - eventHub, - }, - }); - }; - - const selectFile = async (file) => { - const input = wrapper.find({ 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'); - }; - - const expectLinkButtonsToExist = (exist = true) => { - expect(wrapper.findComponent(GlLink).exists()).toBe(exist); - expect(wrapper.findByTestId('copy-media-src').exists()).toBe(exist); - expect(wrapper.findByTestId('edit-media').exists()).toBe(exist); - expect(wrapper.findByTestId('delete-media').exists()).toBe(exist); - }; - - beforeEach(async () => { - buildEditor(); - buildWrapper(); - - tiptapEditor - .chain() - .insertContent(mediaHTML) - .setNodeSelection(4) // select the media - .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.props('editor')).toBe(tiptapEditor); - expect(bubbleMenu.classes()).toEqual(['gl-shadow', 'gl-rounded-base', 'gl-bg-white']); - }); - - it('shows a clickable link to the image', async () => { - const link = wrapper.findComponent(GlLink); - expect(link.attributes()).toEqual( - expect.objectContaining({ - href: `/group1/project1/-/wikis/${filePath}`, - 'aria-label': filePath, - title: filePath, - target: '_blank', - }), - ); - expect(link.text()).toBe(filePath); - }); - - describe('copy button', () => { - it(`copies the canonical link to the ${mediaType} to clipboard`, async () => { - jest.spyOn(navigator.clipboard, 'writeText'); - - await wrapper.findByTestId('copy-media-src').vm.$emit('click'); - - expect(navigator.clipboard.writeText).toHaveBeenCalledWith(filePath); - }); - }); - - describe(`remove ${mediaType} button`, () => { - it(`removes the ${mediaType}`, async () => { - await wrapper.findByTestId('delete-media').vm.$emit('click'); - - expect(tiptapEditor.getHTML()).toBe('

\n \n

'); - }); - }); - - 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(); - }); - }); - - describe('edit button', () => { - let mediaSrcInput; - let mediaTitleInput; - let mediaAltInput; - - beforeEach(async () => { - await wrapper.findByTestId('edit-media').vm.$emit('click'); - - mediaSrcInput = wrapper.findByTestId('media-src'); - mediaTitleInput = wrapper.findByTestId('media-title'); - mediaAltInput = wrapper.findByTestId('media-alt'); - }); - - it('hides the link and copy/edit/remove link buttons', async () => { - expectLinkButtonsToExist(false); - }); - - it(`shows a form to edit the ${mediaType} src/title/alt`, () => { - expect(wrapper.findComponent(GlForm).exists()).toBe(true); - - expect(mediaSrcInput.element.value).toBe(filePath); - expect(mediaTitleInput.element.value).toBe(''); - expect(mediaAltInput.element.value).toBe('test-file'); - }); - - describe('after making changes in the form and clicking apply', () => { - beforeEach(async () => { - mediaSrcInput.setValue('https://gitlab.com/favicon.png'); - mediaAltInput.setValue('gitlab favicon'); - mediaTitleInput.setValue('gitlab favicon'); - - contentEditor.resolveUrl.mockResolvedValue('https://gitlab.com/favicon.png'); - - await wrapper.findComponent(GlForm).vm.$emit('submit', createFakeEvent()); - }); - - it(`updates prosemirror doc with new src to the ${mediaType}`, async () => { - expect(tiptapEditor.getHTML()).toBe(mediaOutputHTML); - }); - - it(`updates the link to the ${mediaType} in the bubble menu`, () => { - const link = wrapper.findComponent(GlLink); - expect(link.attributes()).toEqual( - expect.objectContaining({ - href: 'https://gitlab.com/favicon.png', - 'aria-label': 'https://gitlab.com/favicon.png', - title: 'https://gitlab.com/favicon.png', - target: '_blank', - }), - ); - expect(link.text()).toBe('https://gitlab.com/favicon.png'); - }); - }); - - describe('after making changes in the form and clicking cancel', () => { - beforeEach(async () => { - mediaSrcInput.setValue('https://gitlab.com/favicon.png'); - mediaAltInput.setValue('gitlab favicon'); - mediaTitleInput.setValue('gitlab favicon'); - - await wrapper.findByTestId('cancel-editing-media').vm.$emit('click'); - }); - - it('hides the form and shows the copy/edit/remove link buttons', () => { - expectLinkButtonsToExist(); - }); - - it(`resets the form with old values of the ${mediaType} from prosemirror`, async () => { - // click edit once again to show the form back - await wrapper.findByTestId('edit-media').vm.$emit('click'); - - mediaSrcInput = wrapper.findByTestId('media-src'); - mediaTitleInput = wrapper.findByTestId('media-title'); - mediaAltInput = wrapper.findByTestId('media-alt'); - - expect(mediaSrcInput.element.value).toBe(filePath); - expect(mediaAltInput.element.value).toBe('test-file'); - expect(mediaTitleInput.element.value).toBe(''); - }); - }); - }); - }, -); 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 12484cb13c6..ee9ead8f8a7 100644 --- a/spec/frontend/content_editor/components/content_editor_alert_spec.js +++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js @@ -51,6 +51,16 @@ describe('content_editor/components/content_editor_alert', () => { }, ); + it('does not show primary action by default', async () => { + const message = 'error message'; + + createWrapper(); + eventHub.$emit(ALERT_EVENT, { message }); + await nextTick(); + + expect(findErrorAlert().attributes().primaryButtonText).toBeUndefined(); + }); + it('allows dismissing the error', async () => { const message = 'error message'; @@ -62,4 +72,19 @@ describe('content_editor/components/content_editor_alert', () => { expect(findErrorAlert().exists()).toBe(false); }); + + it('allows dismissing the error with a primary action button', async () => { + const message = 'error message'; + const actionLabel = 'Retry'; + const action = jest.fn(); + + createWrapper(); + eventHub.$emit(ALERT_EVENT, { message, action, actionLabel }); + await nextTick(); + findErrorAlert().vm.$emit('primaryAction'); + await nextTick(); + + expect(action).toHaveBeenCalled(); + expect(findErrorAlert().exists()).toBe(false); + }); }); diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 0ba2672100b..ae52cb05eaf 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -1,136 +1,227 @@ -import { EditorContent } from '@tiptap/vue-2'; +import { GlAlert } from '@gitlab/ui'; +import { EditorContent, Editor } from '@tiptap/vue-2'; +import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; 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.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'; import TopToolbar from '~/content_editor/components/top_toolbar.vue'; import LoadingIndicator from '~/content_editor/components/loading_indicator.vue'; -import { emitEditorEvent } from '../test_utils'; +import waitForPromises from 'helpers/wait_for_promises'; jest.mock('~/emoji'); describe('ContentEditor', () => { let wrapper; - let contentEditor; let renderMarkdown; const uploadsPath = '/uploads'; const findEditorElement = () => wrapper.findByTestId('content-editor'); const findEditorContent = () => wrapper.findComponent(EditorContent); const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); - const createWrapper = (propsData = {}) => { - renderMarkdown = jest.fn(); - + const findLoadingIndicator = () => wrapper.findComponent(LoadingIndicator); + const findContentEditorAlert = () => wrapper.findComponent(ContentEditorAlert); + const createWrapper = ({ markdown } = {}) => { wrapper = shallowMountExtended(ContentEditor, { propsData: { renderMarkdown, uploadsPath, - ...propsData, + markdown, }, stubs: { EditorStateObserver, ContentEditorProvider, - }, - listeners: { - initialized(editor) { - contentEditor = editor; - }, + ContentEditorAlert, }, }); }; + beforeEach(() => { + renderMarkdown = jest.fn(); + }); + afterEach(() => { wrapper.destroy(); }); - it('triggers initialized event and provides contentEditor instance as event data', () => { + it('triggers initialized event', () => { createWrapper(); - expect(contentEditor).not.toBeFalsy(); + expect(wrapper.emitted('initialized')).toHaveLength(1); }); - it('renders EditorContent component and provides tiptapEditor instance', () => { - createWrapper(); + it('renders EditorContent component and provides tiptapEditor instance', async () => { + const markdown = 'hello world'; + + createWrapper({ markdown }); + + renderMarkdown.mockResolvedValueOnce(markdown); + + await nextTick(); const editorContent = findEditorContent(); - expect(editorContent.props().editor).toBe(contentEditor.tiptapEditor); + expect(editorContent.props().editor).toBeInstanceOf(Editor); expect(editorContent.classes()).toContain('md'); }); - it('renders ContentEditorProvider component', () => { - createWrapper(); + it('renders ContentEditorProvider component', async () => { + await createWrapper(); expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true); }); - it('renders top toolbar component', () => { - createWrapper(); + it('renders top toolbar component', async () => { + await createWrapper(); expect(wrapper.findComponent(TopToolbar).exists()).toBe(true); }); - it('adds is-focused class when focus event is emitted', async () => { - createWrapper(); + describe('when setting initial content', () => { + it('displays loading indicator', async () => { + createWrapper(); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' }); + await nextTick(); - expect(findEditorElement().classes()).toContain('is-focused'); - }); + expect(findLoadingIndicator().exists()).toBe(true); + }); - it('removes is-focused class when blur event is emitted', async () => { - createWrapper(); + it('emits loading event', async () => { + createWrapper(); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' }); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'blur' }); + await nextTick(); - expect(findEditorElement().classes()).not.toContain('is-focused'); - }); + expect(wrapper.emitted('loading')).toHaveLength(1); + }); - it('emits change event when document is updated', async () => { - createWrapper(); + describe('succeeds', () => { + beforeEach(async () => { + renderMarkdown.mockResolvedValueOnce('hello world'); - await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'update' }); + createWrapper({ markddown: 'hello world' }); + await nextTick(); + }); - expect(wrapper.emitted('change')).toEqual([ - [ - { - empty: contentEditor.empty, - }, - ], - ]); - }); + it('hides loading indicator', async () => { + await nextTick(); + expect(findLoadingIndicator().exists()).toBe(false); + }); - it('renders content_editor_alert component', () => { - createWrapper(); + it('emits loadingSuccess event', () => { + expect(wrapper.emitted('loadingSuccess')).toHaveLength(1); + }); + }); + + describe('fails', () => { + beforeEach(async () => { + renderMarkdown.mockRejectedValueOnce(new Error()); + + createWrapper({ markddown: 'hello world' }); + await nextTick(); + }); + + it('sets the content editor as read only when loading content fails', async () => { + await nextTick(); - expect(wrapper.findComponent(ContentEditorAlert).exists()).toBe(true); + expect(findEditorContent().props().editor.isEditable).toBe(false); + }); + + it('hides loading indicator', async () => { + await nextTick(); + + expect(findLoadingIndicator().exists()).toBe(false); + }); + + it('emits loadingError event', () => { + expect(wrapper.emitted('loadingError')).toHaveLength(1); + }); + + it('displays error alert indicating that the content editor failed to load', () => { + expect(findContentEditorAlert().text()).toContain( + 'An error occurred while trying to render the content editor. Please try again.', + ); + }); + + describe('when clicking the retry button in the loading error alert and loading succeeds', () => { + beforeEach(async () => { + renderMarkdown.mockResolvedValueOnce('hello markdown'); + await wrapper.findComponent(GlAlert).vm.$emit('primaryAction'); + }); + + it('hides the loading error alert', () => { + expect(findContentEditorAlert().text()).toBe(''); + }); + + it('sets the content editor as writable', async () => { + await nextTick(); + + expect(findEditorContent().props().editor.isEditable).toBe(true); + }); + }); + }); }); - it('renders loading indicator component', () => { - createWrapper(); + describe('when focused event is emitted', () => { + beforeEach(async () => { + createWrapper(); + + findEditorStateObserver().vm.$emit('focus'); + + await nextTick(); + }); - expect(wrapper.findComponent(LoadingIndicator).exists()).toBe(true); + it('adds is-focused class when focus event is emitted', () => { + expect(findEditorElement().classes()).toContain('is-focused'); + }); + + it('removes is-focused class when blur event is emitted', async () => { + findEditorStateObserver().vm.$emit('blur'); + + await nextTick(); + + expect(findEditorElement().classes()).not.toContain('is-focused'); + }); }); - it('renders formatting bubble menu', () => { - createWrapper(); + describe('when editorStateObserver emits docUpdate event', () => { + it('emits change event with the latest markdown', async () => { + const markdown = 'Loaded content'; - expect(wrapper.findComponent(FormattingBubbleMenu).exists()).toBe(true); + renderMarkdown.mockResolvedValueOnce(markdown); + + createWrapper({ markdown: 'initial content' }); + + await nextTick(); + await waitForPromises(); + + findEditorStateObserver().vm.$emit('docUpdate'); + + expect(wrapper.emitted('change')).toEqual([ + [ + { + markdown, + changed: false, + empty: false, + }, + ], + ]); + }); }); it.each` - event - ${'loading'} - ${'loadingSuccess'} - ${'loadingError'} - `('broadcasts $event event triggered by editor-state-observer component', ({ event }) => { + name | component + ${'formatting'} | ${FormattingBubbleMenu} + ${'link'} | ${LinkBubbleMenu} + ${'media'} | ${MediaBubbleMenu} + ${'codeBlock'} | ${CodeBlockBubbleMenu} + `('renders formatting bubble menu', ({ component }) => { createWrapper(); - findEditorStateObserver().vm.$emit(event); - - expect(wrapper.emitted(event)).toHaveLength(1); + expect(wrapper.findComponent(component).exists()).toBe(true); }); }); 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 51a594a606b..e8c2d8c8793 100644 --- a/spec/frontend/content_editor/components/editor_state_observer_spec.js +++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js @@ -4,12 +4,7 @@ import EditorStateObserver, { tiptapToComponentMap, } from '~/content_editor/components/editor_state_observer.vue'; import eventHubFactory from '~/helpers/event_hub_factory'; -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, - ALERT_EVENT, -} from '~/content_editor/constants'; +import { ALERT_EVENT } from '~/content_editor/constants'; import { createTestEditor } from '../test_utils'; describe('content_editor/components/editor_state_observer', () => { @@ -18,9 +13,6 @@ describe('content_editor/components/editor_state_observer', () => { let onDocUpdateListener; let onSelectionUpdateListener; let onTransactionListener; - let onLoadingContentListener; - let onLoadingSuccessListener; - let onLoadingErrorListener; let onAlertListener; let eventHub; @@ -38,9 +30,6 @@ describe('content_editor/components/editor_state_observer', () => { selectionUpdate: onSelectionUpdateListener, transaction: onTransactionListener, [ALERT_EVENT]: onAlertListener, - [LOADING_CONTENT_EVENT]: onLoadingContentListener, - [LOADING_SUCCESS_EVENT]: onLoadingSuccessListener, - [LOADING_ERROR_EVENT]: onLoadingErrorListener, }, }); }; @@ -50,9 +39,6 @@ describe('content_editor/components/editor_state_observer', () => { onSelectionUpdateListener = jest.fn(); onTransactionListener = jest.fn(); onAlertListener = jest.fn(); - onLoadingSuccessListener = jest.fn(); - onLoadingContentListener = jest.fn(); - onLoadingErrorListener = jest.fn(); buildEditor(); }); @@ -81,11 +67,8 @@ describe('content_editor/components/editor_state_observer', () => { }); it.each` - event | listener - ${ALERT_EVENT} | ${() => onAlertListener} - ${LOADING_CONTENT_EVENT} | ${() => onLoadingContentListener} - ${LOADING_SUCCESS_EVENT} | ${() => onLoadingSuccessListener} - ${LOADING_ERROR_EVENT} | ${() => onLoadingErrorListener} + event | listener + ${ALERT_EVENT} | ${() => onAlertListener} `('listens to $event event in the eventBus object', ({ event, listener }) => { const args = {}; @@ -114,9 +97,6 @@ describe('content_editor/components/editor_state_observer', () => { it.each` event ${ALERT_EVENT} - ${LOADING_CONTENT_EVENT} - ${LOADING_SUCCESS_EVENT} - ${LOADING_ERROR_EVENT} `('removes $event event hook from eventHub', ({ event }) => { jest.spyOn(eventHub, '$off'); jest.spyOn(eventHub, '$on'); diff --git a/spec/frontend/content_editor/components/loading_indicator_spec.js b/spec/frontend/content_editor/components/loading_indicator_spec.js index e4fb09b70a4..0065103d01b 100644 --- a/spec/frontend/content_editor/components/loading_indicator_spec.js +++ b/spec/frontend/content_editor/components/loading_indicator_spec.js @@ -1,18 +1,10 @@ import { GlLoadingIcon } from '@gitlab/ui'; -import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import LoadingIndicator from '~/content_editor/components/loading_indicator.vue'; -import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, -} from '~/content_editor/constants'; describe('content_editor/components/loading_indicator', () => { let wrapper; - const findEditorStateObserver = () => wrapper.findComponent(EditorStateObserver); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const createWrapper = () => { @@ -24,48 +16,12 @@ describe('content_editor/components/loading_indicator', () => { }); describe('when loading content', () => { - beforeEach(async () => { + beforeEach(() => { createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - - await nextTick(); }); it('displays loading indicator', () => { expect(findLoadingIcon().exists()).toBe(true); }); }); - - describe('when loading content succeeds', () => { - beforeEach(async () => { - createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - await nextTick(); - findEditorStateObserver().vm.$emit(LOADING_SUCCESS_EVENT); - await nextTick(); - }); - - it('hides loading indicator', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - }); - - describe('when loading content fails', () => { - const error = 'error'; - - beforeEach(async () => { - createWrapper(); - - findEditorStateObserver().vm.$emit(LOADING_CONTENT_EVENT); - await nextTick(); - findEditorStateObserver().vm.$emit(LOADING_ERROR_EVENT, error); - await nextTick(); - }); - - it('hides loading indicator', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - }); }); diff --git a/spec/frontend/content_editor/components/toolbar_image_button_spec.js b/spec/frontend/content_editor/components/toolbar_image_button_spec.js index dab7e67d7c5..5473d43f5a1 100644 --- a/spec/frontend/content_editor/components/toolbar_image_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_image_button_spec.js @@ -1,8 +1,9 @@ -import { GlButton, GlFormInputGroup } from '@gitlab/ui'; +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', () => { @@ -14,15 +15,19 @@ describe('content_editor/components/toolbar_image_button', () => { 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.find({ ref: 'fileSelector' }); + 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 }); @@ -77,4 +82,16 @@ describe('content_editor/components/toolbar_image_button', () => { 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 index fc26a9da471..40e859e96af 100644 --- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js @@ -4,6 +4,7 @@ import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.v 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'); @@ -18,6 +19,9 @@ describe('content_editor/components/toolbar_link_button', () => { tiptapEditor: editor, eventHub: eventHubFactory(), }, + stubs: { + GlDropdown: stubComponent(GlDropdown), + }, }); }; const findDropdown = () => wrapper.findComponent(GlDropdown); @@ -26,7 +30,7 @@ describe('content_editor/components/toolbar_link_button', () => { const findRemoveLinkButton = () => wrapper.findByText('Remove link'); const selectFile = async (file) => { - const input = wrapper.find({ ref: 'fileSelector' }); + 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 }); @@ -205,4 +209,16 @@ describe('content_editor/components/toolbar_link_button', () => { }); }); }); + + 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 62fec8d4e72..a23f8370adf 100644 --- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js +++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js @@ -1,8 +1,10 @@ +import { GlDropdown } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import ToolbarMoreDropdown from '~/content_editor/components/toolbar_more_dropdown.vue'; import Diagram from '~/content_editor/extensions/diagram'; import HorizontalRule from '~/content_editor/extensions/horizontal_rule'; import eventHubFactory from '~/helpers/event_hub_factory'; +import { stubComponent } from 'helpers/stub_component'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils'; describe('content_editor/components/toolbar_more_dropdown', () => { @@ -23,10 +25,15 @@ describe('content_editor/components/toolbar_more_dropdown', () => { tiptapEditor, eventHub, }, + stubs: { + GlDropdown: stubComponent(GlDropdown), + }, propsData, }); }; + const findDropdown = () => wrapper.findComponent(GlDropdown); + beforeEach(() => { buildEditor(); buildWrapper(); @@ -67,4 +74,14 @@ describe('content_editor/components/toolbar_more_dropdown', () => { expect(wrapper.emitted('execute')).toEqual([[{ contentType }]]); }); }); + + describe('a11y tests', () => { + it('sets text, title, and text-sr-only properties to the table button dropdown', () => { + expect(findDropdown().props()).toMatchObject({ + text: 'More', + textSrOnly: true, + }); + expect(findDropdown().attributes('title')).toBe('More'); + }); + }); }); 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 056e5e04e1f..aa4604661e5 100644 --- a/spec/frontend/content_editor/components/toolbar_table_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_table_button_spec.js @@ -1,6 +1,7 @@ import { GlDropdown, GlButton } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import ToolbarTableButton from '~/content_editor/components/toolbar_table_button.vue'; +import { stubComponent } from 'helpers/stub_component'; import { createTestEditor, mockChainedCommands } from '../test_utils'; describe('content_editor/components/toolbar_table_button', () => { @@ -12,6 +13,9 @@ describe('content_editor/components/toolbar_table_button', () => { provide: { tiptapEditor: editor, }, + stubs: { + GlDropdown: stubComponent(GlDropdown), + }, }); }; @@ -98,4 +102,14 @@ describe('content_editor/components/toolbar_table_button', () => { expect(getNumButtons()).toBe(100); // 10x10 (and not 11x11) }); + + describe('a11y tests', () => { + it('sets text, title, and text-sr-only properties to the table button dropdown', () => { + expect(findDropdown().props()).toMatchObject({ + text: 'Insert table', + textSrOnly: true, + }); + expect(findDropdown().attributes('title')).toBe('Insert 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 608be1bd693..3ebb305afbf 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 @@ -53,7 +53,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => { }); }); - describe('when there is an active item ', () => { + describe('when there is an active item', () => { let activeTextStyle; beforeEach(async () => { @@ -68,7 +68,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => { await emitEditorEvent({ event: 'transaction', tiptapEditor }); }); - it('displays the active text style label as the dropdown toggle text ', () => { + it('displays the active text style label as the dropdown toggle text', () => { expect(findDropdown().props().text).toBe(activeTextStyle.label); }); 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 17a365e12bb..a5ef19fb8e8 100644 --- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js +++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js @@ -104,7 +104,7 @@ describe('content/components/wrappers/code_block', () => { it('does not render a preview if showPreview: false', async () => { createWrapper({ language: 'plantuml', isDiagram: true, showPreview: false }); - expect(wrapper.find({ ref: 'diagramContainer' }).exists()).toBe(false); + expect(wrapper.findComponent({ ref: 'diagramContainer' }).exists()).toBe(false); }); it('does not update preview when diagram is not active', async () => { @@ -134,7 +134,7 @@ describe('content/components/wrappers/code_block', () => { await nextTick(); expect(wrapper.find('img').attributes('src')).toBe('url/to/some/diagram'); - expect(wrapper.find(SandboxedMermaid).exists()).toBe(false); + expect(wrapper.findComponent(SandboxedMermaid).exists()).toBe(false); }); it('renders an iframe with preview for a mermaid diagram', async () => { @@ -143,7 +143,7 @@ describe('content/components/wrappers/code_block', () => { await emitEditorEvent({ event: 'transaction', tiptapEditor }); await nextTick(); - expect(wrapper.find(SandboxedMermaid).props('source')).toBe(''); + expect(wrapper.findComponent(SandboxedMermaid).props('source')).toBe(''); expect(wrapper.find('img').exists()).toBe(false); }); }); diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/paste_markdown_spec.js index 53efda6aee2..30e798e8817 100644 --- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js +++ b/spec/frontend/content_editor/extensions/paste_markdown_spec.js @@ -5,12 +5,7 @@ import Frontmatter from '~/content_editor/extensions/frontmatter'; import Bold from '~/content_editor/extensions/bold'; import { VARIANT_DANGER } from '~/flash'; import eventHubFactory from '~/helpers/event_hub_factory'; -import { - ALERT_EVENT, - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, -} from '~/content_editor/constants'; +import { ALERT_EVENT } from '~/content_editor/constants'; import waitForPromises from 'helpers/wait_for_promises'; import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils'; @@ -115,13 +110,6 @@ describe('content_editor/extensions/paste_markdown', () => { expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON()); }); - - it(`triggers ${LOADING_SUCCESS_EVENT}`, async () => { - await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent()); - - expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_CONTENT_EVENT); - expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_SUCCESS_EVENT); - }); }); describe('when rendering markdown fails', () => { @@ -129,13 +117,6 @@ describe('content_editor/extensions/paste_markdown', () => { renderMarkdown.mockRejectedValueOnce(); }); - it(`triggers ${LOADING_ERROR_EVENT} event`, async () => { - await triggerPasteEventHandler(buildClipboardEvent()); - await waitForPromises(); - - expect(eventHub.$emit).toHaveBeenCalledWith(LOADING_ERROR_EVENT); - }); - it(`triggers ${ALERT_EVENT} event`, async () => { await triggerPasteEventHandler(buildClipboardEvent()); await waitForPromises(); diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js index 7ae0a7c13c1..bc43af9bd8b 100644 --- a/spec/frontend/content_editor/remark_markdown_processing_spec.js +++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js @@ -1,8 +1,10 @@ +import Audio from '~/content_editor/extensions/audio'; import Bold from '~/content_editor/extensions/bold'; import Blockquote from '~/content_editor/extensions/blockquote'; import BulletList from '~/content_editor/extensions/bullet_list'; import Code from '~/content_editor/extensions/code'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; +import Diagram from '~/content_editor/extensions/diagram'; import FootnoteDefinition from '~/content_editor/extensions/footnote_definition'; import FootnoteReference from '~/content_editor/extensions/footnote_reference'; import Frontmatter from '~/content_editor/extensions/frontmatter'; @@ -21,22 +23,27 @@ import Sourcemap from '~/content_editor/extensions/sourcemap'; import Strike from '~/content_editor/extensions/strike'; import Table from '~/content_editor/extensions/table'; import TableHeader from '~/content_editor/extensions/table_header'; +import TableOfContents from '~/content_editor/extensions/table_of_contents'; import TableRow from '~/content_editor/extensions/table_row'; import TableCell from '~/content_editor/extensions/table_cell'; import TaskList from '~/content_editor/extensions/task_list'; import TaskItem from '~/content_editor/extensions/task_item'; +import Video from '~/content_editor/extensions/video'; import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import markdownSerializer from '~/content_editor/services/markdown_serializer'; +import { SAFE_VIDEO_EXT, SAFE_AUDIO_EXT, DIAGRAM_LANGUAGES } from '~/content_editor/constants'; import { createTestEditor, createDocBuilder } from './test_utils'; const tiptapEditor = createTestEditor({ extensions: [ + Audio, Blockquote, Bold, BulletList, Code, CodeBlockHighlight, + Diagram, FootnoteDefinition, FootnoteReference, Frontmatter, @@ -55,8 +62,10 @@ const tiptapEditor = createTestEditor({ TableRow, TableHeader, TableCell, + TableOfContents, TaskList, TaskItem, + Video, ...HTMLNodes, ], }); @@ -65,12 +74,14 @@ const { builders: { doc, paragraph, + audio, bold, blockquote, bulletList, code, codeBlock, div, + diagram, footnoteDefinition, footnoteReference, frontmatter, @@ -89,17 +100,21 @@ const { tableRow, tableHeader, tableCell, + tableOfContents, taskItem, taskList, + video, }, } = createDocBuilder({ tiptapEditor, names: { + audio: { nodeType: Audio.name }, blockquote: { nodeType: Blockquote.name }, bold: { markType: Bold.name }, bulletList: { nodeType: BulletList.name }, code: { markType: Code.name }, codeBlock: { nodeType: CodeBlockHighlight.name }, + diagram: { nodeType: Diagram.name }, footnoteDefinition: { nodeType: FootnoteDefinition.name }, footnoteReference: { nodeType: FootnoteReference.name }, frontmatter: { nodeType: Frontmatter.name }, @@ -118,8 +133,10 @@ const { tableCell: { nodeType: TableCell.name }, tableHeader: { nodeType: TableHeader.name }, tableRow: { nodeType: TableRow.name }, + tableOfContents: { nodeType: TableOfContents.name }, taskItem: { nodeType: TaskItem.name }, taskList: { nodeType: TaskList.name }, + video: { nodeType: Video.name }, ...HTMLNodes.reduce( (builders, htmlNode) => ({ ...builders, @@ -1233,6 +1250,62 @@ title: 'layout' ), ), }, + ...SAFE_AUDIO_EXT.map((extension) => { + const src = `http://test.host/video.${extension}`; + const markdown = `![audio](${src})`; + + return { + markdown, + expectedDoc: doc( + paragraph( + source(markdown), + audio({ + ...source(markdown), + canonicalSrc: src, + src, + alt: 'audio', + }), + ), + ), + }; + }), + ...SAFE_VIDEO_EXT.map((extension) => { + const src = `http://test.host/video.${extension}`; + const markdown = `![video](${src})`; + + return { + markdown, + expectedDoc: doc( + paragraph( + source(markdown), + video({ + ...source(markdown), + canonicalSrc: src, + src, + alt: 'video', + }), + ), + ), + }; + }), + ...DIAGRAM_LANGUAGES.map((language) => { + const markdown = `\`\`\`${language} +content +\`\`\``; + + return { + markdown, + expectedDoc: doc(diagram({ ...source(markdown), language }, 'content')), + }; + }), + { + markdown: '[[_TOC_]]', + expectedDoc: doc(tableOfContents(source('[[_TOC_]]'))), + }, + { + markdown: '[TOC]', + expectedDoc: doc(tableOfContents(source('[TOC]'))), + }, ]; const runOnly = examples.find((example) => example.only === true); 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 4a57c7b1942..bd48b7fdd23 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,6 +1,7 @@ import { DOMSerializer } from 'prosemirror-model'; // TODO: DRY up duplication with spec/frontend/content_editor/services/markdown_serializer_spec.js // See https://gitlab.com/groups/gitlab-org/-/epics/7719#plan +import Audio from '~/content_editor/extensions/audio'; import Blockquote from '~/content_editor/extensions/blockquote'; import Bold from '~/content_editor/extensions/bold'; import BulletList from '~/content_editor/extensions/bullet_list'; @@ -33,13 +34,16 @@ import Table from '~/content_editor/extensions/table'; import TableCell from '~/content_editor/extensions/table_cell'; import TableHeader from '~/content_editor/extensions/table_header'; import TableRow from '~/content_editor/extensions/table_row'; +import TableOfContents from '~/content_editor/extensions/table_of_contents'; import TaskItem from '~/content_editor/extensions/task_item'; import TaskList from '~/content_editor/extensions/task_list'; +import Video from '~/content_editor/extensions/video'; import createMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import { createTestEditor } from 'jest/content_editor/test_utils'; const tiptapEditor = createTestEditor({ extensions: [ + Audio, Blockquote, Bold, BulletList, @@ -72,8 +76,10 @@ const tiptapEditor = createTestEditor({ TableCell, TableHeader, TableRow, + TableOfContents, TaskItem, TaskList, + Video, ], }); diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js index a3553e612ca..6175cbdd3d4 100644 --- a/spec/frontend/content_editor/services/content_editor_spec.js +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -1,8 +1,3 @@ -import { - LOADING_CONTENT_EVENT, - LOADING_SUCCESS_EVENT, - LOADING_ERROR_EVENT, -} from '~/content_editor/constants'; import { ContentEditor } from '~/content_editor/services/content_editor'; import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor, createDocBuilder } from '../test_utils'; @@ -14,6 +9,7 @@ describe('content_editor/services/content_editor', () => { let eventHub; let doc; let p; + const testMarkdown = '**bold text**'; beforeEach(() => { const tiptapEditor = createTestEditor(); @@ -36,6 +32,9 @@ describe('content_editor/services/content_editor', () => { }); }); + const testDoc = () => doc(p('document')); + const testEmptyDoc = () => doc(); + describe('.dispose', () => { it('destroys the tiptapEditor', () => { expect(contentEditor.tiptapEditor.destroy).not.toHaveBeenCalled(); @@ -46,51 +45,77 @@ describe('content_editor/services/content_editor', () => { }); }); - describe('when setSerializedContent succeeds', () => { - let document; - const languages = ['javascript']; - const testMarkdown = '**bold text**'; + describe('empty', () => { + it('returns true when tiptapEditor is empty', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testEmptyDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); - beforeEach(() => { - document = doc(p('document')); - deserializer.deserialize.mockResolvedValueOnce({ document, languages }); + expect(contentEditor.empty).toBe(true); }); - it('emits loadingContent and loadingSuccess event in the eventHub', () => { - let loadingContentEmitted = false; + it('returns false when tiptapEditor is not empty', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); - eventHub.$on(LOADING_CONTENT_EVENT, () => { - loadingContentEmitted = true; - }); - eventHub.$on(LOADING_SUCCESS_EVENT, () => { - expect(loadingContentEmitted).toBe(true); - }); + await contentEditor.setSerializedContent(testMarkdown); - contentEditor.setSerializedContent(testMarkdown); + expect(contentEditor.empty).toBe(false); }); + }); - it('sets the deserialized document in the tiptap editor object', async () => { - await contentEditor.setSerializedContent(testMarkdown); + describe('editable', () => { + it('returns true when tiptapEditor is editable', async () => { + contentEditor.setEditable(true); - expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON()); + expect(contentEditor.editable).toBe(true); + }); + + it('returns false when tiptapEditor is readonly', async () => { + contentEditor.setEditable(false); + + expect(contentEditor.editable).toBe(false); }); }); - describe('when setSerializedContent fails', () => { - const error = 'error'; + describe('changed', () => { + it('returns true when the initial document changes', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); + + contentEditor.tiptapEditor.commands.insertContent(' new content'); + + expect(contentEditor.changed).toBe(true); + }); + + it('returns false when the initial document hasn’t changed', async () => { + deserializer.deserialize.mockResolvedValueOnce({ document: testDoc() }); + + await contentEditor.setSerializedContent(testMarkdown); + + expect(contentEditor.changed).toBe(false); + }); + + it('returns false when an initial document is not set and the document is empty', () => { + expect(contentEditor.changed).toBe(false); + }); - beforeEach(() => { - deserializer.deserialize.mockRejectedValueOnce(error); + it('returns true when an initial document is not set and the document is not empty', () => { + contentEditor.tiptapEditor.commands.insertContent('new content'); + + expect(contentEditor.changed).toBe(true); }); + }); + + describe('when setSerializedContent succeeds', () => { + it('sets the deserialized document in the tiptap editor object', async () => { + const document = testDoc(); + + deserializer.deserialize.mockResolvedValueOnce({ document }); - it('emits loadingError event', async () => { - eventHub.$on(LOADING_ERROR_EVENT, (e) => { - expect(e).toBe('error'); - }); + await contentEditor.setSerializedContent(testMarkdown); - await expect(() => contentEditor.setSerializedContent('**bold text**')).rejects.toEqual( - error, - ); + expect(contentEditor.tiptapEditor.state.doc.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 0e5281be9bf..56394c85e8b 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -1,3 +1,4 @@ +import Audio from '~/content_editor/extensions/audio'; import Blockquote from '~/content_editor/extensions/blockquote'; import Bold from '~/content_editor/extensions/bold'; import BulletList from '~/content_editor/extensions/bullet_list'; @@ -33,6 +34,7 @@ import TableHeader from '~/content_editor/extensions/table_header'; import TableRow from '~/content_editor/extensions/table_row'; import TaskItem from '~/content_editor/extensions/task_item'; import TaskList from '~/content_editor/extensions/task_list'; +import Video from '~/content_editor/extensions/video'; import markdownSerializer from '~/content_editor/services/markdown_serializer'; import remarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer'; import { createTestEditor, createDocBuilder } from '../test_utils'; @@ -41,6 +43,7 @@ jest.mock('~/emoji'); const tiptapEditor = createTestEditor({ extensions: [ + Audio, Blockquote, Bold, BulletList, @@ -73,6 +76,7 @@ const tiptapEditor = createTestEditor({ TableRow, TaskItem, TaskList, + Video, ...HTMLMarks, ...HTMLNodes, ], @@ -80,6 +84,7 @@ const tiptapEditor = createTestEditor({ const { builders: { + audio, doc, blockquote, bold, @@ -114,6 +119,7 @@ const { tableRow, taskItem, taskList, + video, }, } = createDocBuilder({ tiptapEditor, @@ -1230,6 +1236,21 @@ paragraph ); }); + it('serializes audio and video elements', () => { + expect( + serialize( + paragraph( + audio({ alt: 'audio', canonicalSrc: 'audio.mp3' }), + ' and ', + video({ alt: 'video', canonicalSrc: 'video.mov' }), + ), + ), + ).toBe( + ` +![audio](audio.mp3) and ![video](video.mov)`.trimLeft(), + ); + }); + const defaultEditAction = (initialContent) => { tiptapEditor.chain().setContent(initialContent.toJSON()).insertContent(' modified').run(); }; diff --git a/spec/frontend/crm/form_spec.js b/spec/frontend/crm/form_spec.js index f0e9150cada..57e28b396cf 100644 --- a/spec/frontend/crm/form_spec.js +++ b/spec/frontend/crm/form_spec.js @@ -298,7 +298,7 @@ describe('Reusable form component', () => { `( 'should render the correct component for #$id with the value "$value"', ({ index, id, component, value }) => { - const findFormElement = () => findFormGroup(index).find(component); + const findFormElement = () => findFormGroup(index).findComponent(component); expect(findFormElement().attributes('id')).toBe(id); expect(findFormElement().attributes('value')).toBe(value); @@ -307,7 +307,8 @@ describe('Reusable form component', () => { it('should render a checked GlFormCheckbox for #active', () => { const activeCheckboxIndex = 6; - const findFormElement = () => findFormGroup(activeCheckboxIndex).find(GlFormCheckbox); + const findFormElement = () => + findFormGroup(activeCheckboxIndex).findComponent(GlFormCheckbox); expect(findFormElement().attributes('id')).toBe('active'); expect(findFormElement().attributes('checked')).toBe('true'); diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js index a2e2e88ac60..a19ee01c2a5 100644 --- a/spec/frontend/crm/mock_data.js +++ b/spec/frontend/crm/mock_data.js @@ -102,6 +102,13 @@ export const getGroupOrganizationsQueryResponse = { active: true, }, ], + pageInfo: { + __typename: 'PageInfo', + hasNextPage: false, + endCursor: 'eyJsYXN0X25hbWUiOiJMZWRuZXIiLCJpZCI6IjE3OSJ9', + hasPreviousPage: false, + startCursor: 'eyJsYXN0X25hbWUiOiJCYXJ0b24iLCJpZCI6IjE5MyJ9', + }, }, }, }, @@ -155,6 +162,21 @@ export const updateContactMutationResponse = { }, }; +export const getGroupOrganizationsCountQueryResponse = { + data: { + group: { + __typename: 'Group', + id: 'gid://gitlab/Group/26', + organizationStateCounts: { + all: 24, + active: 21, + inactive: 3, + __typename: 'OrganizationStateCountsType', + }, + }, + }, +}; + export const updateContactMutationErrorResponse = { data: { customerRelationsContactUpdate: { diff --git a/spec/frontend/crm/organizations_root_spec.js b/spec/frontend/crm/organizations_root_spec.js index 1780a5945a6..a0b56596177 100644 --- a/spec/frontend/crm/organizations_root_spec.js +++ b/spec/frontend/crm/organizations_root_spec.js @@ -1,14 +1,19 @@ -import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; -import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import OrganizationsRoot from '~/crm/organizations/components/organizations_root.vue'; import routes from '~/crm/organizations/routes'; import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql'; -import { getGroupOrganizationsQueryResponse } from './mock_data'; +import getGroupOrganizationsCountByStateQuery from '~/crm/organizations/components/graphql/get_group_organizations_count_by_state.query.graphql'; +import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue'; +import { + getGroupOrganizationsQueryResponse, + getGroupOrganizationsCountQueryResponse, +} from './mock_data'; describe('Customer relations organizations root app', () => { Vue.use(VueApollo); @@ -21,23 +26,31 @@ describe('Customer relations organizations root app', () => { const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName }); const findIssuesLinks = () => wrapper.findAllByTestId('issues-link'); const findNewOrganizationButton = () => wrapper.findByTestId('new-organization-button'); - const findError = () => wrapper.findComponent(GlAlert); + const findTable = () => wrapper.findComponent(PaginatedTableWithSearchAndTabs); const successQueryHandler = jest.fn().mockResolvedValue(getGroupOrganizationsQueryResponse); + const successCountQueryHandler = jest + .fn() + .mockResolvedValue(getGroupOrganizationsCountQueryResponse); const basePath = '/groups/flightjs/-/crm/organizations'; const mountComponent = ({ queryHandler = successQueryHandler, - mountFunction = shallowMountExtended, + countQueryHandler = successCountQueryHandler, canAdminCrmOrganization = true, + textQuery = null, } = {}) => { - fakeApollo = createMockApollo([[getGroupOrganizationsQuery, queryHandler]]); - wrapper = mountFunction(OrganizationsRoot, { + fakeApollo = createMockApollo([ + [getGroupOrganizationsQuery, queryHandler], + [getGroupOrganizationsCountByStateQuery, countQueryHandler], + ]); + wrapper = mountExtended(OrganizationsRoot, { router, provide: { canAdminCrmOrganization, groupFullPath: 'flightjs', groupIssuesPath: '/issues', + textQuery, }, apolloProvider: fakeApollo, }); @@ -57,9 +70,33 @@ describe('Customer relations organizations root app', () => { router = null; }); - it('should render loading spinner', () => { + it('should render table with default props and loading spinner', () => { mountComponent(); + expect(findTable().props()).toMatchObject({ + items: [], + itemsCount: {}, + pageInfo: {}, + statusTabs: [ + { title: 'Active', status: 'ACTIVE', filters: 'active' }, + { title: 'Inactive', status: 'INACTIVE', filters: 'inactive' }, + { title: 'All', status: 'ALL', filters: 'all' }, + ], + showItems: true, + showErrorMsg: false, + trackViewsOptions: { category: 'Customer Relations', action: 'view_organizations_list' }, + i18n: { + emptyText: 'No organizations found', + issuesButtonLabel: 'View issues', + editButtonLabel: 'Edit', + title: 'Customer relations organizations', + newOrganization: 'New organization', + errorText: 'Something went wrong. Please try again.', + }, + serverErrorMessage: '', + filterSearchKey: 'organizations', + filterSearchTokens: [], + }); expect(findLoadingIcon().exists()).toBe(true); }); @@ -77,11 +114,25 @@ describe('Customer relations organizations root app', () => { }); }); - it('should render error message on reject', async () => { - mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') }); - await waitForPromises(); + describe('error', () => { + it('should render on reject', async () => { + mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') }); + await waitForPromises(); + + expect(wrapper.text()).toContain('Something went wrong. Please try again.'); + }); + + it('should be removed on error-alert-dismissed event', async () => { + mountComponent({ queryHandler: jest.fn().mockRejectedValue('ERROR') }); + await waitForPromises(); - expect(findError().exists()).toBe(true); + expect(wrapper.text()).toContain('Something went wrong. Please try again.'); + + findTable().vm.$emit('error-alert-dismissed'); + await waitForPromises(); + + expect(wrapper.text()).not.toContain('Something went wrong. Please try again.'); + }); }); describe('on successful load', () => { @@ -89,20 +140,27 @@ describe('Customer relations organizations root app', () => { mountComponent(); await waitForPromises(); - expect(findError().exists()).toBe(false); + expect(wrapper.text()).not.toContain('Something went wrong. Please try again.'); }); it('renders correct results', async () => { - mountComponent({ mountFunction: mountExtended }); + mountComponent(); await waitForPromises(); expect(findRowByName(/Test Inc/i)).toHaveLength(1); expect(findRowByName(/VIP/i)).toHaveLength(1); expect(findRowByName(/120/i)).toHaveLength(1); - const issueLink = findIssuesLinks().at(0); - expect(issueLink.exists()).toBe(true); - expect(issueLink.attributes('href')).toBe('/issues?crm_organization_id=2'); + expect(findIssuesLinks()).toHaveLength(3); + + const links = findIssuesLinks().wrappers.map((w) => w.attributes('href')); + expect(links).toEqual( + expect.arrayContaining([ + '/issues?crm_organization_id=1', + '/issues?crm_organization_id=2', + '/issues?crm_organization_id=3', + ]), + ); }); }); }); diff --git a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap b/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap index 7f211c1028e..92927ef16ec 100644 --- a/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap +++ b/spec/frontend/cycle_analytics/__snapshots__/total_time_spec.js.snap @@ -1,28 +1,28 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TotalTime with a blank object should render -- 1`] = `" -- "`; +exports[`TotalTime with a blank object should render -- 1`] = `" -- "`; exports[`TotalTime with a valid time object with {"days": 3, "mins": 47, "seconds": 3} 1`] = ` -" +" 3 days" `; exports[`TotalTime with a valid time object with {"hours": 7, "mins": 20, "seconds": 10} 1`] = ` -" +" 7 hrs" `; exports[`TotalTime with a valid time object with {"hours": 23, "mins": 10} 1`] = ` -" +" 23 hrs" `; exports[`TotalTime with a valid time object with {"mins": 47, "seconds": 3} 1`] = ` -" +" 47 mins" `; exports[`TotalTime with a valid time object with {"seconds": 35} 1`] = ` -" +" 35 s" `; diff --git a/spec/frontend/cycle_analytics/base_spec.js b/spec/frontend/cycle_analytics/base_spec.js index ea3da86c7b2..013bea671a8 100644 --- a/spec/frontend/cycle_analytics/base_spec.js +++ b/spec/frontend/cycle_analytics/base_spec.js @@ -201,7 +201,7 @@ describe('Value stream analytics component', () => { it('renders the stage table with a loading icon', () => { const tableWrapper = findStageTable(); expect(tableWrapper.exists()).toBe(true); - expect(tableWrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(tableWrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders the path navigation loading state', () => { diff --git a/spec/frontend/cycle_analytics/path_navigation_spec.js b/spec/frontend/cycle_analytics/path_navigation_spec.js index fa9eadbd071..fec1526359c 100644 --- a/spec/frontend/cycle_analytics/path_navigation_spec.js +++ b/spec/frontend/cycle_analytics/path_navigation_spec.js @@ -56,7 +56,9 @@ describe('Project PathNavigation', () => { describe('displays correctly', () => { it('has the correct props', () => { - expect(wrapper.find(GlPath).props('items')).toMatchObject(transformedProjectStagePathData); + expect(wrapper.findComponent(GlPath).props('items')).toMatchObject( + transformedProjectStagePathData, + ); }); it('contains all the expected stages', () => { @@ -69,11 +71,11 @@ describe('Project PathNavigation', () => { describe('loading', () => { describe('is false', () => { it('displays the gl-path component', () => { - expect(wrapper.find(GlPath).exists()).toBe(true); + expect(wrapper.findComponent(GlPath).exists()).toBe(true); }); it('hides the gl-skeleton-loading component', () => { - expect(wrapper.find(GlSkeletonLoader).exists()).toBe(false); + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false); }); it('renders each stage', () => { @@ -112,11 +114,11 @@ describe('Project PathNavigation', () => { }); it('hides the gl-path component', () => { - expect(wrapper.find(GlPath).exists()).toBe(false); + expect(wrapper.findComponent(GlPath).exists()).toBe(false); }); it('displays the gl-skeleton-loading component', () => { - expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true); + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js index 23e41f35b00..9c8cd6a3dbc 100644 --- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js +++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js @@ -176,7 +176,7 @@ describe('ValueStreamMetrics', () => { await waitForPromises(); }); - it('it should render an error message', () => { + it('should render an error message', () => { expect(createFlash).toHaveBeenCalledWith({ message: `There was an error while fetching value stream analytics ${fakeReqName} data.`, }); 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 7c46c280d46..bbafdc000db 100644 --- a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js @@ -42,7 +42,7 @@ describe('Deploy freeze modal', () => { wrapper.find('#deploy-freeze-start').trigger('input'); wrapper.find('#deploy-freeze-end').trigger('input'); - wrapper.find(TimezoneDropdown).trigger('input'); + wrapper.findComponent(TimezoneDropdown).trigger('input'); }; afterEach(() => { 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 cc044800e5e..637efe30022 100644 --- a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js +++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js @@ -31,11 +31,11 @@ describe('Deploy freeze settings', () => { describe('Deploy freeze table contains components', () => { it('contains deploy freeze table', () => { - expect(wrapper.find(DeployFreezeTable).exists()).toBe(true); + expect(wrapper.findComponent(DeployFreezeTable).exists()).toBe(true); }); it('contains deploy freeze modal', () => { - expect(wrapper.find(DeployFreezeModal).exists()).toBe(true); + expect(wrapper.findComponent(DeployFreezeModal).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js index aea81daecef..567d18f8b92 100644 --- a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js +++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js @@ -30,8 +30,8 @@ describe('Deploy freeze timezone dropdown', () => { wrapper.setData({ searchTerm }); }; - const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem); - const findDropdownItemByIndex = (index) => wrapper.findAll(GlDropdownItem).at(index); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); afterEach(() => { wrapper.destroy(); @@ -96,7 +96,7 @@ describe('Deploy freeze timezone dropdown', () => { }); it('renders selected time zone as dropdown label', () => { - expect(wrapper.find(GlDropdown).vm.text).toBe('Alaska'); + expect(wrapper.findComponent(GlDropdown).vm.text).toBe('Alaska'); }); }); }); diff --git a/spec/frontend/deprecated_jquery_dropdown_spec.js b/spec/frontend/deprecated_jquery_dropdown_spec.js index b18d53b317d..4a070395eaf 100644 --- a/spec/frontend/deprecated_jquery_dropdown_spec.js +++ b/spec/frontend/deprecated_jquery_dropdown_spec.js @@ -314,7 +314,7 @@ describe('deprecatedJQueryDropdown', () => { }); describe('with a trackSuggestionsClickedLabel', () => { - it('it includes data-track attributes', () => { + it('includes data-track attributes', () => { const dropdown = dropdownWithOptions({ trackSuggestionClickedLabel: 'some_value_for_label', }); @@ -333,7 +333,7 @@ describe('deprecatedJQueryDropdown', () => { expect(link).toHaveAttr('data-track-property', 'suggestion-category'); }); - it('it defaults property to no_category when category not provided', () => { + it('defaults property to no_category when category not provided', () => { const dropdown = dropdownWithOptions({ trackSuggestionClickedLabel: 'some_value_for_label', }); 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 28833b4af5c..df511586c10 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 @@ -43,6 +43,7 @@ describe('Design note component', () => { wrapper = shallowMountExtended(DesignNote, { propsData: { note: {}, + noteableId: 'gid://gitlab/DesignManagement::Design/6', ...props, }, data() { 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 f7ce742b933..e36f5c79e3e 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,5 +1,6 @@ import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import Autosave from '~/autosave'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; const showModal = jest.fn(); @@ -13,6 +14,7 @@ const GlModal = { describe('Design reply form component', () => { let wrapper; + let originalGon; const findTextarea = () => wrapper.find('textarea'); const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' }); @@ -24,6 +26,7 @@ describe('Design reply form component', () => { propsData: { value: '', isSaving: false, + noteableId: 'gid://gitlab/DesignManagement::Design/6', ...props, }, stubs: { GlModal }, @@ -31,8 +34,14 @@ describe('Design reply form component', () => { }); } + beforeEach(() => { + originalGon = window.gon; + window.gon.current_user_id = 1; + }); + afterEach(() => { wrapper.destroy(); + window.gon = originalGon; }); it('textarea has focus after component mount', () => { @@ -66,6 +75,25 @@ describe('Design reply form component', () => { expect(findSubmitButton().html()).toMatchSnapshot(); }); + it.each` + discussionId | shortDiscussionId + ${undefined} | ${'new'} + ${'gid://gitlab/DiffDiscussion/123'} | ${123} + `( + 'initializes autosave support on discussion with proper key', + async ({ discussionId, shortDiscussionId }) => { + createComponent({ discussionId }); + await nextTick(); + + // We discourage testing `wrapper.vm` properties but + // since `autosave` library instantiates on component + // there's no other way to test whether instantiation + // happened correctly or not. + expect(wrapper.vm.autosaveDiscussion).toBeInstanceOf(Autosave); + expect(wrapper.vm.autosaveDiscussion.key).toBe(`autosave/Discussion/6/${shortDiscussionId}`); + }, + ); + describe('when form has no text', () => { beforeEach(() => { createComponent({ @@ -120,28 +148,37 @@ describe('Design reply form component', () => { }); it('emits submitForm event on Comment button click', async () => { + const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + findSubmitButton().vm.$emit('click'); await nextTick(); expect(wrapper.emitted('submit-form')).toBeTruthy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); it('emits submitForm event on textarea ctrl+enter keydown', async () => { + const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + findTextarea().trigger('keydown.enter', { ctrlKey: true, }); await nextTick(); expect(wrapper.emitted('submit-form')).toBeTruthy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); it('emits submitForm event on textarea meta+enter keydown', async () => { + const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + findTextarea().trigger('keydown.enter', { metaKey: true, }); await nextTick(); expect(wrapper.emitted('submit-form')).toBeTruthy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); it('emits input event on changing textarea content', async () => { @@ -180,10 +217,13 @@ describe('Design reply form component', () => { }); it('emits cancelForm event on modal Ok button click', () => { + const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset'); + findTextarea().trigger('keyup.esc'); findModal().vm.$emit('ok'); expect(wrapper.emitted('cancel-form')).toBeTruthy(); + expect(autosaveResetSpy).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js index 30eddcee86a..4a339899473 100644 --- a/spec/frontend/design_management/components/design_presentation_spec.js +++ b/spec/frontend/design_management/components/design_presentation_spec.js @@ -525,7 +525,7 @@ describe('Design management design presentation component', () => { { clientX: 10, clientY: 10 }, { mouseup: true }, ).then(() => { - expect(wrapper.emitted('openCommentForm')).toBeFalsy(); + expect(wrapper.emitted('openCommentForm')).toBeUndefined(); }); }); 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 8fe3e92360a..096d776a7d2 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 @@ -11,7 +11,7 @@ exports[`Design management list item component when item appears in view after i exports[`Design management list item component with notes renders item with multiple comments 1`] = ` { await nextTick(); wrapper.findComponent(DeleteButton).vm.$emit('delete-selected-designs'); - expect(wrapper.emitted().delete).toBeTruthy(); + expect(wrapper.emitted().delete).toHaveLength(1); }); it('renders download button with correct link', () => { 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 9997f02cd01..8cfe11c9040 100644 --- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap @@ -9,9 +9,7 @@ exports[`Design management index page designs renders error 1`] = ` -
+
-
+
@@ -195,7 +195,7 @@ exports[`Design management design index page with error GlAlert is rendered in c diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index f90feaadfb0..1033b509419 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -254,7 +254,7 @@ describe('Design management index page', () => { 'gl-flex-direction-column', 'col-md-6', 'col-lg-3', - 'gl-mb-3', + 'gl-mt-5', ]); }); }); diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js index 96f2ac1692c..b88206c3b9a 100644 --- a/spec/frontend/diffs/components/app_spec.js +++ b/spec/frontend/diffs/components/app_spec.js @@ -30,7 +30,7 @@ const UPDATED_COMMIT_URL = `${TEST_HOST}/COMMIT/NEW`; Vue.use(Vuex); function getCollapsedFilesWarning(wrapper) { - return wrapper.find(CollapsedFilesWarning); + return wrapper.findComponent(CollapsedFilesWarning); } describe('diffs/components/app', () => { @@ -167,7 +167,7 @@ describe('diffs/components/app', () => { state.diffs.isLoading = true; }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('displays loading icon on batch loading', () => { @@ -175,13 +175,13 @@ describe('diffs/components/app', () => { state.diffs.batchLoadingState = 'loading'; }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('displays diffs container when not loading', () => { createComponent(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find('#diffs').exists()).toBe(true); }); @@ -263,7 +263,7 @@ describe('diffs/components/app', () => { it('renders empty state when no diff files exist', () => { createComponent(); - expect(wrapper.find(NoChanges).exists()).toBe(true); + expect(wrapper.findComponent(NoChanges).exists()).toBe(true); }); it('does not render empty state when diff files exist', () => { @@ -273,8 +273,8 @@ describe('diffs/components/app', () => { }); }); - expect(wrapper.find(NoChanges).exists()).toBe(false); - expect(wrapper.findAll(DiffFile).length).toBe(1); + expect(wrapper.findComponent(NoChanges).exists()).toBe(false); + expect(wrapper.findAllComponents(DiffFile).length).toBe(1); }); }); @@ -487,8 +487,8 @@ describe('diffs/components/app', () => { state.diffs.mergeRequestDiff = mergeRequestDiff; }); - expect(wrapper.find(CompareVersions).exists()).toBe(true); - expect(wrapper.find(CompareVersions).props()).toEqual( + expect(wrapper.findComponent(CompareVersions).exists()).toBe(true); + expect(wrapper.findComponent(CompareVersions).props()).toEqual( expect.objectContaining({ diffFilesCountText: null, }), @@ -506,8 +506,8 @@ describe('diffs/components/app', () => { state.diffs.size = 1; }); - expect(wrapper.find(HiddenFilesWarning).exists()).toBe(true); - expect(wrapper.find(HiddenFilesWarning).props()).toEqual( + expect(wrapper.findComponent(HiddenFilesWarning).exists()).toBe(true); + expect(wrapper.findComponent(HiddenFilesWarning).props()).toEqual( expect.objectContaining({ total: '5', plainDiffPath: 'plain diff path', @@ -547,7 +547,7 @@ describe('diffs/components/app', () => { }; }); - expect(wrapper.find(CommitWidget).exists()).toBe(true); + expect(wrapper.findComponent(CommitWidget).exists()).toBe(true); }); it('should display diff file if there are diff files', () => { @@ -555,13 +555,13 @@ describe('diffs/components/app', () => { state.diffs.diffFiles.push({ sha: '123' }); }); - expect(wrapper.find(DiffFile).exists()).toBe(true); + expect(wrapper.findComponent(DiffFile).exists()).toBe(true); }); it("doesn't render tree list when no changes exist", () => { createComponent(); - expect(wrapper.find(TreeList).exists()).toBe(false); + expect(wrapper.findComponent(TreeList).exists()).toBe(false); }); it('should render tree list', () => { @@ -569,7 +569,7 @@ describe('diffs/components/app', () => { state.diffs.diffFiles = [{ file_hash: '111', file_path: '111.js' }]; }); - expect(wrapper.find(TreeList).exists()).toBe(true); + expect(wrapper.findComponent(TreeList).exists()).toBe(true); }); }); @@ -636,12 +636,12 @@ describe('diffs/components/app', () => { await nextTick(); - expect(wrapper.findAll(DiffFile).length).toBe(1); + expect(wrapper.findAllComponents(DiffFile).length).toBe(1); }); describe('pagination', () => { const fileByFileNav = () => wrapper.find('[data-testid="file-by-file-navigation"]'); - const paginator = () => fileByFileNav().find(GlPagination); + const paginator = () => fileByFileNav().findComponent(GlPagination); it('sets previous button as disabled', async () => { createComponent({ fileByFileUserPreference: true }, ({ state }) => { @@ -682,7 +682,7 @@ describe('diffs/components/app', () => { ${'123'} | ${2} ${'312'} | ${1} `( - 'it calls navigateToDiffFileIndex with $index when $link is clicked', + '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' }); diff --git a/spec/frontend/diffs/components/collapsed_files_warning_spec.js b/spec/frontend/diffs/components/collapsed_files_warning_spec.js index cc4f13ab0cf..eca5b536a35 100644 --- a/spec/frontend/diffs/components/collapsed_files_warning_spec.js +++ b/spec/frontend/diffs/components/collapsed_files_warning_spec.js @@ -28,8 +28,8 @@ describe('CollapsedFilesWarning', () => { Vue.use(Vuex); const getAlertActionButton = () => - wrapper.find(CollapsedFilesWarning).find('button.gl-alert-action:first-child'); - const getAlertCloseButton = () => wrapper.find(CollapsedFilesWarning).find('button'); + wrapper.findComponent(CollapsedFilesWarning).find('button.gl-alert-action:first-child'); + const getAlertCloseButton = () => wrapper.findComponent(CollapsedFilesWarning).find('button'); const createComponent = (props = {}, { full } = { full: false }) => { const mounter = full ? mount : shallowMount; diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js index e52c5abbc7b..440f169be86 100644 --- a/spec/frontend/diffs/components/commit_item_spec.js +++ b/spec/frontend/diffs/components/commit_item_spec.js @@ -27,7 +27,7 @@ describe('diffs/components/commit_item', () => { const getAvatarElement = () => wrapper.find('.user-avatar-link'); const getCommitterElement = () => wrapper.find('.committer'); const getCommitActionsElement = () => wrapper.find('.commit-actions'); - const getCommitPipelineStatus = () => wrapper.find(CommitPipelineStatus); + const getCommitPipelineStatus = () => wrapper.findComponent(CommitPipelineStatus); const mountComponent = (propsData) => { wrapper = mount(Component, { @@ -111,8 +111,8 @@ describe('diffs/components/commit_item', () => { const descElement = getDescElement(); const descExpandElement = getDescExpandElement(); - expect(descElement.exists()).toBeFalsy(); - expect(descExpandElement.exists()).toBeFalsy(); + expect(descElement.exists()).toBe(false); + expect(descExpandElement.exists()).toBe(false); }); }); diff --git a/spec/frontend/diffs/components/commit_widget_spec.js b/spec/frontend/diffs/components/commit_widget_spec.js index fbff473e4df..f650ead6f83 100644 --- a/spec/frontend/diffs/components/commit_widget_spec.js +++ b/spec/frontend/diffs/components/commit_widget_spec.js @@ -12,7 +12,7 @@ describe('diffs/components/commit_widget', () => { }); it('renders commit item', () => { - const commitElement = wrapper.find(CommitItem); + const commitElement = wrapper.findComponent(CommitItem); expect(commitElement.exists()).toBe(true); }); diff --git a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js index 98f88226742..09128b04caa 100644 --- a/spec/frontend/diffs/components/compare_dropdown_layout_spec.js +++ b/spec/frontend/diffs/components/compare_dropdown_layout_spec.js @@ -34,7 +34,7 @@ describe('CompareDropdownLayout', () => { findListItems().wrappers.map((listItem) => ({ href: listItem.find('a').attributes('href'), text: trimText(listItem.text()), - createdAt: listItem.findAll(TimeAgo).wrappers[0]?.props('time'), + createdAt: listItem.findAllComponents(TimeAgo).wrappers[0]?.props('time'), isActive: listItem.classes().includes('is-active'), })); diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js index 81a817c47dc..b5dce4fc924 100644 --- a/spec/frontend/diffs/components/diff_code_quality_spec.js +++ b/spec/frontend/diffs/components/diff_code_quality_spec.js @@ -17,7 +17,6 @@ describe('DiffCodeQuality', () => { return mountFunction(DiffCodeQuality, { propsData: { expandedLines: [], - line: 1, codeQuality, }, }); @@ -28,9 +27,7 @@ describe('DiffCodeQuality', () => { expect(wrapper.findByTestId('diff-codequality').exists()).toBe(true); await wrapper.findByTestId('diff-codequality-close').trigger('click'); - expect(wrapper.emitted('hideCodeQualityFindings').length).toBe(1); - expect(wrapper.emitted().hideCodeQualityFindings[0][0]).toBe(wrapper.props('line')); }); it('renders correct amount of list items for codequality array and their description', async () => { diff --git a/spec/frontend/diffs/components/diff_comment_cell_spec.js b/spec/frontend/diffs/components/diff_comment_cell_spec.js index b636a178593..2acfc2c6d7e 100644 --- a/spec/frontend/diffs/components/diff_comment_cell_spec.js +++ b/spec/frontend/diffs/components/diff_comment_cell_spec.js @@ -20,24 +20,24 @@ describe('DiffCommentCell', () => { it('renders discussions if line has discussions', () => { const wrapper = createWrapper({ renderDiscussion: true }); - expect(wrapper.find(DiffDiscussions).exists()).toBe(true); + expect(wrapper.findComponent(DiffDiscussions).exists()).toBe(true); }); it('does not render discussions if line has no discussions', () => { const wrapper = createWrapper(); - expect(wrapper.find(DiffDiscussions).exists()).toBe(false); + expect(wrapper.findComponent(DiffDiscussions).exists()).toBe(false); }); it('renders discussion reply if line has no draft', () => { const wrapper = createWrapper(); - expect(wrapper.find(DiffDiscussionReply).exists()).toBe(true); + expect(wrapper.findComponent(DiffDiscussionReply).exists()).toBe(true); }); it('does not render discussion reply if line has draft', () => { const wrapper = createWrapper({ hasDraft: true }); - expect(wrapper.find(DiffDiscussionReply).exists()).toBe(false); + expect(wrapper.findComponent(DiffDiscussionReply).exists()).toBe(false); }); }); diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js index 6844e6e497a..9f593ee0d49 100644 --- a/spec/frontend/diffs/components/diff_content_spec.js +++ b/spec/frontend/diffs/components/diff_content_spec.js @@ -110,13 +110,13 @@ describe('DiffContent', () => { props: { diffFile: textDiffFile }, }); - expect(wrapper.find(DiffView).exists()).toBe(true); + expect(wrapper.findComponent(DiffView).exists()).toBe(true); }); it('renders rendering more lines loading icon', () => { createComponent({ props: { diffFile: { ...textDiffFile, renderingLines: true } } }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); @@ -133,7 +133,7 @@ describe('DiffContent', () => { props: { diffFile: { ...emptyDiffFile, viewer: { name: diffViewerModes.no_preview } } }, }); - expect(wrapper.find(NoPreviewViewer).exists()).toBe(true); + expect(wrapper.findComponent(NoPreviewViewer).exists()).toBe(true); }); it('should render not diffable view if viewer set to non_diffable', () => { @@ -141,7 +141,7 @@ describe('DiffContent', () => { props: { diffFile: { ...emptyDiffFile, viewer: { name: diffViewerModes.not_diffable } } }, }); - expect(wrapper.find(NotDiffableViewer).exists()).toBe(true); + expect(wrapper.findComponent(NotDiffableViewer).exists()).toBe(true); }); }); @@ -156,7 +156,7 @@ describe('DiffContent', () => { }, }); - expect(wrapper.find(DiffDiscussions).exists()).toBe(true); + expect(wrapper.findComponent(DiffDiscussions).exists()).toBe(true); }); it('emits saveDiffDiscussion when note-form emits `handleFormUpdate`', () => { @@ -169,7 +169,7 @@ describe('DiffContent', () => { }, }); - wrapper.find(NoteForm).vm.$emit('handleFormUpdate', noteStub); + wrapper.findComponent(NoteForm).vm.$emit('handleFormUpdate', noteStub); expect(saveDiffDiscussionMock).toHaveBeenCalledWith(expect.any(Object), { note: noteStub, formData: { diff --git a/spec/frontend/diffs/components/diff_discussion_reply_spec.js b/spec/frontend/diffs/components/diff_discussion_reply_spec.js index f03c0357a0e..5ccd2002462 100644 --- a/spec/frontend/diffs/components/diff_discussion_reply_spec.js +++ b/spec/frontend/diffs/components/diff_discussion_reply_spec.js @@ -64,7 +64,7 @@ describe('DiffDiscussionReply', () => { hasForm: false, }); - expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true); + expect(wrapper.findComponent(ReplyPlaceholder).exists()).toBe(true); }); }); @@ -83,6 +83,6 @@ describe('DiffDiscussionReply', () => { hasForm: false, }); - expect(wrapper.find(NoteSignedOutWidget).exists()).toBe(true); + expect(wrapper.findComponent(NoteSignedOutWidget).exists()).toBe(true); }); }); diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js index 2da68adddf6..e9a0e0745fd 100644 --- a/spec/frontend/diffs/components/diff_discussions_spec.js +++ b/spec/frontend/diffs/components/diff_discussions_spec.js @@ -32,11 +32,11 @@ describe('DiffDiscussions', () => { it('should have notes list', () => { createComponent(); - expect(wrapper.find(NoteableDiscussion).exists()).toBe(true); - expect(wrapper.find(DiscussionNotes).exists()).toBe(true); - expect(wrapper.find(DiscussionNotes).findAll(TimelineEntryItem).length).toBe( - discussionsMockData.notes.length, - ); + expect(wrapper.findComponent(NoteableDiscussion).exists()).toBe(true); + expect(wrapper.findComponent(DiscussionNotes).exists()).toBe(true); + expect( + wrapper.findComponent(DiscussionNotes).findAllComponents(TimelineEntryItem).length, + ).toBe(discussionsMockData.notes.length); }); }); @@ -48,7 +48,7 @@ describe('DiffDiscussions', () => { const diffNotesToggle = findDiffNotesToggle(); expect(diffNotesToggle.exists()).toBe(true); - expect(diffNotesToggle.find(GlIcon).exists()).toBe(true); + expect(diffNotesToggle.findComponent(GlIcon).exists()).toBe(true); expect(diffNotesToggle.classes('diff-notes-collapse')).toBe(true); }); @@ -80,12 +80,12 @@ describe('DiffDiscussions', () => { discussions[0].expanded = false; createComponent({ discussions, shouldCollapseDiscussions: true }); - expect(wrapper.find(NoteableDiscussion).isVisible()).toBe(false); + expect(wrapper.findComponent(NoteableDiscussion).isVisible()).toBe(false); }); it('renders badge on avatar', () => { createComponent({ renderAvatarBadge: true }); - const noteableDiscussion = wrapper.find(NoteableDiscussion); + const noteableDiscussion = wrapper.findComponent(NoteableDiscussion); expect(noteableDiscussion.find('.design-note-pin').exists()).toBe(true); expect(noteableDiscussion.find('.design-note-pin').text().trim()).toBe('1'); diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js index 92b8b2d4aa3..c23eb2f3d24 100644 --- a/spec/frontend/diffs/components/diff_file_header_spec.js +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -76,18 +76,19 @@ describe('DiffFileHeader component', () => { wrapper.destroy(); }); - const findHeader = () => wrapper.find({ ref: 'header' }); - const findTitleLink = () => wrapper.find({ ref: 'titleWrapper' }); - const findExpandButton = () => wrapper.find({ ref: 'expandDiffToFullFileButton' }); + const findHeader = () => wrapper.findComponent({ ref: 'header' }); + const findTitleLink = () => wrapper.findComponent({ ref: 'titleWrapper' }); + const findExpandButton = () => wrapper.findComponent({ ref: 'expandDiffToFullFileButton' }); const findFileActions = () => wrapper.find('.file-actions'); - const findModeChangedLine = () => wrapper.find({ ref: 'fileMode' }); + const findModeChangedLine = () => wrapper.findComponent({ ref: 'fileMode' }); const findLfsLabel = () => wrapper.find('[data-testid="label-lfs"]'); - const findToggleDiscussionsButton = () => wrapper.find({ ref: 'toggleDiscussionsButton' }); - const findExternalLink = () => wrapper.find({ ref: 'externalLink' }); - const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' }); - const findViewFileButton = () => wrapper.find({ ref: 'viewButton' }); - const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' }); - const findEditButton = () => wrapper.find({ ref: 'editButton' }); + const findToggleDiscussionsButton = () => + wrapper.findComponent({ ref: 'toggleDiscussionsButton' }); + 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 findEditButton = () => wrapper.findComponent({ ref: 'editButton' }); const findReviewFileCheckbox = () => wrapper.find("[data-testid='fileReviewCheckbox']"); const createComponent = ({ props, options = {} } = {}) => { @@ -153,7 +154,7 @@ describe('DiffFileHeader component', () => { }); it('displays a copy to clipboard button', () => { - expect(wrapper.find(ClipboardButton).exists()).toBe(true); + expect(wrapper.findComponent(ClipboardButton).exists()).toBe(true); }); it('triggers the copy to clipboard tracking event', () => { diff --git a/spec/frontend/diffs/components/diff_file_row_spec.js b/spec/frontend/diffs/components/diff_file_row_spec.js index 1d1c5fec293..c5b76551fcc 100644 --- a/spec/frontend/diffs/components/diff_file_row_spec.js +++ b/spec/frontend/diffs/components/diff_file_row_spec.js @@ -32,7 +32,7 @@ describe('Diff File Row component', () => { ...diffFileRowProps, }); - expect(wrapper.find(FileRow).props()).toEqual( + expect(wrapper.findComponent(FileRow).props()).toEqual( expect.objectContaining({ ...sharedProps, }), @@ -47,7 +47,7 @@ describe('Diff File Row component', () => { showTooltip: true, }); - expect(wrapper.find(ChangedFileIcon).props()).toEqual( + expect(wrapper.findComponent(ChangedFileIcon).props()).toEqual( expect.objectContaining({ file: {}, size: 16, @@ -74,7 +74,7 @@ describe('Diff File Row component', () => { hideFileStats: false, viewedFiles: isViewed ? { '#123456789': true } : {}, }); - expect(wrapper.find(FileRow).props('fileClasses')).toBe(expected); + expect(wrapper.findComponent(FileRow).props('fileClasses')).toBe(expected); }, ); @@ -92,7 +92,7 @@ describe('Diff File Row component', () => { }, hideFileStats, }); - expect(wrapper.find(FileRowStats).exists()).toEqual(value); + expect(wrapper.findComponent(FileRowStats).exists()).toEqual(value); }); }); diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js index 9e8d9e1ca29..944cec77efb 100644 --- a/spec/frontend/diffs/components/diff_file_spec.js +++ b/spec/frontend/diffs/components/diff_file_spec.js @@ -100,7 +100,7 @@ function createComponent({ file, first = false, last = false, options = {}, prop }; } -const findDiffHeader = (wrapper) => wrapper.find(DiffFileHeaderComponent); +const findDiffHeader = (wrapper) => wrapper.findComponent(DiffFileHeaderComponent); const findDiffContentArea = (wrapper) => wrapper.find('[data-testid="content-area"]'); const findLoader = (wrapper) => wrapper.find('[data-testid="loader-icon"]'); const findToggleButton = (wrapper) => wrapper.find('[data-testid="expand-button"]'); @@ -209,14 +209,14 @@ describe('DiffFile', () => { expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0); expect(el.querySelector('.js-file-title')).toBeDefined(); - expect(wrapper.find(DiffFileHeaderComponent).exists()).toBe(true); + expect(wrapper.findComponent(DiffFileHeaderComponent).exists()).toBe(true); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); markFileToBeRendered(store); await nextTick(); - expect(wrapper.find(DiffContentComponent).exists()).toBe(true); + expect(wrapper.findComponent(DiffContentComponent).exists()).toBe(true); }); }); @@ -320,7 +320,7 @@ describe('DiffFile', () => { }); it('should have the file content', async () => { - expect(wrapper.find(DiffContentComponent).exists()).toBe(true); + expect(wrapper.findComponent(DiffContentComponent).exists()).toBe(true); }); it('should style the component so that it `.has-body` for layout purposes', () => { @@ -473,8 +473,8 @@ describe('DiffFile', () => { await nextTick(); expect(wrapper.classes('has-body')).toBe(true); - expect(wrapper.find(DiffContentComponent).exists()).toBe(true); - expect(wrapper.find(DiffContentComponent).isVisible()).toBe(true); + expect(wrapper.findComponent(DiffContentComponent).exists()).toBe(true); + expect(wrapper.findComponent(DiffContentComponent).isVisible()).toBe(true); }, ); }); diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js index c18f0b721da..f13988fc11f 100644 --- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js +++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue'; +import { HIDE_COMMENTS } from '~/diffs/i18n'; import discussionsMockData from '../mock_data/diff_discussions'; const getDiscussionsMockData = () => [{ ...discussionsMockData }]; @@ -40,7 +41,12 @@ describe('DiffGutterAvatars', () => { findCollapseButton().trigger('click'); await nextTick(); - expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy(); + expect(wrapper.emitted().toggleLineDiscussions).toBeDefined(); + }); + + it('renders the proper title and aria-label', () => { + expect(findCollapseButton().attributes('title')).toBe(HIDE_COMMENTS); + expect(findCollapseButton().attributes('aria-label')).toBe(HIDE_COMMENTS); }); }); @@ -69,14 +75,14 @@ describe('DiffGutterAvatars', () => { findUserAvatars().at(0).trigger('click'); await nextTick(); - expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy(); + expect(wrapper.emitted().toggleLineDiscussions).toBeDefined(); }); it('should emit toggleDiscussions event on more count text click', async () => { findMoreCount().trigger('click'); await nextTick(); - expect(wrapper.emitted().toggleLineDiscussions).toBeTruthy(); + expect(wrapper.emitted().toggleLineDiscussions).toBeDefined(); }); }); 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 542d61c4680..9493dc8855e 100644 --- a/spec/frontend/diffs/components/diff_line_note_form_spec.js +++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js @@ -82,7 +82,7 @@ describe('DiffLineNoteForm', () => { }); it('shows note form', () => { - expect(wrapper.find(NoteForm).exists()).toBe(true); + expect(wrapper.findComponent(NoteForm).exists()).toBe(true); }); it('passes the provided range of lines to comment form', () => { diff --git a/spec/frontend/diffs/components/diff_line_spec.js b/spec/frontend/diffs/components/diff_line_spec.js new file mode 100644 index 00000000000..37368eb1461 --- /dev/null +++ b/spec/frontend/diffs/components/diff_line_spec.js @@ -0,0 +1,65 @@ +import { shallowMount } from '@vue/test-utils'; +import DiffLine from '~/diffs/components/diff_line.vue'; +import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue'; + +const EXAMPLE_LINE_NUMBER = 3; +const EXAMPLE_DESCRIPTION = 'example description'; +const EXAMPLE_SEVERITY = 'example severity'; + +const left = { + line: { + left: { + codequality: [ + { + line: EXAMPLE_LINE_NUMBER, + description: EXAMPLE_DESCRIPTION, + severity: EXAMPLE_SEVERITY, + }, + ], + }, + }, +}; + +const right = { + line: { + right: { + codequality: [ + { + line: EXAMPLE_LINE_NUMBER, + description: EXAMPLE_DESCRIPTION, + severity: EXAMPLE_SEVERITY, + }, + ], + }, + }, +}; + +const mockData = [right, left]; + +describe('DiffLine', () => { + const createWrapper = (propsData) => { + return shallowMount(DiffLine, { propsData }); + }; + + it('should emit event when hideCodeQualityFindings is called', () => { + const wrapper = createWrapper(right); + + wrapper.findComponent(DiffCodeQuality).vm.$emit('hideCodeQualityFindings'); + expect(wrapper.emitted()).toEqual({ + hideCodeQualityFindings: [[EXAMPLE_LINE_NUMBER]], + }); + }); + + mockData.forEach((element) => { + it('should set correct props for DiffCodeQuality', () => { + const wrapper = createWrapper(element); + expect(wrapper.findComponent(DiffCodeQuality).props('codeQuality')).toEqual([ + { + line: EXAMPLE_LINE_NUMBER, + description: EXAMPLE_DESCRIPTION, + severity: EXAMPLE_SEVERITY, + }, + ]); + }); + }); +}); diff --git a/spec/frontend/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js index 09fe69e97de..3a04547fa69 100644 --- a/spec/frontend/diffs/components/diff_stats_spec.js +++ b/spec/frontend/diffs/components/diff_stats_spec.js @@ -87,7 +87,7 @@ describe('diff_stats', () => { describe('files changes', () => { const findIcon = (name) => wrapper - .findAll(GlIcon) + .findAllComponents(GlIcon) .filter((c) => c.attributes('name') === name) .at(0).element.parentNode; diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js index 15923a1c6de..1dd4a2f6c23 100644 --- a/spec/frontend/diffs/components/diff_view_spec.js +++ b/spec/frontend/diffs/components/diff_view_spec.js @@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DiffView from '~/diffs/components/diff_view.vue'; -import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue'; +import DiffLine from '~/diffs/components/diff_line.vue'; import { diffCodeQuality } from '../mock_data/diff_code_quality'; describe('DiffView', () => { @@ -51,28 +51,27 @@ describe('DiffView', () => { return shallowMount(DiffView, { propsData, store, stubs, provide }); }; - it('does not render a codeQuality diff view when there is no finding', () => { + it('does not render a diff-line component when there is no finding', () => { const wrapper = createWrapper(); - expect(wrapper.findComponent(DiffCodeQuality).exists()).toBe(false); + expect(wrapper.findComponent(DiffLine).exists()).toBe(false); }); - it('does render a codeQuality diff view with the correct props when there is a finding & refactorCodeQualityInlineFindings flag is true ', async () => { + 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 }, }); wrapper.findComponent(DiffRow).vm.$emit('toggleCodeQualityFindings', 2); await nextTick(); - expect(wrapper.findComponent(DiffCodeQuality).exists()).toBe(true); - expect(wrapper.findComponent(DiffCodeQuality).props().codeQuality.length).not.toBe(0); + expect(wrapper.findComponent(DiffLine).props('line')).toBe(diffCodeQuality.diffLines[2]); }); - it('does not render a codeQuality diff view when there is a finding & refactorCodeQualityInlineFindings flag is false ', async () => { + 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(DiffCodeQuality).exists()).toBe(false); + expect(wrapper.findComponent(DiffLine).exists()).toBe(false); }); it.each` @@ -89,8 +88,8 @@ describe('DiffView', () => { diffLines: [{ renderCommentRow: true, ...sides }], inline: type === 'inline', }); - expect(wrapper.findAll(DiffCommentCell).length).toBe(total); - expect(wrapper.find(container).find(DiffCommentCell).exists()).toBe(true); + expect(wrapper.findAllComponents(DiffCommentCell).length).toBe(total); + expect(wrapper.find(container).findComponent(DiffCommentCell).exists()).toBe(true); }, ); @@ -98,7 +97,7 @@ describe('DiffView', () => { const wrapper = createWrapper({ diffLines: [{ renderCommentRow: true, left: { lineDraft: { isDraft: true } } }], }); - expect(wrapper.find(DraftNote).exists()).toBe(true); + expect(wrapper.findComponent(DraftNote).exists()).toBe(true); }); describe('drag operations', () => { diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js index 70191620eb6..ccf942bdcef 100644 --- a/spec/frontend/diffs/components/image_diff_overlay_spec.js +++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js @@ -57,7 +57,7 @@ describe('Diffs image diff overlay component', () => { it('renders icon when showCommentIcon is true', () => { createComponent({ showCommentIcon: true }); - expect(wrapper.find(GlIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlIcon).exists()).toBe(true); }); it('sets badge comment positions', () => { diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js index 6903b844e5e..dbfe9770e07 100644 --- a/spec/frontend/diffs/components/no_changes_spec.js +++ b/spec/frontend/diffs/components/no_changes_spec.js @@ -56,7 +56,7 @@ describe('Diff no changes empty state', () => { it('Show create commit button', () => { createComponent(); - expect(wrapper.find(GlButton).exists()).toBe(true); + expect(wrapper.findComponent(GlButton).exists()).toBe(true); }); it.each` diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js index 931a9562d36..ca7de8fd751 100644 --- a/spec/frontend/diffs/components/tree_list_spec.js +++ b/spec/frontend/diffs/components/tree_list_spec.js @@ -106,7 +106,7 @@ describe('Diffs tree list component', () => { ${'index.js'} | ${1} ${'app/*.js'} | ${1} ${'*.js, *.rb'} | ${2} - `('it returns $itemSize item for $extension', async ({ extension, itemSize }) => { + `('returns $itemSize item for $extension', async ({ extension, itemSize }) => { wrapper.find('[data-testid="diff-tree-search"]').setValue(extension); await nextTick(); @@ -175,7 +175,7 @@ describe('Diffs tree list component', () => { await nextTick(); // Have to use $attrs['viewed-files'] because we are passing down an object // and attributes('') stringifies values (e.g. [object])... - expect(wrapper.find(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds); + expect(wrapper.findComponent(FileTree).vm.$attrs['viewed-files']).toBe(viewedDiffFileIds); }); }); }); diff --git a/spec/frontend/editor/components/source_editor_toolbar_spec.js b/spec/frontend/editor/components/source_editor_toolbar_spec.js index 6e99eadbd97..bead39ca744 100644 --- a/spec/frontend/editor/components/source_editor_toolbar_spec.js +++ b/spec/frontend/editor/components/source_editor_toolbar_spec.js @@ -68,7 +68,7 @@ describe('Source Editor Toolbar', () => { }); describe('buttons update', () => { - it('it properly updates buttons on Apollo cache update', async () => { + it('properly updates buttons on Apollo cache update', async () => { const item = buildButton('first', { group: EDITOR_TOOLBAR_RIGHT_GROUP, }); diff --git a/spec/frontend/editor/source_editor_extension_spec.js b/spec/frontend/editor/source_editor_extension_spec.js index 78453aaa491..3424e71d326 100644 --- a/spec/frontend/editor/source_editor_extension_spec.js +++ b/spec/frontend/editor/source_editor_extension_spec.js @@ -16,7 +16,7 @@ describe('Editor Extension', () => { 'throws when definition = $definition and setupOptions = $setupOptions', ({ definition, setupOptions }) => { const constructExtension = () => new EditorExtension({ definition, setupOptions }); - expect(constructExtension).toThrowError(EDITOR_EXTENSION_DEFINITION_ERROR); + expect(constructExtension).toThrow(EDITOR_EXTENSION_DEFINITION_ERROR); }, ); diff --git a/spec/frontend/editor/source_editor_instance_spec.js b/spec/frontend/editor/source_editor_instance_spec.js index 1223fee320e..20ba23d56ff 100644 --- a/spec/frontend/editor/source_editor_instance_spec.js +++ b/spec/frontend/editor/source_editor_instance_spec.js @@ -248,7 +248,7 @@ describe('Source Editor Instance', () => { const useExtension = () => { seInstance.use(extensions); }; - expect(useExtension).toThrowError(thrownError); + expect(useExtension).toThrow(thrownError); }, ); @@ -336,7 +336,7 @@ describe('Source Editor Instance', () => { const unuse = () => { seInstance.unuse(unuseExtension); }; - expect(unuse).toThrowError(thrownError); + expect(unuse).toThrow(thrownError); }, ); @@ -382,7 +382,7 @@ describe('Source Editor Instance', () => { }, ); - it('it does not remove entry from the global registry to keep for potential future re-use', () => { + it('does not remove entry from the global registry to keep for potential future re-use', () => { const extensionStore = new Map(); seInstance = new SourceEditorInstance({}, extensionStore); const extensions = seInstance.use(fullExtensionsArray); diff --git a/spec/frontend/editor/source_editor_webide_ext_spec.js b/spec/frontend/editor/source_editor_webide_ext_spec.js index 096b6b1646f..f418eab668a 100644 --- a/spec/frontend/editor/source_editor_webide_ext_spec.js +++ b/spec/frontend/editor/source_editor_webide_ext_spec.js @@ -30,7 +30,7 @@ describe('Source Editor Web IDE Extension', () => { const sideBySideSpy = jest.spyOn(instance, 'updateOptions'); instance.use({ definition: EditorWebIdeExtension }); - expect(sideBySideSpy).toBeCalledWith({ renderSideBySide }); + expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide }); }, ); @@ -45,11 +45,11 @@ describe('Source Editor Web IDE Extension', () => { const sideBySideSpy = jest.spyOn(instance, 'updateOptions'); await emitter.fire(); - expect(sideBySideSpy).toBeCalledWith({ renderSideBySide: true }); + expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide: true }); editorEl.style.width = '0px'; await emitter.fire(); - expect(sideBySideSpy).toBeCalledWith({ renderSideBySide: false }); + expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide: false }); }); }); }); diff --git a/spec/frontend/emoji/components/category_spec.js b/spec/frontend/emoji/components/category_spec.js index 82dc0cdc250..90816f28d5b 100644 --- a/spec/frontend/emoji/components/category_spec.js +++ b/spec/frontend/emoji/components/category_spec.js @@ -22,7 +22,7 @@ describe('Emoji category component', () => { }); it('renders emoji groups', () => { - expect(wrapper.findAll(EmojiGroup).length).toBe(2); + expect(wrapper.findAllComponents(EmojiGroup).length).toBe(2); }); it('renders group', async () => { @@ -30,19 +30,19 @@ describe('Emoji category component', () => { // eslint-disable-next-line no-restricted-syntax await wrapper.setData({ renderGroup: true }); - expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true'); + expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true'); }); it('renders group on appear', async () => { - wrapper.find(GlIntersectionObserver).vm.$emit('appear'); + wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); await nextTick(); - expect(wrapper.find(EmojiGroup).attributes('rendergroup')).toBe('true'); + expect(wrapper.findComponent(EmojiGroup).attributes('rendergroup')).toBe('true'); }); it('emits appear event on appear', async () => { - wrapper.find(GlIntersectionObserver).vm.$emit('appear'); + wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); await nextTick(); diff --git a/spec/frontend/emoji/components/utils_spec.js b/spec/frontend/emoji/components/utils_spec.js index 56f514ee9a8..a17ddb3bb9a 100644 --- a/spec/frontend/emoji/components/utils_spec.js +++ b/spec/frontend/emoji/components/utils_spec.js @@ -4,13 +4,13 @@ import { getFrequentlyUsedEmojis, addToFrequentlyUsed } from '~/emoji/components jest.mock('~/lib/utils/cookies'); describe('getFrequentlyUsedEmojis', () => { - it('it returns null when no saved emojis set', () => { + it('returns null when no saved emojis set', () => { jest.spyOn(Cookies, 'get').mockReturnValue(null); expect(getFrequentlyUsedEmojis()).toBe(null); }); - it('it returns frequently used emojis object', () => { + it('returns frequently used emojis object', () => { jest.spyOn(Cookies, 'get').mockReturnValue('thumbsup,thumbsdown'); expect(getFrequentlyUsedEmojis()).toEqual({ diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js index dc8f50e0e4b..36c3eeb5a52 100644 --- a/spec/frontend/emoji/index_spec.js +++ b/spec/frontend/emoji/index_spec.js @@ -120,177 +120,177 @@ describe('emoji', () => { describe('isFlagEmoji', () => { it('should gracefully handle empty string', () => { - expect(isFlagEmoji('')).toBeFalsy(); + expect(isFlagEmoji('')).toBe(false); }); it('should detect flag_ac', () => { - expect(isFlagEmoji('🇦🇨')).toBeTruthy(); + expect(isFlagEmoji('🇦🇨')).toBe(true); }); it('should detect flag_us', () => { - expect(isFlagEmoji('🇺🇸')).toBeTruthy(); + expect(isFlagEmoji('🇺🇸')).toBe(true); }); it('should detect flag_zw', () => { - expect(isFlagEmoji('🇿🇼')).toBeTruthy(); + expect(isFlagEmoji('🇿🇼')).toBe(true); }); it('should not detect flags', () => { - expect(isFlagEmoji('🎏')).toBeFalsy(); + expect(isFlagEmoji('🎏')).toBe(false); }); it('should not detect triangular_flag_on_post', () => { - expect(isFlagEmoji('🚩')).toBeFalsy(); + expect(isFlagEmoji('🚩')).toBe(false); }); it('should not detect single letter', () => { - expect(isFlagEmoji('🇦')).toBeFalsy(); + expect(isFlagEmoji('🇦')).toBe(false); }); it('should not detect >2 letters', () => { - expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy(); + expect(isFlagEmoji('🇦🇧🇨')).toBe(false); }); }); describe('isRainbowFlagEmoji', () => { it('should gracefully handle empty string', () => { - expect(isRainbowFlagEmoji('')).toBeFalsy(); + expect(isRainbowFlagEmoji('')).toBe(false); }); it('should detect rainbow_flag', () => { - expect(isRainbowFlagEmoji('🏳🌈')).toBeTruthy(); + expect(isRainbowFlagEmoji('🏳🌈')).toBe(true); }); it("should not detect flag_white on its' own", () => { - expect(isRainbowFlagEmoji('🏳')).toBeFalsy(); + expect(isRainbowFlagEmoji('🏳')).toBe(false); }); it("should not detect rainbow on its' own", () => { - expect(isRainbowFlagEmoji('🌈')).toBeFalsy(); + expect(isRainbowFlagEmoji('🌈')).toBe(false); }); it('should not detect flag_white with something else', () => { - expect(isRainbowFlagEmoji('🏳🔵')).toBeFalsy(); + expect(isRainbowFlagEmoji('🏳🔵')).toBe(false); }); }); describe('isKeycapEmoji', () => { it('should gracefully handle empty string', () => { - expect(isKeycapEmoji('')).toBeFalsy(); + expect(isKeycapEmoji('')).toBe(false); }); it('should detect one(keycap)', () => { - expect(isKeycapEmoji('1️⃣')).toBeTruthy(); + expect(isKeycapEmoji('1️⃣')).toBe(true); }); it('should detect nine(keycap)', () => { - expect(isKeycapEmoji('9️⃣')).toBeTruthy(); + expect(isKeycapEmoji('9️⃣')).toBe(true); }); it('should not detect ten(keycap)', () => { - expect(isKeycapEmoji('🔟')).toBeFalsy(); + expect(isKeycapEmoji('🔟')).toBe(false); }); it('should not detect hash(keycap)', () => { - expect(isKeycapEmoji('#⃣')).toBeFalsy(); + expect(isKeycapEmoji('#⃣')).toBe(false); }); }); describe('isSkinToneComboEmoji', () => { it('should gracefully handle empty string', () => { - expect(isSkinToneComboEmoji('')).toBeFalsy(); + expect(isSkinToneComboEmoji('')).toBe(false); }); it('should detect hand_splayed_tone5', () => { - expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy(); + expect(isSkinToneComboEmoji('🖐🏿')).toBe(true); }); it('should not detect hand_splayed', () => { - expect(isSkinToneComboEmoji('🖐')).toBeFalsy(); + expect(isSkinToneComboEmoji('🖐')).toBe(false); }); it('should detect lifter_tone1', () => { - expect(isSkinToneComboEmoji('🏋🏻')).toBeTruthy(); + expect(isSkinToneComboEmoji('🏋🏻')).toBe(true); }); it('should not detect lifter', () => { - expect(isSkinToneComboEmoji('🏋')).toBeFalsy(); + expect(isSkinToneComboEmoji('🏋')).toBe(false); }); it('should detect rowboat_tone4', () => { - expect(isSkinToneComboEmoji('🚣🏾')).toBeTruthy(); + expect(isSkinToneComboEmoji('🚣🏾')).toBe(true); }); it('should not detect rowboat', () => { - expect(isSkinToneComboEmoji('🚣')).toBeFalsy(); + expect(isSkinToneComboEmoji('🚣')).toBe(false); }); it('should not detect individual tone emoji', () => { - expect(isSkinToneComboEmoji('🏻')).toBeFalsy(); + expect(isSkinToneComboEmoji('🏻')).toBe(false); }); }); describe('isHorceRacingSkinToneComboEmoji', () => { it('should gracefully handle empty string', () => { - expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy(); + expect(isHorceRacingSkinToneComboEmoji('')).toBeUndefined(); }); it('should detect horse_racing_tone2', () => { - expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy(); + expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBe(true); }); it('should not detect horse_racing', () => { - expect(isHorceRacingSkinToneComboEmoji('🏇')).toBeFalsy(); + expect(isHorceRacingSkinToneComboEmoji('🏇')).toBe(false); }); }); describe('isPersonZwjEmoji', () => { it('should gracefully handle empty string', () => { - expect(isPersonZwjEmoji('')).toBeFalsy(); + expect(isPersonZwjEmoji('')).toBe(false); }); it('should detect couple_mm', () => { - expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBeTruthy(); + expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBe(true); }); it('should not detect couple_with_heart', () => { - expect(isPersonZwjEmoji('💑')).toBeFalsy(); + expect(isPersonZwjEmoji('💑')).toBe(false); }); it('should not detect couplekiss', () => { - expect(isPersonZwjEmoji('💏')).toBeFalsy(); + expect(isPersonZwjEmoji('💏')).toBe(false); }); it('should detect family_mmb', () => { - expect(isPersonZwjEmoji('👨‍👨‍👦')).toBeTruthy(); + expect(isPersonZwjEmoji('👨‍👨‍👦')).toBe(true); }); it('should detect family_mwgb', () => { - expect(isPersonZwjEmoji('👨‍👩‍👧‍👦')).toBeTruthy(); + expect(isPersonZwjEmoji('👨‍👩‍👧‍👦')).toBe(true); }); it('should not detect family', () => { - expect(isPersonZwjEmoji('👪')).toBeFalsy(); + expect(isPersonZwjEmoji('👪')).toBe(false); }); it('should detect kiss_ww', () => { - expect(isPersonZwjEmoji('👩‍❤️‍💋‍👩')).toBeTruthy(); + expect(isPersonZwjEmoji('👩‍❤️‍💋‍👩')).toBe(true); }); it('should not detect girl', () => { - expect(isPersonZwjEmoji('👧')).toBeFalsy(); + expect(isPersonZwjEmoji('👧')).toBe(false); }); it('should not detect girl_tone5', () => { - expect(isPersonZwjEmoji('👧🏿')).toBeFalsy(); + expect(isPersonZwjEmoji('👧🏿')).toBe(false); }); it('should not detect man', () => { - expect(isPersonZwjEmoji('👨')).toBeFalsy(); + expect(isPersonZwjEmoji('👨')).toBe(false); }); it('should not detect woman', () => { - expect(isPersonZwjEmoji('👩')).toBeFalsy(); + expect(isPersonZwjEmoji('👩')).toBe(false); }); }); @@ -298,13 +298,13 @@ describe('emoji', () => { it('should gracefully handle empty string with unicode support', () => { const isSupported = isEmojiUnicodeSupported({ '1.0': true }, '', '1.0'); - expect(isSupported).toBeTruthy(); + expect(isSupported).toBe(true); }); it('should gracefully handle empty string without unicode support', () => { const isSupported = isEmojiUnicodeSupported({}, '', '1.0'); - expect(isSupported).toBeFalsy(); + expect(isSupported).toBeUndefined(); }); it('bomb(6.0) with 6.0 support', () => { @@ -316,7 +316,7 @@ describe('emoji', () => { emojiFixtureMap[emojiKey].unicodeVersion, ); - expect(isSupported).toBeTruthy(); + expect(isSupported).toBe(true); }); it('bomb(6.0) without 6.0 support', () => { @@ -328,7 +328,7 @@ describe('emoji', () => { emojiFixtureMap[emojiKey].unicodeVersion, ); - expect(isSupported).toBeFalsy(); + expect(isSupported).toBe(false); }); it('bomb(6.0) without 6.0 but with 9.0 support', () => { @@ -340,7 +340,7 @@ describe('emoji', () => { emojiFixtureMap[emojiKey].unicodeVersion, ); - expect(isSupported).toBeFalsy(); + expect(isSupported).toBe(false); }); it('construction_worker_tone5(8.0) without skin tone modifier support', () => { @@ -367,7 +367,7 @@ describe('emoji', () => { emojiFixtureMap[emojiKey].unicodeVersion, ); - expect(isSupported).toBeFalsy(); + expect(isSupported).toBe(false); }); it('use native keycap on >=57 chrome', () => { @@ -386,7 +386,7 @@ describe('emoji', () => { emojiFixtureMap[emojiKey].unicodeVersion, ); - expect(isSupported).toBeTruthy(); + expect(isSupported).toBe(true); }); it('fallback keycap on <57 chrome', () => { @@ -405,7 +405,7 @@ describe('emoji', () => { emojiFixtureMap[emojiKey].unicodeVersion, ); - expect(isSupported).toBeFalsy(); + expect(isSupported).toBe(false); }); }); diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js index 6cc363e000b..4cbbb60b74c 100644 --- a/spec/frontend/environments/deployment_spec.js +++ b/spec/frontend/environments/deployment_spec.js @@ -1,4 +1,6 @@ -import { GlCollapse } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlLoadingIcon } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { useFakeDate } from 'helpers/fake_date'; import { stubTransition } from 'helpers/stub_transition'; @@ -8,9 +10,13 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import Deployment from '~/environments/components/deployment.vue'; import Commit from '~/environments/components/commit.vue'; import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue'; -import { resolvedEnvironment } from './graphql/mock_data'; +import createMockApollo from '../__helpers__/mock_apollo_helper'; +import waitForPromises from '../__helpers__/wait_for_promises'; +import getDeploymentDetails from '../../../app/assets/javascripts/environments/graphql/queries/deployment_details.query.graphql'; +import { resolvedEnvironment, resolvedDeploymentDetails } from './graphql/mock_data'; describe('~/environments/components/deployment.vue', () => { + Vue.use(VueApollo); useFakeDate(2022, 0, 8, 16); let deployment; @@ -20,14 +26,23 @@ describe('~/environments/components/deployment.vue', () => { deployment = resolvedEnvironment.lastDeployment; }); - const createWrapper = ({ propsData = {} } = {}) => - mountExtended(Deployment, { + const createWrapper = ({ propsData = {}, options = {} } = {}) => { + const mockApollo = createMockApollo([ + [getDeploymentDetails, jest.fn().mockResolvedValue(resolvedDeploymentDetails)], + ]); + + return mountExtended(Deployment, { + stubs: { transition: stubTransition() }, propsData: { deployment, + visible: true, ...propsData, }, - stubs: { transition: stubTransition() }, + apolloProvider: mockApollo, + provide: { projectPath: '/1' }, + ...options, }); + }; afterEach(() => { wrapper?.destroy(); @@ -102,10 +117,11 @@ describe('~/environments/components/deployment.vue', () => { }); it('shows the short SHA for the commit of the deployment', () => { - const sha = wrapper.findByTitle(__('Commit SHA')); + const sha = wrapper.findByRole('link', { name: __('Commit SHA') }); expect(sha.exists()).toBe(true); expect(sha.text()).toBe(deployment.commit.shortId); + expect(sha.attributes('href')).toBe(deployment.commit.commitPath); }); it('shows the commit icon', () => { @@ -183,29 +199,12 @@ describe('~/environments/components/deployment.vue', () => { }); }); - describe('collapse', () => { - let collapse; - let button; - + describe('details', () => { beforeEach(() => { wrapper = createWrapper(); - collapse = wrapper.findComponent(GlCollapse); - button = wrapper.findComponent({ ref: 'details-toggle' }); }); - it('is collapsed by default', () => { - expect(collapse.attributes('visible')).toBeUndefined(); - expect(button.props('icon')).toBe('expand-down'); - expect(button.text()).toBe(__('Show details')); - }); - - it('opens on click', async () => { - await button.trigger('click'); - - expect(button.text()).toBe(__('Hide details')); - expect(button.props('icon')).toBe('expand-up'); - expect(collapse.attributes('visible')).toBe('visible'); - + it('shows information about the deployment', () => { const username = wrapper.findByRole('link', { name: `@${deployment.user.username}` }); expect(username.attributes('href')).toBe(deployment.user.path); @@ -221,24 +220,43 @@ describe('~/environments/components/deployment.vue', () => { const ref = wrapper.findByRole('link', { name: deployment.ref.name }); expect(ref.attributes('href')).toBe(deployment.ref.refPath); }); + + it('shows information about tags related to the deployment', async () => { + expect(wrapper.findByText(__('Tags')).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + + await waitForPromises(); + + for (let i = 1; i < 6; i += 1) { + const tagName = __(`testTag${i}`); + const testTag = wrapper.findByText(tagName); + expect(testTag.exists()).toBe(true); + expect(testTag.attributes('href')).toBe(`tags/${tagName}`); + } + expect(wrapper.findByText(__('testTag6')).exists()).toBe(false); + expect(wrapper.findByText(__('Tag')).exists()).toBe(false); + // with more than 5 tags, show overflow marker + expect(wrapper.findByText('...').exists()).toBe(true); + }); }); describe('with tagged deployment', () => { - beforeEach(async () => { + beforeEach(() => { wrapper = createWrapper({ propsData: { deployment: { ...deployment, tag: true } } }); - await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click'); }); - it('shows tag instead of branch', () => { - const refLabel = wrapper.findByText(__('Tag')); + it('shows tags instead of branch', () => { + const refLabel = wrapper.findByText(__('Tags')); expect(refLabel.exists()).toBe(true); + + const branchLabel = wrapper.findByText(__('Branch')); + expect(branchLabel.exists()).toBe(false); }); }); describe('with API deployment', () => { - beforeEach(async () => { + beforeEach(() => { wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } }); - await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click'); }); it('shows API instead of a job name', () => { @@ -247,13 +265,12 @@ describe('~/environments/components/deployment.vue', () => { }); }); describe('without a job path', () => { - beforeEach(async () => { + beforeEach(() => { wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: { name: deployment.deployable.name } }, }, }); - await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click'); }); it('shows a span instead of a link', () => { diff --git a/spec/frontend/environments/environment_table_spec.js b/spec/frontend/environments/environment_table_spec.js index 49a643aaac8..a86cfdd56ba 100644 --- a/spec/frontend/environments/environment_table_spec.js +++ b/spec/frontend/environments/environment_table_spec.js @@ -363,7 +363,7 @@ describe('Environment table', () => { }); describe('sortedEnvironments', () => { - it('it should sort children as well', () => { + it('should sort children as well', () => { const mockItems = [ { name: 'production', diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js index 57f98c81124..aff54107d6b 100644 --- a/spec/frontend/environments/environments_app_spec.js +++ b/spec/frontend/environments/environments_app_spec.js @@ -50,6 +50,7 @@ describe('~/environments/components/environments_app.vue', () => { defaultBranchName: 'main', helpPagePath: '/help', projectId: '1', + projectPath: '/1', ...provide, }, apolloProvider, diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js index 7e436476a8f..d246641b94b 100644 --- a/spec/frontend/environments/graphql/mock_data.js +++ b/spec/frontend/environments/graphql/mock_data.js @@ -757,3 +757,41 @@ export const resolvedFolder = { stoppedCount: 0, __typename: 'LocalEnvironmentFolder', }; + +export const resolvedDeploymentDetails = { + data: { + project: { + id: 'gid://gitlab/Project/20', + deployment: { + id: 'gid://gitlab/Deployment/99', + iid: '55', + tags: [ + { + name: 'testTag1', + path: 'tags/testTag1', + }, + { + name: 'testTag2', + path: 'tags/testTag2', + }, + { + name: 'testTag3', + path: 'tags/testTag3', + }, + { + name: 'testTag4', + path: 'tags/testTag4', + }, + { + name: 'testTag5', + path: 'tags/testTag5', + }, + { + name: 'testTag6', + path: 'tags/testTag6', + }, + ], + }, + }, + }, +}; diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js index a151595bf64..76cd09cfb4e 100644 --- a/spec/frontend/environments/new_environment_item_spec.js +++ b/spec/frontend/environments/new_environment_item_spec.js @@ -24,7 +24,7 @@ describe('~/environments/components/new_environment_item.vue', () => { mountExtended(EnvironmentItem, { apolloProvider, propsData: { environment: resolvedEnvironment, ...propsData }, - provide: { helpPagePath: '/help', projectId: '1' }, + provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1' }, stubs: { transition: stubTransition() }, }); diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js index 5a1c1c7714c..2405cb82eac 100644 --- a/spec/frontend/environments/new_environment_spec.js +++ b/spec/frontend/environments/new_environment_spec.js @@ -65,7 +65,7 @@ describe('~/environments/components/new.vue', () => { input | value ${() => name} | ${'test'} ${() => url} | ${'https://example.org'} - `('it changes the value of the input to $value', async ({ input, value }) => { + `('changes the value of the input to $value', async ({ input, value }) => { await input().setValue(value); expect(input().element.value).toBe(value); 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 b7dffbbec04..805ada54509 100644 --- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js @@ -164,19 +164,19 @@ describe('ErrorTrackingList', () => { expect(findSortDropdown().exists()).toBe(true); }); - it('it searches by query', () => { + it('searches by query', () => { findSearchBox().vm.$emit('input', 'search'); findSearchBox().trigger('keyup.enter'); expect(actions.searchByQuery.mock.calls[0][1]).toBe('search'); }); - it('it sorts by fields', () => { + it('sorts by fields', () => { const findSortItem = () => findSortDropdown().find('.dropdown-item'); findSortItem().trigger('click'); expect(actions.sortByField).toHaveBeenCalled(); }); - it('it filters by status', () => { + it('filters by status', () => { const findStatusFilter = () => findStatusFilterDropdown().find('.dropdown-item'); findStatusFilter().trigger('click'); expect(actions.filterByStatus).toHaveBeenCalled(); diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js index 693fcff50ca..0de4277b08a 100644 --- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js +++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js @@ -62,7 +62,7 @@ describe('Stacktrace Entry', () => { ); }); - it('should render only lineNo:columnNO when there is no errorFn ', () => { + it('should render only lineNo:columnNO when there is no errorFn', () => { const extraInfo = { errorLine: 34, errorFn: null, errorColumn: 77 }; mountComponent({ expanded: false, lines: [], ...extraInfo }); const fileHeaderContent = trimText(findFileHeaderContent()); @@ -70,7 +70,7 @@ describe('Stacktrace Entry', () => { expect(fileHeaderContent).toContain(`${extraInfo.errorLine}:${extraInfo.errorColumn}`); }); - it('should render only lineNo when there is no errorColumn ', () => { + it('should render only lineNo when there is no errorColumn', () => { const extraInfo = { errorLine: 34, errorFn: 'errorFn', errorColumn: null }; mountComponent({ expanded: false, lines: [], ...extraInfo }); const fileHeaderContent = trimText(findFileHeaderContent()); diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js index c660c9c4a99..7a714cc1ebc 100644 --- a/spec/frontend/error_tracking_settings/components/app_spec.js +++ b/spec/frontend/error_tracking_settings/components/app_spec.js @@ -76,23 +76,23 @@ describe('error tracking settings app', () => { describe('section', () => { it('renders the form and dropdown', () => { - expect(wrapper.find(ErrorTrackingForm).exists()).toBeTruthy(); - expect(wrapper.find(ProjectDropdown).exists()).toBeTruthy(); + expect(wrapper.findComponent(ErrorTrackingForm).exists()).toBe(true); + expect(wrapper.findComponent(ProjectDropdown).exists()).toBe(true); }); it('renders the Save Changes button', () => { - expect(wrapper.find('.js-error-tracking-button').exists()).toBeTruthy(); + expect(wrapper.find('.js-error-tracking-button').exists()).toBe(true); }); it('enables the button by default', () => { - expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeFalsy(); + expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeUndefined(); }); it('disables the button when saving', async () => { store.state.settingsLoading = true; await nextTick(); - expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeTruthy(); + expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBe('true'); }); }); 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 b44af547658..c9095441d41 100644 --- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js +++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js @@ -42,7 +42,7 @@ describe('error tracking settings project dropdown', () => { describe('empty project list', () => { it('renders the dropdown', () => { expect(wrapper.find('#project-dropdown').exists()).toBe(true); - expect(wrapper.find(GlDropdown).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdown).exists()).toBe(true); }); it('shows helper text', () => { @@ -57,8 +57,8 @@ describe('error tracking settings project dropdown', () => { }); it('does not contain any dropdown items', () => { - expect(wrapper.find(GlDropdownItem).exists()).toBe(false); - expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available'); + expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdown).props('text')).toBe('No projects available'); }); }); @@ -71,12 +71,12 @@ describe('error tracking settings project dropdown', () => { it('renders the dropdown', () => { expect(wrapper.find('#project-dropdown').exists()).toBe(true); - expect(wrapper.find(GlDropdown).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdown).exists()).toBe(true); }); it('contains a number of dropdown items', () => { - expect(wrapper.find(GlDropdownItem).exists()).toBe(true); - expect(wrapper.findAll(GlDropdownItem).length).toBe(2); + expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true); + expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(2); }); }); diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js index e8103df78bc..2b9710c9085 100644 --- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js +++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js @@ -8,7 +8,7 @@ import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdo import axios from '~/lib/utils/axios_utils'; import httpStatusCodes from '~/lib/utils/http_status'; -describe('Feature flags > Environments dropdown ', () => { +describe('Feature flags > Environments dropdown', () => { let wrapper; let mock; const results = ['production', 'staging']; diff --git a/spec/frontend/feature_flags/store/edit/actions_spec.js b/spec/frontend/feature_flags/store/edit/actions_spec.js index b6114cb0c9f..7132e83a940 100644 --- a/spec/frontend/feature_flags/store/edit/actions_spec.js +++ b/spec/frontend/feature_flags/store/edit/actions_spec.js @@ -40,7 +40,7 @@ describe('Feature flags Edit Module actions', () => { }); describe('success', () => { - it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess ', () => { + it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess', () => { const featureFlag = { name: 'name', description: 'description', @@ -75,7 +75,7 @@ describe('Feature flags Edit Module actions', () => { }); describe('error', () => { - it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError ', () => { + it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError', () => { mock.onPut(`${TEST_HOST}/endpoint.json`).replyOnce(500, { message: [] }); return testAction( @@ -154,7 +154,7 @@ describe('Feature flags Edit Module actions', () => { }); describe('success', () => { - it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess ', () => { + it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess', () => { mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 1 }); return testAction( @@ -176,7 +176,7 @@ describe('Feature flags Edit Module actions', () => { }); describe('error', () => { - it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError ', () => { + it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError', () => { mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {}); return testAction( diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js index ce62c3b0473..96a7d868316 100644 --- a/spec/frontend/feature_flags/store/index/actions_spec.js +++ b/spec/frontend/feature_flags/store/index/actions_spec.js @@ -56,7 +56,7 @@ describe('Feature flags actions', () => { }); describe('success', () => { - it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess ', () => { + it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess', () => { mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, getRequestData, {}); return testAction( @@ -78,7 +78,7 @@ describe('Feature flags actions', () => { }); describe('error', () => { - it('dispatches requestFeatureFlags and receiveFeatureFlagsError ', () => { + it('dispatches requestFeatureFlags and receiveFeatureFlagsError', () => { mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {}); return testAction( @@ -153,7 +153,7 @@ describe('Feature flags actions', () => { }); describe('success', () => { - it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess ', () => { + it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess', () => { mock.onPost(`${TEST_HOST}/endpoint.json`).replyOnce(200, rotateData, {}); return testAction( @@ -175,7 +175,7 @@ describe('Feature flags actions', () => { }); describe('error', () => { - it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError ', () => { + it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError', () => { mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {}); return testAction( diff --git a/spec/frontend/feature_flags/store/new/actions_spec.js b/spec/frontend/feature_flags/store/new/actions_spec.js index 1dcd2da1d93..dbe6669c868 100644 --- a/spec/frontend/feature_flags/store/new/actions_spec.js +++ b/spec/frontend/feature_flags/store/new/actions_spec.js @@ -33,7 +33,7 @@ describe('Feature flags New Module Actions', () => { }); describe('success', () => { - it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess ', () => { + it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagSuccess', () => { const actionParams = { name: 'name', description: 'description', @@ -68,7 +68,7 @@ describe('Feature flags New Module Actions', () => { }); describe('error', () => { - it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError ', () => { + it('dispatches requestCreateFeatureFlag and receiveCreateFeatureFlagError', () => { const actionParams = { name: 'name', description: 'description', 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 897ad5ee2bf..91457f10bf8 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 @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue'; import eventHub from '~/filtered_search/event_hub'; import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; @@ -6,12 +6,12 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered describe('Recent Searches Dropdown Content', () => { let wrapper; - const findLocalStorageNote = () => wrapper.find({ ref: 'localStorageNote' }); - const findDropdownItems = () => wrapper.findAll({ ref: 'dropdownItem' }); - const findDropdownNote = () => wrapper.find({ ref: 'dropdownNote' }); + const findLocalStorageNote = () => wrapper.findByTestId('local-storage-note'); + const findDropdownItems = () => wrapper.findAllByTestId('dropdown-item'); + const findDropdownNote = () => wrapper.findByTestId('dropdown-note'); const createComponent = (props) => { - wrapper = shallowMount(RecentSearchesDropdownContent, { + wrapper = shallowMountExtended(RecentSearchesDropdownContent, { propsData: { allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(), items: [], @@ -94,7 +94,7 @@ describe('Recent Searches Dropdown Content', () => { }); it('emits requestClearRecentSearches on Clear resent searches button', () => { - wrapper.find({ ref: 'clearButton' }).trigger('click'); + wrapper.findByTestId('clear-button').trigger('click'); expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled(); }); diff --git a/spec/frontend/filtered_search/droplab/drop_down_spec.js b/spec/frontend/filtered_search/droplab/drop_down_spec.js index f49dbfcf79c..6fbb4394944 100644 --- a/spec/frontend/filtered_search/droplab/drop_down_spec.js +++ b/spec/frontend/filtered_search/droplab/drop_down_spec.js @@ -557,11 +557,11 @@ describe('DropLab DropDown', () => { DropDown.prototype.show.call(testContext.dropdown); }); - it('it should set .list display to block', () => { + it('should set .list display to block', () => { expect(testContext.list.style.display).toBe('block'); }); - it('it should set .hidden to false', () => { + it('should set .hidden to false', () => { expect(testContext.dropdown.hidden).toBe(false); }); @@ -591,11 +591,11 @@ describe('DropLab DropDown', () => { DropDown.prototype.hide.call(testContext.dropdown); }); - it('it should set .list display to none', () => { + it('should set .list display to none', () => { expect(testContext.list.style.display).toBe('none'); }); - it('it should set .hidden to true', () => { + it('should set .hidden to true', () => { expect(testContext.dropdown.hidden).toBe(true); }); }); @@ -648,11 +648,11 @@ describe('DropLab DropDown', () => { DropDown.prototype.destroy.call(testContext.dropdown); }); - it('it should call .hide', () => { + it('should call .hide', () => { expect(testContext.dropdown.hide).toHaveBeenCalled(); }); - it('it should call .removeEventListener', () => { + it('should call .removeEventListener', () => { expect(testContext.list.removeEventListener).toHaveBeenCalledWith( 'click', testContext.eventWrapper.clickEvent, diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb index 75bc8c8df25..7d95c506e6c 100644 --- a/spec/frontend/fixtures/api_merge_requests.rb +++ b/spec/frontend/fixtures/api_merge_requests.rb @@ -7,7 +7,7 @@ RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do include JavaScriptFixturesHelpers let_it_be(:admin) { create(:admin, name: 'root') } - let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' )} + let_it_be(:namespace) { create(:namespace, name: 'gitlab-test' ) } let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') } let_it_be(:early_mrs) do 4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") } diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb index eada2f8e0f7..5acc1095d5c 100644 --- a/spec/frontend/fixtures/api_projects.rb +++ b/spec/frontend/fixtures/api_projects.rb @@ -7,7 +7,7 @@ RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do include JavaScriptFixturesHelpers let(:admin) { create(:admin, name: 'root') } - let(:namespace) { create(:namespace, name: 'gitlab-test' )} + 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') } diff --git a/spec/frontend/fixtures/application_settings.rb b/spec/frontend/fixtures/application_settings.rb index a7a989f31ec..b3ce23c8cd7 100644 --- a/spec/frontend/fixtures/application_settings.rb +++ b/spec/frontend/fixtures/application_settings.rb @@ -8,7 +8,7 @@ RSpec.describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', ty include AdminModeHelper let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project_empty_repo, namespace: namespace, path: 'application-settings') } before do diff --git a/spec/frontend/fixtures/blob.rb b/spec/frontend/fixtures/blob.rb index b2bbdd2749e..54c5b83da3e 100644 --- a/spec/frontend/fixtures/blob.rb +++ b/spec/frontend/fixtures/blob.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } let(:user) { project.first_owner } diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb index b3bb4b8873a..6cda2f0f665 100644 --- a/spec/frontend/fixtures/branches.rb +++ b/spec/frontend/fixtures/branches.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe 'Branches (JavaScript fixtures)' do include JavaScriptFixturesHelpers - let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } let_it_be(:user) { project.first_owner } diff --git a/spec/frontend/fixtures/clusters.rb b/spec/frontend/fixtures/clusters.rb index 49596d98774..426a76f29e0 100644 --- a/spec/frontend/fixtures/clusters.rb +++ b/spec/frontend/fixtures/clusters.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::ClustersController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, :repository, namespace: namespace) } let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } let(:user) { project.first_owner } diff --git a/spec/frontend/fixtures/deploy_keys.rb b/spec/frontend/fixtures/deploy_keys.rb index 154084e0181..24d602216d8 100644 --- a/spec/frontend/fixtures/deploy_keys.rb +++ b/spec/frontend/fixtures/deploy_keys.rb @@ -7,11 +7,11 @@ RSpec.describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :c include AdminModeHelper let(:admin) { create(:admin) } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } - let(:project2) { create(:project, :internal)} - let(:project3) { create(:project, :internal)} - let(:project4) { create(:project, :internal)} + let(:project2) { create(:project, :internal) } + let(:project3) { create(:project, :internal) } + let(:project4) { create(:project, :internal) } before do # Using an admin for these fixtures because they are used for verifying a frontend diff --git a/spec/frontend/fixtures/groups.rb b/spec/frontend/fixtures/groups.rb index ddd436b98c6..9c22ff176ff 100644 --- a/spec/frontend/fixtures/groups.rb +++ b/spec/frontend/fixtures/groups.rb @@ -6,7 +6,7 @@ RSpec.describe 'Groups (JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers let(:user) { create(:user) } - let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre')} + let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre') } before do group.add_owner(user) diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb index cde796497d4..e3d88098841 100644 --- a/spec/frontend/fixtures/issues.rb +++ b/spec/frontend/fixtures/issues.rb @@ -6,7 +6,7 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr include JavaScriptFixturesHelpers let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') } - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') } render_views diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb index 2e15eefdce6..3657a5405a4 100644 --- a/spec/frontend/fixtures/jobs.rb +++ b/spec/frontend/fixtures/jobs.rb @@ -7,7 +7,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do include JavaScriptFixturesHelpers include GraphqlHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, :repository, namespace: namespace, path: 'builds-project') } let(:user) { project.first_owner } let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) } diff --git a/spec/frontend/fixtures/labels.rb b/spec/frontend/fixtures/labels.rb index 6736baed199..2445c9376e2 100644 --- a/spec/frontend/fixtures/labels.rb +++ b/spec/frontend/fixtures/labels.rb @@ -6,7 +6,7 @@ RSpec.describe 'Labels (JavaScript fixtures)' do include JavaScriptFixturesHelpers let(:user) { create(:user) } - let(:group) { create(:group, name: 'frontend-fixtures-group' )} + let(:group) { create(:group, name: 'frontend-fixtures-group' ) } let(:project) { create(:project_empty_repo, namespace: group, path: 'labels-project') } let!(:project_label_bug) { create(:label, project: project, title: 'bug', color: '#FF0000') } diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb index cb4eb43b88d..cbf26a70e5f 100644 --- a/spec/frontend/fixtures/merge_requests.rb +++ b/spec/frontend/fixtures/merge_requests.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') } let(:user) { project.first_owner } diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb index 7f0d650b710..ff4b27844a6 100644 --- a/spec/frontend/fixtures/merge_requests_diffs.rb +++ b/spec/frontend/fixtures/merge_requests_diffs.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') } let(:user) { project.first_owner } let(:merge_request) { create(:merge_request, source_project: project, target_project: project, description: '- [ ] Task List Item') } diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb index d59b01b04af..7f8b3d378d3 100644 --- a/spec/frontend/fixtures/metrics_dashboard.rb +++ b/spec/frontend/fixtures/metrics_dashboard.rb @@ -7,7 +7,7 @@ RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do include MetricsDashboardHelpers let_it_be(:user) { create(:user) } - let_it_be(:namespace) { create(:namespace, name: 'monitoring' )} + let_it_be(:namespace) { create(:namespace, name: 'monitoring' ) } let_it_be(:project) { project_with_dashboard_namespace('.gitlab/dashboards/test.yml', nil, namespace: namespace) } let_it_be(:environment) { create(:environment, id: 1, project: project) } let_it_be(:params) { { environment: environment } } diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb index e155d27920d..5b7a445557e 100644 --- a/spec/frontend/fixtures/pipeline_schedules.rb +++ b/spec/frontend/fixtures/pipeline_schedules.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, :public, :repository) } let(:user) { project.first_owner } let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb index 709e14183df..114db26d6a9 100644 --- a/spec/frontend/fixtures/pipelines.rb +++ b/spec/frontend/fixtures/pipelines.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'pipelines-project') } let_it_be(:commit_without_author) { RepoHelpers.another_sample_commit } diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb index fa7d61df3e8..b9c427c7505 100644 --- a/spec/frontend/fixtures/projects.rb +++ b/spec/frontend/fixtures/projects.rb @@ -8,7 +8,7 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do runners_token = 'runnerstoken:intabulasreferre' - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token, avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) } let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff', avatar: fixture_file_upload('spec/fixtures/dk.png', 'image/png')) } let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) } diff --git a/spec/frontend/fixtures/raw.rb b/spec/frontend/fixtures/raw.rb index b117cfea5fa..7bd5b8c5f6c 100644 --- a/spec/frontend/fixtures/raw.rb +++ b/spec/frontend/fixtures/raw.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe 'Raw files', '(JavaScript fixtures)' do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') } let(:response) { @response } diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb index db1ef67998f..b2da383d657 100644 --- a/spec/frontend/fixtures/search.rb +++ b/spec/frontend/fixtures/search.rb @@ -23,40 +23,41 @@ RSpec.describe SearchController, '(JavaScript fixtures)', type: :controller do let(:namespace) { create(:namespace, name: 'frontend-fixtures') } let(:project) { create(:project, :public, :repository, namespace: namespace, path: 'search-project') } let(:blobs) do - Kaminari.paginate_array([ - Gitlab::Search::FoundBlob.new( - path: 'CHANGELOG', - basename: 'CHANGELOG', - ref: 'master', - data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat", - project: project, - project_id: project.id, - startline: 2), - Gitlab::Search::FoundBlob.new( - path: 'CONTRIBUTING', - basename: 'CONTRIBUTING', - ref: 'master', - data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat", - project: project, - project_id: project.id, - startline: 2), - Gitlab::Search::FoundBlob.new( - path: 'README', - basename: 'README', - ref: 'master', - data: "foo\nSend # this is the highlight\nbaz\nboo\nbat", - project: project, - project_id: project.id, - startline: 2), - Gitlab::Search::FoundBlob.new( - path: 'test', - basename: 'test', - ref: 'master', - data: "foo\nSend # this is the highlight\nbaz\nboo\nbat", - project: project, - project_id: project.id, - startline: 2) - ], + Kaminari.paginate_array( + [ + Gitlab::Search::FoundBlob.new( + path: 'CHANGELOG', + basename: 'CHANGELOG', + ref: 'master', + data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat", + project: project, + project_id: project.id, + startline: 2), + Gitlab::Search::FoundBlob.new( + path: 'CONTRIBUTING', + basename: 'CONTRIBUTING', + ref: 'master', + data: "hello\nworld\nfoo\nSend # this is the highligh\nbaz\nboo\nbat", + project: project, + project_id: project.id, + startline: 2), + Gitlab::Search::FoundBlob.new( + path: 'README', + basename: 'README', + ref: 'master', + data: "foo\nSend # this is the highlight\nbaz\nboo\nbat", + project: project, + project_id: project.id, + startline: 2), + Gitlab::Search::FoundBlob.new( + path: 'test', + basename: 'test', + ref: 'master', + data: "foo\nSend # this is the highlight\nbaz\nboo\nbat", + project: project, + project_id: project.id, + startline: 2) + ], total_count: 4, limit: 4, offset: 0) diff --git a/spec/frontend/fixtures/snippet.rb b/spec/frontend/fixtures/snippet.rb index f05ff3ee269..58d4bc5c1f3 100644 --- a/spec/frontend/fixtures/snippet.rb +++ b/spec/frontend/fixtures/snippet.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe SnippetsController, '(JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') } let(:user) { project.first_owner } let(:snippet) { create(:personal_snippet, :public, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: user) } diff --git a/spec/frontend/fixtures/startup_css.rb b/spec/frontend/fixtures/startup_css.rb index cf7383fa6ca..bd2d63a1827 100644 --- a/spec/frontend/fixtures/startup_css.rb +++ b/spec/frontend/fixtures/startup_css.rb @@ -69,11 +69,25 @@ RSpec.describe 'Startup CSS fixtures', type: :controller do it_behaves_like 'startup css project fixtures', 'dark' end - describe RegistrationsController, '(Startup CSS fixtures)', type: :controller do + describe SessionsController, '(Startup CSS fixtures)', type: :controller do + include DeviseHelpers + + before do + set_devise_mapping(context: request) + end + it 'startup_css/sign-in.html' do get :new expect(response).to be_successful end + + it 'startup_css/sign-in-old.html' do + stub_feature_flags(restyle_login_page: false) + + get :new + + expect(response).to be_successful + end end end diff --git a/spec/frontend/fixtures/todos.rb b/spec/frontend/fixtures/todos.rb index 7dce09e8f49..d934396f803 100644 --- a/spec/frontend/fixtures/todos.rb +++ b/spec/frontend/fixtures/todos.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe 'Todos (JavaScript fixtures)' do include JavaScriptFixturesHelpers - let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:namespace) { create(:namespace, name: 'frontend-fixtures' ) } let(:project) { create(:project_empty_repo, namespace: namespace, path: 'todos-project') } let(:user) { project.first_owner } let(:issue_1) { create(:issue, title: 'issue_1', project: project) } diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js index 6cd32ff6b40..e26c52f0bf7 100644 --- a/spec/frontend/flash_spec.js +++ b/spec/frontend/flash_spec.js @@ -36,7 +36,7 @@ describe('Flash', () => { hideFlash(el, false); expect(el.style.opacity).toBe(''); - expect(el.style.transition).toBeFalsy(); + expect(el.style.transition).toHaveLength(0); }); it('removes element after transitionend', () => { 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 eef5dc86c1a..e6673fa78ec 100644 --- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js +++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js @@ -1,7 +1,7 @@ import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { trimText } from 'helpers/text_helper'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; @@ -16,18 +16,18 @@ describe('FrequentItemsListItemComponent', () => { let trackingSpy; let store; - const findTitle = () => wrapper.find({ ref: 'frequentItemsItemTitle' }); + const findTitle = () => wrapper.findByTestId('frequent-items-item-title'); const findAvatar = () => wrapper.findComponent(ProjectAvatar); - const findAllTitles = () => wrapper.findAll({ ref: 'frequentItemsItemTitle' }); - const findNamespace = () => wrapper.find({ ref: 'frequentItemsItemNamespace' }); + const findAllTitles = () => wrapper.findAllByTestId('frequent-items-item-title'); + const findNamespace = () => wrapper.findByTestId('frequent-items-item-namespace'); const findAllButtons = () => wrapper.findAllComponents(GlButton); - const findAllNamespace = () => wrapper.findAll({ ref: 'frequentItemsItemNamespace' }); + const findAllNamespace = () => wrapper.findAllByTestId('frequent-items-item-namespace'); const findAllAvatars = () => wrapper.findAllComponents(ProjectAvatar); const findAllMetadataContainers = () => - wrapper.findAll({ ref: 'frequentItemsItemMetadataContainer' }); + wrapper.findAllByTestId('frequent-items-item-metadata-container'); const createComponent = (props = {}) => { - wrapper = shallowMount(frequentItemsListItemComponent, { + wrapper = shallowMountExtended(frequentItemsListItemComponent, { store, propsData: { itemId: mockProject.id, 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 beaab1913d0..9f08a432a3d 100644 --- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js +++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js @@ -1,6 +1,6 @@ -import { mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue'; import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue'; import { createStore } from '~/frequent_items/store'; @@ -12,7 +12,7 @@ describe('FrequentItemsListComponent', () => { let wrapper; const createComponent = (props = {}) => { - wrapper = mount(frequentItemsListComponent, { + wrapper = mountExtended(frequentItemsListComponent, { store: createStore(), propsData: { namespace: 'projects', @@ -94,8 +94,8 @@ describe('FrequentItemsListComponent', () => { await nextTick(); expect(wrapper.classes('frequent-items-list-container')).toBe(true); - expect(wrapper.findAll({ ref: 'frequentItemsList' })).toHaveLength(1); - expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(5); + expect(wrapper.findAllByTestId('frequent-items-list')).toHaveLength(1); + expect(wrapper.findAllComponents(frequentItemsListItemComponent)).toHaveLength(5); }); it('should render component element with empty message', async () => { @@ -105,7 +105,7 @@ describe('FrequentItemsListComponent', () => { await nextTick(); expect(wrapper.vm.$el.querySelectorAll('li.section-empty')).toHaveLength(1); - expect(wrapper.findAll(frequentItemsListItemComponent)).toHaveLength(0); + expect(wrapper.findAllComponents(frequentItemsListItemComponent)).toHaveLength(0); }); }); }); diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js index d0a4cf70f5f..94fc97b82c2 100644 --- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js +++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js @@ -23,7 +23,7 @@ describe('FrequentItemsSearchInputComponent', () => { }, }); - const findSearchBoxByType = () => wrapper.find(GlSearchBoxByType); + const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); beforeEach(() => { store = createStore(); diff --git a/spec/frontend/frequent_items/utils_spec.js b/spec/frontend/frequent_items/utils_spec.js index 33c655a6ffd..8d4c89bd48f 100644 --- a/spec/frontend/frequent_items/utils_spec.js +++ b/spec/frontend/frequent_items/utils_spec.js @@ -10,25 +10,25 @@ import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_ describe('Frequent Items utils spec', () => { describe('isMobile', () => { - it('returns true when the screen is medium ', () => { + it('returns true when the screen is medium', () => { jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md'); expect(isMobile()).toBe(true); }); - it('returns true when the screen is small ', () => { + it('returns true when the screen is small', () => { jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm'); expect(isMobile()).toBe(true); }); - it('returns true when the screen is extra-small ', () => { + it('returns true when the screen is extra-small', () => { jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs'); expect(isMobile()).toBe(true); }); - it('returns false when the screen is larger than medium ', () => { + it('returns false when the screen is larger than medium', () => { jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg'); expect(isMobile()).toBe(false); diff --git a/spec/frontend/google_cloud/databases/panel_spec.js b/spec/frontend/google_cloud/databases/panel_spec.js index 490c0136651..e6a0d74f348 100644 --- a/spec/frontend/google_cloud/databases/panel_spec.js +++ b/spec/frontend/google_cloud/databases/panel_spec.js @@ -2,6 +2,8 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import Panel from '~/google_cloud/databases/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/databases/service_table.vue'; +import InstanceTable from '~/google_cloud/databases/cloudsql/instance_table.vue'; describe('google_cloud/databases/panel', () => { let wrapper; @@ -10,6 +12,11 @@ describe('google_cloud/databases/panel', () => { configurationUrl: 'configuration-url', deploymentsUrl: 'deployments-url', databasesUrl: 'databases-url', + cloudsqlPostgresUrl: 'cloudsql-postgres-url', + cloudsqlMysqlUrl: 'cloudsql-mysql-url', + cloudsqlSqlserverUrl: 'cloudsql-sqlserver-url', + cloudsqlInstances: [], + emptyIllustrationUrl: 'empty-illustration-url', }; beforeEach(() => { @@ -33,4 +40,14 @@ describe('google_cloud/databases/panel', () => { expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl); expect(target.props('databasesUrl')).toBe(props.databasesUrl); }); + + it('contains Databases service table', () => { + const target = wrapper.findComponent(ServiceTable); + expect(target.exists()).toBe(true); + }); + + it('contains CloudSQL instance table', () => { + const target = wrapper.findComponent(InstanceTable); + expect(target.exists()).toBe(true); + }); }); diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js index 6a7eb1fd9f1..ec9e1ef8e5f 100644 --- a/spec/frontend/google_tag_manager/index_spec.js +++ b/spec/frontend/google_tag_manager/index_spec.js @@ -8,13 +8,13 @@ import { trackSaasTrialSubmit, trackSaasTrialSkip, trackSaasTrialGroup, - trackSaasTrialProject, trackSaasTrialGetStarted, trackTrialAcceptTerms, trackCheckout, trackTransaction, trackAddToCartUsageTab, getNamespaceId, + trackCompanyForm, } from '~/google_tag_manager'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { logError } from '~/lib/logger'; @@ -149,9 +149,6 @@ describe('~/google_tag_manager/index', () => { createTestCase(trackSaasTrialGroup, { forms: [{ cls: 'js-saas-trial-group', expectation: { event: 'saasTrialGroup' } }], }), - createTestCase(trackSaasTrialProject, { - forms: [{ id: 'new_project', expectation: { event: 'saasTrialProject' } }], - }), createTestCase(trackProjectImport, { links: [ { @@ -440,6 +437,34 @@ describe('~/google_tag_manager/index', () => { }); }); }); + + describe('when trackCompanyForm is invoked', () => { + it('with an ultimate trial', () => { + expect(spy).not.toHaveBeenCalled(); + + trackCompanyForm('ultimate_trial'); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith({ + event: 'aboutYourCompanyFormSubmit', + aboutYourCompanyType: 'ultimate_trial', + }); + expect(logError).not.toHaveBeenCalled(); + }); + + it('with a free account', () => { + expect(spy).not.toHaveBeenCalled(); + + trackCompanyForm('free_account'); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith({ + event: 'aboutYourCompanyFormSubmit', + aboutYourCompanyType: 'free_account', + }); + expect(logError).not.toHaveBeenCalled(); + }); + }); }); describe.each([ @@ -452,11 +477,11 @@ describe('~/google_tag_manager/index', () => { }); it('no ops', () => { - setHTMLFixture(createHTML({ forms: [{ id: 'new_project' }] })); + setHTMLFixture(createHTML({ forms: [{ cls: 'js-saas-trial-group' }] })); - trackSaasTrialProject(); + trackSaasTrialGroup(); - triggerEvent('#new_project', 'submit'); + triggerEvent('.js-saas-trial-group', 'submit'); expect(spy).not.toHaveBeenCalled(); expect(logError).not.toHaveBeenCalled(); @@ -477,11 +502,11 @@ describe('~/google_tag_manager/index', () => { }); it('logs error', () => { - setHTMLFixture(createHTML({ forms: [{ id: 'new_project' }] })); + setHTMLFixture(createHTML({ forms: [{ cls: 'js-saas-trial-group' }] })); - trackSaasTrialProject(); + trackSaasTrialGroup(); - triggerEvent('#new_project', 'submit'); + triggerEvent('.js-saas-trial-group', 'submit'); expect(logError).toHaveBeenCalledWith( 'Unexpected error while pushing to dataLayer', diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js index a6bbea648d2..a4a7530184d 100644 --- a/spec/frontend/groups/components/app_spec.js +++ b/spec/frontend/groups/components/app_spec.js @@ -40,7 +40,7 @@ describe('AppComponent', () => { const store = new GroupsStore({ hideProjects: false }); const service = new GroupsService(mockEndpoint); - const createShallowComponent = ({ propsData = {}, provide = {} } = {}) => { + const createShallowComponent = ({ propsData = {} } = {}) => { store.state.pageInfo = mockPageInfo; wrapper = shallowMount(appComponent, { propsData: { @@ -53,10 +53,6 @@ describe('AppComponent', () => { mocks: { $toast, }, - provide: { - renderEmptyState: false, - ...provide, - }, }); vm = wrapper.vm; }; @@ -402,8 +398,7 @@ describe('AppComponent', () => { ({ action, groups, fromSearch, renderEmptyState, expected }) => { it(expected ? 'renders empty state' : 'does not render empty state', async () => { createShallowComponent({ - propsData: { action }, - provide: { renderEmptyState }, + propsData: { action, renderEmptyState }, }); vm.updateGroups(groups, fromSearch); @@ -420,7 +415,6 @@ describe('AppComponent', () => { it('renders legacy empty state', async () => { createShallowComponent({ propsData: { action: 'subgroups_and_projects' }, - provide: { renderEmptyState: false }, }); vm.updateGroups([], false); @@ -481,7 +475,7 @@ describe('AppComponent', () => { it('should render loading icon', async () => { vm.isLoading = true; await nextTick(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('should render groups tree', async () => { @@ -494,7 +488,7 @@ describe('AppComponent', () => { it('renders modal confirmation dialog', () => { createShallowComponent(); - const findGlModal = wrapper.find(GlModal); + const findGlModal = wrapper.findComponent(GlModal); expect(findGlModal.exists()).toBe(true); expect(findGlModal.attributes('title')).toBe('Are you sure?'); diff --git a/spec/frontend/groups/components/empty_state_spec.js b/spec/frontend/groups/components/empty_state_spec.js index c0e71e814d0..fbeaa32b1ec 100644 --- a/spec/frontend/groups/components/empty_state_spec.js +++ b/spec/frontend/groups/components/empty_state_spec.js @@ -68,7 +68,7 @@ describe('EmptyState', () => { it('renders empty state', () => { createComponent({ provide: { canCreateSubgroups: false, canCreateProjects: false } }); - expect(wrapper.find(GlEmptyState).props()).toMatchObject({ + expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ title: EmptyState.i18n.withoutLinks.title, description: EmptyState.i18n.withoutLinks.description, svgPath: defaultProvide.emptySubgroupIllustration, diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js index 9906f62878f..3aa66644c19 100644 --- a/spec/frontend/groups/components/group_item_spec.js +++ b/spec/frontend/groups/components/group_item_spec.js @@ -8,9 +8,9 @@ import { getGroupItemMicrodata } from '~/groups/store/utils'; import * as urlUtilities from '~/lib/utils/url_utility'; import { ITEM_TYPE } from '~/groups/constants'; import { - VISIBILITY_LEVEL_PRIVATE, - VISIBILITY_LEVEL_INTERNAL, - VISIBILITY_LEVEL_PUBLIC, + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, } from '~/visibility_level/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -19,7 +19,7 @@ import { mockParentGroupItem, mockChildren } from '../mock_data'; const createComponent = ( propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] }, provide = { - currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE, + currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE_STRING, }, ) => { return mountExtended(GroupItem, { @@ -274,7 +274,7 @@ describe('GroupItemComponent', () => { ${'itemscope'} | ${'itemscope'} ${'itemtype'} | ${'https://schema.org/Organization'} ${'itemprop'} | ${'subOrganization'} - `('it does set correct $attr', ({ attr, value } = {}) => { + `('does set correct $attr', ({ attr, value } = {}) => { expect(wrapper.attributes(attr)).toBe(value); }); @@ -283,7 +283,7 @@ describe('GroupItemComponent', () => { ${'img'} | ${'logo'} ${'[data-testid="group-name"]'} | ${'name'} ${'[data-testid="group-description"]'} | ${'description'} - `('it does set correct $selector', ({ selector, propValue } = {}) => { + `('does set correct $selector', ({ selector, propValue } = {}) => { expect(wrapper.find(selector).attributes('itemprop')).toBe(propValue); }); }); @@ -320,16 +320,16 @@ describe('GroupItemComponent', () => { describe('when showing projects', () => { describe.each` - itemVisibility | currentGroupVisibility | isPopoverShown - ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} - ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} - ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PUBLIC} | ${false} - ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_PRIVATE} | ${false} - ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_PRIVATE} | ${true} - ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_PRIVATE} | ${true} - ${VISIBILITY_LEVEL_PRIVATE} | ${VISIBILITY_LEVEL_INTERNAL} | ${false} - ${VISIBILITY_LEVEL_INTERNAL} | ${VISIBILITY_LEVEL_INTERNAL} | ${false} - ${VISIBILITY_LEVEL_PUBLIC} | ${VISIBILITY_LEVEL_INTERNAL} | ${true} + itemVisibility | currentGroupVisibility | isPopoverShown + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${false} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${false} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${true} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${true} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${false} + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${true} `( 'when item visibility is $itemVisibility and parent group visibility is $currentGroupVisibility', ({ itemVisibility, currentGroupVisibility, isPopoverShown }) => { @@ -374,7 +374,7 @@ describe('GroupItemComponent', () => { wrapper = createComponent({ group: { ...mockParentGroupItem, - visibility: VISIBILITY_LEVEL_PUBLIC, + visibility: VISIBILITY_LEVEL_PUBLIC_STRING, type: ITEM_TYPE.PROJECT, }, parentGroup: mockChildren[0], diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js index 6c1eb373b7e..866868eff36 100644 --- a/spec/frontend/groups/components/groups_spec.js +++ b/spec/frontend/groups/components/groups_spec.js @@ -6,7 +6,7 @@ import GroupItemComponent from '~/groups/components/group_item.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import GroupsComponent from '~/groups/components/groups.vue'; import eventHub from '~/groups/event_hub'; -import { VISIBILITY_LEVEL_PRIVATE } from '~/visibility_level/constants'; +import { VISIBILITY_LEVEL_PRIVATE_STRING } from '~/visibility_level/constants'; import { mockGroups, mockPageInfo } from '../mock_data'; describe('GroupsComponent', () => { @@ -26,7 +26,7 @@ describe('GroupsComponent', () => { ...propsData, }, provide: { - currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE, + currentGroupVisibility: VISIBILITY_LEVEL_PRIVATE_STRING, }, }); }; diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js index 1924f400861..d25b45bd662 100644 --- a/spec/frontend/groups/components/invite_members_banner_spec.js +++ b/spec/frontend/groups/components/invite_members_banner_spec.js @@ -71,7 +71,7 @@ describe('InviteMembersBanner', () => { describe('when the button is clicked', () => { beforeEach(() => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - wrapper.find(GlBanner).vm.$emit('primary'); + wrapper.findComponent(GlBanner).vm.$emit('primary'); }); it('calls openModal through the eventHub', () => { @@ -92,7 +92,7 @@ describe('InviteMembersBanner', () => { mockAxios.onPost(provide.calloutsPath).replyOnce(200); const dismissEvent = 'invite_members_banner_dismissed'; - wrapper.find(GlBanner).vm.$emit('close'); + wrapper.findComponent(GlBanner).vm.$emit('close'); expect(trackingSpy).toHaveBeenCalledWith(trackCategory, dismissEvent, { label: provide.trackLabel, @@ -102,7 +102,7 @@ describe('InviteMembersBanner', () => { describe('rendering', () => { const findBanner = () => { - return wrapper.find(GlBanner); + return wrapper.findComponent(GlBanner); }; beforeEach(() => { @@ -132,16 +132,16 @@ describe('InviteMembersBanner', () => { }); it('should render the banner when not dismissed', () => { - expect(wrapper.find(GlBanner).exists()).toBe(true); + expect(wrapper.findComponent(GlBanner).exists()).toBe(true); }); it('should close the banner when dismiss is clicked', async () => { mockAxios.onPost(provide.calloutsPath).replyOnce(200); - expect(wrapper.find(GlBanner).exists()).toBe(true); - wrapper.find(GlBanner).vm.$emit('close'); + expect(wrapper.findComponent(GlBanner).exists()).toBe(true); + wrapper.findComponent(GlBanner).vm.$emit('close'); await nextTick(); - expect(wrapper.find(GlBanner).exists()).toBe(false); + expect(wrapper.findComponent(GlBanner).exists()).toBe(false); }); }); }); diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js index 4bf92bb5642..2333f04bb2e 100644 --- a/spec/frontend/groups/components/item_caret_spec.js +++ b/spec/frontend/groups/components/item_caret_spec.js @@ -22,8 +22,8 @@ describe('ItemCaret', () => { } }); - const findAllGlIcons = () => wrapper.findAll(GlIcon); - const findGlIcon = () => wrapper.find(GlIcon); + const findAllGlIcons = () => wrapper.findAllComponents(GlIcon); + const findGlIcon = () => wrapper.findComponent(GlIcon); describe('template', () => { it('renders component template correctly', () => { diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js index fdc267bc14a..0c2912adc66 100644 --- a/spec/frontend/groups/components/item_stats_spec.js +++ b/spec/frontend/groups/components/item_stats_spec.js @@ -24,7 +24,7 @@ describe('ItemStats', () => { } }); - const findItemStatsValue = () => wrapper.find(ItemStatsValue); + const findItemStatsValue = () => wrapper.findComponent(ItemStatsValue); describe('template', () => { it('renders component container element correctly', () => { diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js index 98186120a81..b9db83c7dd7 100644 --- a/spec/frontend/groups/components/item_stats_value_spec.js +++ b/spec/frontend/groups/components/item_stats_value_spec.js @@ -25,7 +25,7 @@ describe('ItemStatsValue', () => { } }); - const findGlIcon = () => wrapper.find(GlIcon); + const findGlIcon = () => wrapper.findComponent(GlIcon); const findStatValue = () => wrapper.find('[data-testid="itemStatValue"]'); describe('template', () => { diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js index f3652f1a410..aa00e82150b 100644 --- a/spec/frontend/groups/components/item_type_icon_spec.js +++ b/spec/frontend/groups/components/item_type_icon_spec.js @@ -23,7 +23,7 @@ describe('ItemTypeIcon', () => { } }); - const findGlIcon = () => wrapper.find(GlIcon); + const findGlIcon = () => wrapper.findComponent(GlIcon); describe('template', () => { it('renders component template correctly', () => { diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js new file mode 100644 index 00000000000..352bf25b84f --- /dev/null +++ b/spec/frontend/groups/components/overview_tabs_spec.js @@ -0,0 +1,187 @@ +import { GlTab } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import OverviewTabs from '~/groups/components/overview_tabs.vue'; +import GroupsApp from '~/groups/components/app.vue'; +import GroupsStore from '~/groups/store/groups_store'; +import GroupsService from '~/groups/service/groups_service'; +import { createRouter } from '~/groups/init_overview_tabs'; +import { + ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + ACTIVE_TAB_SHARED, + ACTIVE_TAB_ARCHIVED, +} from '~/groups/constants'; +import axios from '~/lib/utils/axios_utils'; + +const router = createRouter(); + +describe('OverviewTabs', () => { + let wrapper; + + const endpoints = { + subgroups_and_projects: '/groups/foobar/-/children.json', + shared: '/groups/foobar/-/shared_projects.json', + archived: '/groups/foobar/-/children.json?archived=only', + }; + + const routerMock = { + push: jest.fn(), + }; + + const createComponent = async ({ + route = { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } }, + } = {}) => { + wrapper = mountExtended(OverviewTabs, { + router, + provide: { + endpoints, + }, + mocks: { $route: route, $router: routerMock }, + }); + + await nextTick(); + }; + + const findTabPanels = () => wrapper.findAllComponents(GlTab); + const findTab = (name) => wrapper.findByRole('tab', { name }); + const findSelectedTab = () => wrapper.findByRole('tab', { selected: true }); + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(async () => { + // eslint-disable-next-line no-new + new AxiosMockAdapter(axios); + }); + + it('renders `Subgroups and projects` tab with `GroupsApp` component', async () => { + await createComponent(); + + const tabPanel = findTabPanels().at(0); + + expect(tabPanel.vm.$attrs).toMatchObject({ + title: OverviewTabs.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS], + lazy: false, + }); + expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ + action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + store: new GroupsStore({ showSchemaMarkup: true }), + service: new GroupsService(endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]), + hideProjects: false, + renderEmptyState: true, + }); + }); + + it('renders `Shared projects` tab and renders `GroupsApp` component after clicking tab', async () => { + await createComponent(); + + const tabPanel = findTabPanels().at(1); + + expect(tabPanel.vm.$attrs).toMatchObject({ + title: OverviewTabs.i18n[ACTIVE_TAB_SHARED], + lazy: true, + }); + + await findTab(OverviewTabs.i18n[ACTIVE_TAB_SHARED]).trigger('click'); + + expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ + action: ACTIVE_TAB_SHARED, + store: new GroupsStore(), + service: new GroupsService(endpoints[ACTIVE_TAB_SHARED]), + hideProjects: false, + renderEmptyState: false, + }); + + expect(tabPanel.vm.$attrs.lazy).toBe(false); + }); + + it('renders `Archived projects` tab and renders `GroupsApp` component after clicking tab', async () => { + await createComponent(); + + const tabPanel = findTabPanels().at(2); + + expect(tabPanel.vm.$attrs).toMatchObject({ + title: OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED], + lazy: true, + }); + + await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click'); + + expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({ + action: ACTIVE_TAB_ARCHIVED, + store: new GroupsStore(), + service: new GroupsService(endpoints[ACTIVE_TAB_ARCHIVED]), + hideProjects: false, + renderEmptyState: false, + }); + + expect(tabPanel.vm.$attrs.lazy).toBe(false); + }); + + describe.each([ + [ + { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo/bar/baz' } }, + OverviewTabs.i18n[ACTIVE_TAB_SHARED], + { + name: ACTIVE_TAB_SHARED, + params: { group: ['foo', 'bar', 'baz'] }, + }, + ], + [ + { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: ['foo', 'bar', 'baz'] } }, + OverviewTabs.i18n[ACTIVE_TAB_SHARED], + { + name: ACTIVE_TAB_SHARED, + params: { group: ['foo', 'bar', 'baz'] }, + }, + ], + [ + { name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, params: { group: 'foo' } }, + OverviewTabs.i18n[ACTIVE_TAB_SHARED], + { + name: ACTIVE_TAB_SHARED, + params: { group: ['foo'] }, + }, + ], + [ + { name: ACTIVE_TAB_SHARED, params: { group: 'foo/bar' } }, + OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED], + { + name: ACTIVE_TAB_ARCHIVED, + params: { group: ['foo', 'bar'] }, + }, + ], + [ + { name: ACTIVE_TAB_SHARED, params: { group: 'foo/bar' } }, + OverviewTabs.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS], + { + name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + params: { group: ['foo', 'bar'] }, + }, + ], + [ + { name: ACTIVE_TAB_ARCHIVED, params: { group: ['foo'] } }, + OverviewTabs.i18n[ACTIVE_TAB_SHARED], + { + name: ACTIVE_TAB_SHARED, + params: { group: ['foo'] }, + }, + ], + ])('when current route is %j', (currentRoute, tabToClick, expectedRoute) => { + beforeEach(async () => { + await createComponent({ route: currentRoute }); + }); + + it(`sets ${OverviewTabs.i18n[currentRoute.name]} as active tab`, () => { + expect(findSelectedTab().text()).toBe(OverviewTabs.i18n[currentRoute.name]); + }); + + it(`pushes expected route when ${tabToClick} tab is clicked`, async () => { + await findTab(tabToClick).trigger('click'); + + expect(routerMock.push).toHaveBeenCalledWith(expectedRoute); + }); + }); +}); diff --git a/spec/frontend/groups/components/visibility_level_dropdown_spec.js b/spec/frontend/groups/components/visibility_level_dropdown_spec.js deleted file mode 100644 index 61b7bbb0833..00000000000 --- a/spec/frontend/groups/components/visibility_level_dropdown_spec.js +++ /dev/null @@ -1,70 +0,0 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Component from '~/groups/components/visibility_level_dropdown.vue'; - -describe('Visibility Level Dropdown', () => { - let wrapper; - - const options = [ - { level: 0, label: 'Private', description: 'Private description' }, - { level: 20, label: 'Public', description: 'Public description' }, - ]; - const defaultLevel = 0; - - const createComponent = (propsData) => { - wrapper = shallowMount(Component, { - propsData, - }); - }; - - beforeEach(() => { - createComponent({ - visibilityLevelOptions: options, - defaultLevel, - }); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - const hiddenInputValue = () => - wrapper.find("input[name='group[visibility_level]']").attributes('value'); - const dropdownText = () => wrapper.find(GlDropdown).props('text'); - const findDropdownItems = () => - wrapper.findAll(GlDropdownItem).wrappers.map((option) => ({ - text: option.text(), - secondaryText: option.props('secondaryText'), - })); - - describe('Default values', () => { - it('sets the value of the hidden input to the default value', () => { - expect(hiddenInputValue()).toBe(options[0].level.toString()); - }); - - it('sets the text of the dropdown to the default value', () => { - expect(dropdownText()).toBe(options[0].label); - }); - - it('shows all dropdown options', () => { - expect(findDropdownItems()).toEqual( - options.map(({ label, description }) => ({ text: label, secondaryText: description })), - ); - }); - }); - - describe('Selecting an option', () => { - beforeEach(() => { - wrapper.findAll(GlDropdownItem).at(1).vm.$emit('click'); - }); - - it('sets the value of the hidden input to the selected value', () => { - expect(hiddenInputValue()).toBe(options[1].level.toString()); - }); - - it('sets the text of the dropdown to the selected value', () => { - expect(dropdownText()).toBe(options[1].label); - }); - }); -}); diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js index 9515ca8c812..40c1843d461 100644 --- a/spec/frontend/header_search/init_spec.js +++ b/spec/frontend/header_search/init_spec.js @@ -24,10 +24,10 @@ describe('Header Search EventListener', () => { const addEventListenerSpy = jest.spyOn(searchInputBox, 'addEventListener'); initHeaderSearch(); - expect(addEventListenerSpy).toBeCalledTimes(2); + expect(addEventListenerSpy).toHaveBeenCalledTimes(2); }); - it('removes event listener ', async () => { + it('removes event listener', async () => { const searchInputBox = document?.querySelector('#search'); const removeEventListenerSpy = jest.spyOn(searchInputBox, 'removeEventListener'); jest.mock('~/header_search', () => ({ initHeaderSearchApp: jest.fn() })); @@ -39,7 +39,7 @@ describe('Header Search EventListener', () => { [cleanEventListeners], ); - expect(removeEventListenerSpy).toBeCalledTimes(2); + expect(removeEventListenerSpy).toHaveBeenCalledTimes(2); }); it('attaches new vue dropdown when feature flag is enabled', async () => { @@ -53,7 +53,7 @@ describe('Header Search EventListener', () => { () => {}, ); - expect(mockVueApp).toBeCalled(); + expect(mockVueApp).toHaveBeenCalled(); }); it('attaches old vue dropdown when feature flag is disabled', async () => { @@ -69,6 +69,6 @@ describe('Header Search EventListener', () => { () => {}, ); - expect(mockLegacyApp).toBeCalled(); + expect(mockLegacyApp).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js index 8ccd7fb17e3..3a8624ad9dd 100644 --- a/spec/frontend/header_search/mock_data.js +++ b/spec/frontend/header_search/mock_data.js @@ -222,6 +222,20 @@ export const MOCK_AUTOCOMPLETE_OPTIONS = [ ]; export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [ + { + category: 'Groups', + data: [ + { + category: 'Groups', + html_id: 'autocomplete-Groups-1', + + id: 1, + label: 'Gitlab Org / MockGroup1', + value: 'MockGroup1', + url: 'group/1', + }, + ], + }, { category: 'Projects', data: [ @@ -245,20 +259,6 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [ }, ], }, - { - category: 'Groups', - data: [ - { - category: 'Groups', - html_id: 'autocomplete-Groups-1', - - id: 1, - label: 'Gitlab Org / MockGroup1', - value: 'MockGroup1', - url: 'group/1', - }, - ], - }, { category: 'Help', data: [ @@ -274,6 +274,14 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [ ]; export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [ + { + category: 'Groups', + html_id: 'autocomplete-Groups-1', + id: 1, + label: 'Gitlab Org / MockGroup1', + value: 'MockGroup1', + url: 'group/1', + }, { category: 'Projects', html_id: 'autocomplete-Projects-0', @@ -290,14 +298,6 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [ value: 'MockProject2', url: 'project/2', }, - { - category: 'Groups', - html_id: 'autocomplete-Groups-1', - id: 1, - label: 'Gitlab Org / MockGroup1', - value: 'MockGroup1', - url: 'group/1', - }, { category: 'Help', html_id: 'autocomplete-Help-3', diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js index 1748d89a6d3..1ae149128ca 100644 --- a/spec/frontend/header_search/store/actions_spec.js +++ b/spec/frontend/header_search/store/actions_spec.js @@ -2,9 +2,18 @@ import MockAdapter from 'axios-mock-adapter'; import testAction from 'helpers/vuex_action_helper'; import * as actions from '~/header_search/store/actions'; import * as types from '~/header_search/store/mutation_types'; -import createState from '~/header_search/store/state'; +import initState from '~/header_search/store/state'; import axios from '~/lib/utils/axios_utils'; -import { MOCK_SEARCH, MOCK_AUTOCOMPLETE_OPTIONS_RES } from '../mock_data'; +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'; jest.mock('~/flash'); @@ -12,10 +21,15 @@ describe('Header Search Store Actions', () => { let state; let mock; - beforeEach(() => { - state = createState({}); - mock = new MockAdapter(axios); - }); + 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; @@ -24,12 +38,14 @@ describe('Header Search Store Actions', () => { describe.each` axiosMock | type | expectedMutations - ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]} - ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]} + ${{ method: 'onGet', code: 200, 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: 500, 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(() => { - mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res); + state = createState({}); + mock = new MockAdapter(axios); + mock[axiosMock.method]().reply(axiosMock.code, axiosMock.res); }); it(`should dispatch the correct mutations`, () => { return testAction({ @@ -41,7 +57,35 @@ describe('Header Search Store Actions', () => { }); }); + 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, @@ -52,6 +96,10 @@ describe('Header Search Store Actions', () => { }); describe('setSearch', () => { + beforeEach(() => { + state = createState({}); + }); + it('calls the SET_SEARCH mutation', () => { return testAction({ action: actions.setSearch, diff --git a/spec/frontend/header_search/store/getters_spec.js b/spec/frontend/header_search/store/getters_spec.js index c76be3c0360..a1d9481b5cc 100644 --- a/spec/frontend/header_search/store/getters_spec.js +++ b/spec/frontend/header_search/store/getters_spec.js @@ -72,30 +72,6 @@ describe('Header Search Store Getters', () => { }); }); - describe.each` - project | ref | expectedPath - ${null} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}`} - ${MOCK_PROJECT} | ${null} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}`} - ${null} | ${MOCK_PROJECT.id} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_ref=${MOCK_PROJECT.id}`} - ${MOCK_PROJECT} | ${MOCK_PROJECT.id} | ${`${MOCK_AUTOCOMPLETE_PATH}?term=${MOCK_SEARCH}&project_id=${MOCK_PROJECT.id}&project_ref=${MOCK_PROJECT.id}`} - `('autocompleteQuery', ({ project, ref, expectedPath }) => { - describe(`when project is ${project?.name} and project ref is ${ref}`, () => { - beforeEach(() => { - createState({ - searchContext: { - project, - ref, - }, - }); - state.search = MOCK_SEARCH; - }); - - it(`should return ${expectedPath}`, () => { - expect(getters.autocompleteQuery(state)).toBe(expectedPath); - }); - }); - }); - describe.each` group | group_metadata | project | project_metadata | expectedPath ${null} | ${null} | ${null} | ${null} | ${MOCK_ISSUE_PATH} diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js index 81c81fc0a9f..4406d14d990 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js @@ -40,7 +40,7 @@ describe('Multi-file editor commit sidebar list', () => { wrapper = mountComponent({ fileList: [] }); }); - it('renders no changes text ', () => { + it('renders no changes text', () => { expect(wrapper.text()).toContain('No changes'); }); }); 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 d899bc4f7d8..ee6ed694285 100644 --- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js @@ -1,6 +1,6 @@ import Vue, { nextTick } from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue'; +import RadioGroup from '~/ide/components/commit_sidebar/radio_group.vue'; import { createStore } from '~/ide/stores'; describe('IDE commit sidebar radio group', () => { @@ -10,7 +10,7 @@ describe('IDE commit sidebar radio group', () => { beforeEach(async () => { store = createStore(); - const Component = Vue.extend(radioGroup); + const Component = Vue.extend(RadioGroup); store.state.commit.commitAction = '2'; @@ -38,7 +38,7 @@ describe('IDE commit sidebar radio group', () => { vm = new Vue({ components: { - radioGroup, + RadioGroup, }, store, render: (createElement) => @@ -62,7 +62,7 @@ describe('IDE commit sidebar radio group', () => { beforeEach(async () => { vm.$destroy(); - const Component = Vue.extend(radioGroup); + const Component = Vue.extend(RadioGroup); store.state.commit.commitAction = '1'; store.state.commit.newBranchName = 'test-123'; diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js index 532cb6e795c..043dcade858 100644 --- a/spec/frontend/ide/components/preview/navigator_spec.js +++ b/spec/frontend/ide/components/preview/navigator_spec.js @@ -76,7 +76,7 @@ describe('IDE clientside preview navigator', () => { listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` }); await nextTick(); findBackButton().trigger('click'); - expect(findBackButton().attributes('disabled')).toBeFalsy(); + expect(findBackButton().attributes()).not.toHaveProperty('disabled'); }); it('is disabled when there is no previous entry', async () => { @@ -117,7 +117,7 @@ describe('IDE clientside preview navigator', () => { findBackButton().trigger('click'); await nextTick(); - expect(findForwardButton().attributes('disabled')).toBeFalsy(); + expect(findForwardButton().attributes()).not.toHaveProperty('disabled'); }); it('is disabled when there is no next entry', async () => { diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js index a37c08af0a1..2efef9918b1 100644 --- a/spec/frontend/ide/components/shared/tokened_input_spec.js +++ b/spec/frontend/ide/components/shared/tokened_input_spec.js @@ -50,7 +50,7 @@ describe('IDE shared/TokenedInput', () => { }); it('renders input', () => { - expect(vm.$refs.input).toBeTruthy(); + expect(vm.$refs.input).toBeInstanceOf(HTMLInputElement); expect(vm.$refs.input).toHaveValue(TEST_VALUE); }); diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js new file mode 100644 index 00000000000..ec8559f1b56 --- /dev/null +++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js @@ -0,0 +1,62 @@ +import { start } from '@gitlab/web-ide'; +import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide'; +import { TEST_HOST } from 'helpers/test_constants'; + +jest.mock('@gitlab/web-ide'); + +const ROOT_ELEMENT_ID = 'ide'; +const TEST_NONCE = 'test123nonce'; +const TEST_PROJECT = { path_with_namespace: 'group1/project1' }; +const TEST_BRANCH_NAME = '12345-foo-patch'; +const TEST_GITLAB_URL = 'https://test-gitlab/'; +const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path'; + +describe('ide/init_gitlab_web_ide', () => { + const createRootElement = () => { + const el = document.createElement('div'); + + el.id = ROOT_ELEMENT_ID; + // why: We'll test that this class is removed later + el.classList.add('ide-loading'); + el.dataset.project = JSON.stringify(TEST_PROJECT); + el.dataset.cspNonce = TEST_NONCE; + el.dataset.branchName = TEST_BRANCH_NAME; + + document.body.append(el); + }; + const findRootElement = () => document.getElementById(ROOT_ELEMENT_ID); + const act = () => initGitlabWebIDE(findRootElement()); + + beforeEach(() => { + process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH; + window.gon.gitlab_url = TEST_GITLAB_URL; + + createRootElement(); + + act(); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('calls start with element', () => { + expect(start).toHaveBeenCalledWith(findRootElement(), { + baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, + projectPath: TEST_PROJECT.path_with_namespace, + ref: TEST_BRANCH_NAME, + gitlabUrl: TEST_GITLAB_URL, + nonce: TEST_NONCE, + }); + }); + + it('clears classes and data from root element', () => { + const rootEl = findRootElement(); + + // why: Snapshot to test that `ide-loading` was removed and no other + // artifacts are remaining. + expect(rootEl.outerHTML).toBe( + '
', + ); + }); +}); diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js index d1c31cd412b..38a54e569a9 100644 --- a/spec/frontend/ide/stores/actions/file_spec.js +++ b/spec/frontend/ide/stores/actions/file_spec.js @@ -78,7 +78,7 @@ describe('IDE store file actions', () => { }); }); - it('switches to the next available file before closing the current one ', () => { + it('switches to the next available file before closing the current one', () => { const f = file('newOpenFile'); store.state.openFiles.push(f); diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js index d65039e89cc..4e8467de759 100644 --- a/spec/frontend/ide/stores/modules/commit/actions_spec.js +++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js @@ -210,7 +210,7 @@ describe('IDE commit module actions', () => { branch, }); store.state.openFiles.forEach((entry) => { - expect(entry.changed).toBeFalsy(); + expect(entry.changed).toBe(false); }); }); diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js index 1c1e1e7ebd4..b896437ecb2 100644 --- a/spec/frontend/import_entities/components/group_dropdown_spec.js +++ b/spec/frontend/import_entities/components/group_dropdown_spec.js @@ -42,7 +42,7 @@ describe('Import entities group dropdown component', () => { createComponent({ namespaces }); namespacesTracker.mockReset(); - wrapper.find(GlSearchBoxByType).vm.$emit('input', 'match'); + wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match'); await nextTick(); 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 cdc508a0033..f97ea046cbe 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 @@ -99,7 +99,7 @@ describe('import table', () => { }); await waitForPromises(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('does not renders loading icon when request is completed', async () => { @@ -108,7 +108,7 @@ describe('import table', () => { }); await waitForPromises(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); }); @@ -123,7 +123,7 @@ describe('import table', () => { }); await waitForPromises(); - expect(wrapper.find(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND); + expect(wrapper.findComponent(GlEmptyState).props().title).toBe(i18n.NO_GROUPS_FOUND); }); }); @@ -268,7 +268,7 @@ describe('import table', () => { }); it('correctly passes pagination info from query', () => { - expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO); + expect(wrapper.findComponent(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO); }); it('renders pagination dropdown', () => { @@ -293,7 +293,7 @@ describe('import table', () => { it('updates page when page change is requested', async () => { const REQUESTED_PAGE = 2; - wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE); + wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE); await waitForPromises(); expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( @@ -316,7 +316,7 @@ describe('import table', () => { }, versionValidation: FAKE_VERSION_VALIDATION, }); - wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE); + wrapper.findComponent(PaginationLinks).props().change(REQUESTED_PAGE); await waitForPromises(); expect(wrapper.text()).toContain('Showing 21-21 of 38 groups that you own from'); @@ -539,8 +539,8 @@ describe('import table', () => { }); await waitForPromises(); - expect(wrapper.find(GlAlert).exists()).toBe(true); - expect(wrapper.find(GlAlert).text()).toContain('projects (require v14.8.0)'); + expect(wrapper.findComponent(GlAlert).exists()).toBe(true); + expect(wrapper.findComponent(GlAlert).text()).toContain('projects (require v14.8.0)'); }); it('does not renders alert when there are no unavailable features', async () => { @@ -558,7 +558,7 @@ describe('import table', () => { }); await waitForPromises(); - expect(wrapper.find(GlAlert).exists()).toBe(false); + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); }); }); 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 d3f86672f33..18dc1217fec 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 @@ -22,8 +22,8 @@ describe('import target cell', () => { let wrapper; let group; - const findNameInput = () => wrapper.find(GlFormInput); - const findNamespaceDropdown = () => wrapper.find(ImportGroupDropdown); + const findNameInput = () => wrapper.findComponent(GlFormInput); + const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown); const createComponent = (props) => { wrapper = shallowMount(ImportTargetCell, { 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 ea88c361f7b..9eae4ed974e 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 @@ -33,12 +33,12 @@ describe('BitbucketStatusTable', () => { it('renders import table component', () => { createComponent({ providerTitle: 'Test' }); - expect(wrapper.find(ImportProjectsTable).exists()).toBe(true); + expect(wrapper.findComponent(ImportProjectsTable).exists()).toBe(true); }); it('passes alert in incompatible-repos-warning slot', () => { createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub); - expect(wrapper.find(GlAlert).exists()).toBe(true); + expect(wrapper.findComponent(GlAlert).exists()).toBe(true); }); it('passes actions slot to import project table component', () => { @@ -46,14 +46,14 @@ describe('BitbucketStatusTable', () => { createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub, { actions: actionsSlotContent, }); - expect(wrapper.find(ImportProjectsTable).text()).toBe(actionsSlotContent); + expect(wrapper.findComponent(ImportProjectsTable).text()).toBe(actionsSlotContent); }); it('dismisses alert when requested', async () => { createComponent({ providerTitle: 'Test' }, ImportProjectsTableStub); - wrapper.find(GlAlert).vm.$emit('dismiss'); + wrapper.findComponent(GlAlert).vm.$emit('dismiss'); await nextTick(); - expect(wrapper.find(GlAlert).exists()).toBe(false); + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); }); 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 140fec3863b..c0ae4294e3d 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 @@ -30,10 +30,10 @@ describe('ImportProjectsTable', () => { const findImportAllButton = () => wrapper - .findAll(GlButton) + .findAllComponents(GlButton) .filter((w) => w.props().variant === 'confirm') .at(0); - const findImportAllModal = () => wrapper.find({ ref: 'importAllModal' }); + const findImportAllModal = () => wrapper.findComponent({ ref: 'importAllModal' }); const importAllFn = jest.fn(); const importAllModalShowFn = jest.fn(); @@ -89,13 +89,13 @@ describe('ImportProjectsTable', () => { it('renders a loading icon while repos are loading', () => { createComponent({ state: { isLoadingRepos: true } }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders a loading icon while namespaces are loading', () => { createComponent({ state: { isLoadingNamespaces: true } }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders a table with provider repos', () => { @@ -109,7 +109,7 @@ describe('ImportProjectsTable', () => { state: { namespaces: [{ fullPath: 'path' }], repositories }, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find('table').exists()).toBe(true); expect( wrapper @@ -118,7 +118,7 @@ describe('ImportProjectsTable', () => { .exists(), ).toBe(true); - expect(wrapper.findAll(ProviderRepoTableRow)).toHaveLength(repositories.length); + expect(wrapper.findAllComponents(ProviderRepoTableRow)).toHaveLength(repositories.length); }); it.each` @@ -170,7 +170,7 @@ describe('ImportProjectsTable', () => { it('renders an empty state if there are no repositories available', () => { createComponent({ state: { repositories: [] } }); - expect(wrapper.find(ProviderRepoTableRow).exists()).toBe(false); + expect(wrapper.findComponent(ProviderRepoTableRow).exists()).toBe(false); expect(wrapper.text()).toContain(`No ${providerTitle} repositories found`); }); @@ -231,11 +231,11 @@ describe('ImportProjectsTable', () => { }); it('renders intersection observer component', () => { - expect(wrapper.find(GlIntersectionObserver).exists()).toBe(true); + expect(wrapper.findComponent(GlIntersectionObserver).exists()).toBe(true); }); it('calls fetchRepos when intersection observer appears', async () => { - wrapper.find(GlIntersectionObserver).vm.$emit('appear'); + wrapper.findComponent(GlIntersectionObserver).vm.$emit('appear'); await nextTick(); 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 41a005199e1..17a07b1e9f9 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 @@ -74,11 +74,13 @@ describe('ProviderRepoTableRow', () => { }); it('renders empty import status', () => { - expect(wrapper.find(ImportStatus).props().status).toBe(STATUSES.NONE); + expect(wrapper.findComponent(ImportStatus).props().status).toBe(STATUSES.NONE); }); it('renders a group namespace select', () => { - expect(wrapper.find(ImportGroupDropdown).props().namespaces).toBe(availableNamespaces); + expect(wrapper.findComponent(ImportGroupDropdown).props().namespaces).toBe( + availableNamespaces, + ); }); it('renders import button', () => { @@ -127,11 +129,13 @@ describe('ProviderRepoTableRow', () => { }); it('renders proper import status', () => { - expect(wrapper.find(ImportStatus).props().status).toBe(repo.importedProject.importStatus); + expect(wrapper.findComponent(ImportStatus).props().status).toBe( + repo.importedProject.importStatus, + ); }); it('does not renders a namespace select', () => { - expect(wrapper.find(GlDropdown).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdown).exists()).toBe(false); }); it('does not render import button', () => { @@ -139,7 +143,7 @@ describe('ProviderRepoTableRow', () => { }); it('passes stats to import status component', () => { - expect(wrapper.find(ImportStatus).props().stats).toBe(FAKE_STATS); + expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS); }); }); @@ -165,7 +169,7 @@ describe('ProviderRepoTableRow', () => { }); it('renders badge with error', () => { - expect(wrapper.find(GlBadge).text()).toBe('Incompatible project'); + expect(wrapper.findComponent(GlBadge).text()).toBe('Incompatible project'); }); }); }); diff --git a/spec/frontend/import_entities/import_projects/store/getters_spec.js b/spec/frontend/import_entities/import_projects/store/getters_spec.js index 55826b20ca3..110b692b222 100644 --- a/spec/frontend/import_entities/import_projects/store/getters_spec.js +++ b/spec/frontend/import_entities/import_projects/store/getters_spec.js @@ -85,7 +85,7 @@ describe('import_projects store getters', () => { }); describe('hasImportableRepos', () => { - it('returns true if there are any importable projects ', () => { + it('returns true if there are any importable projects', () => { localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO]; expect(hasImportableRepos(localState)).toBe(true); @@ -99,7 +99,7 @@ describe('import_projects store getters', () => { }); describe('importAllCount', () => { - it('returns count of available importable projects ', () => { + it('returns count of available importable projects', () => { localState.repositories = [ IMPORTABLE_REPO, IMPORTABLE_REPO, diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js index 356480f931e..e8d222dc2e9 100644 --- a/spec/frontend/incidents/components/incidents_list_spec.js +++ b/spec/frontend/incidents/components/incidents_list_spec.js @@ -40,16 +40,16 @@ describe('Incidents List', () => { all: 26, }; - const findTable = () => wrapper.find(GlTable); + const findTable = () => wrapper.findComponent(GlTable); const findTableRows = () => wrapper.findAll('table tbody tr'); - const findAlert = () => wrapper.find(GlAlert); - const findLoader = () => wrapper.find(GlLoadingIcon); - const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip); + const findAlert = () => wrapper.findComponent(GlAlert); + const findLoader = () => wrapper.findComponent(GlLoadingIcon); + const findTimeAgo = () => wrapper.findAllComponents(TimeAgoTooltip); const findAssignees = () => wrapper.findAll('[data-testid="incident-assignees"]'); const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]'); const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']"); - const findEmptyState = () => wrapper.find(GlEmptyState); - const findSeverity = () => wrapper.findAll(SeverityToken); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findSeverity = () => wrapper.findAllComponents(SeverityToken); const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]'); const findIncidentLink = () => wrapper.findByTestId('incident-link'); @@ -179,7 +179,7 @@ describe('Incidents List', () => { }); it('renders an avatar component when there is an assignee', () => { - const avatar = findAssignees().at(1).find(GlAvatar); + const avatar = findAssignees().at(1).findComponent(GlAvatar); const { src, label } = avatar.attributes(); const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0]; 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 ff40f1fa008..394d1f12bcb 100644 --- a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js +++ b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js @@ -20,10 +20,10 @@ describe('IncidentsSettingTabs', () => { } }); - const findToggleButton = () => wrapper.find({ ref: 'toggleBtn' }); - const findSectionHeader = () => wrapper.find({ ref: 'sectionHeader' }); + const findToggleButton = () => wrapper.findComponent({ ref: 'toggleBtn' }); + const findSectionHeader = () => wrapper.findComponent({ ref: 'sectionHeader' }); - const findIntegrationTabs = () => wrapper.findAll(GlTab); + const findIntegrationTabs = () => wrapper.findAllComponents(GlTab); it('renders header text', () => { expect(findSectionHeader().text()).toBe('Incidents'); }); diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js index 8ee55928926..c329ca8522f 100644 --- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js +++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js @@ -24,7 +24,7 @@ describe('TriggerFields', () => { }); const findTriggerLabel = () => wrapper.findByTestId('trigger-fields-group').find('label'); - const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAll(GlFormGroup); + const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAllComponents(GlFormGroup); const findAllGlFormCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox); const findAllGlFormInputs = () => wrapper.findAllComponents(GlFormInput); @@ -86,7 +86,7 @@ describe('TriggerFields', () => { expect(checkboxes).toHaveLength(2); checkboxes.wrappers.forEach((checkbox, index) => { - const checkBox = checkbox.find(GlFormCheckbox); + const checkBox = checkbox.findComponent(GlFormCheckbox); expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText); expect(checkbox.find('[type=hidden]').attributes('name')).toBe( 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 b4d42d90d99..8b2d13be309 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 @@ -53,7 +53,7 @@ afterEach(() => { describe('ImportProjectMembersModal', () => { const findGlModal = () => wrapper.findComponent(GlModal); - const findIntroText = () => wrapper.find({ ref: 'modalIntro' }).text(); + const findIntroText = () => wrapper.findComponent({ ref: 'modalIntro' }).text(); const clickImportButton = () => findGlModal().vm.$emit('primary', { preventDefault: jest.fn() }); const closeModal = () => findGlModal().vm.$emit('hidden', { preventDefault: jest.fn() }); const findFormGroup = () => wrapper.findByTestId('form-group'); 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 2058784b033..e9e1fbad07b 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -19,6 +19,7 @@ import { MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT, LEARN_GITLAB, EXPANDED_ERRORS, + EMPTY_INVITES_ERROR_TEXT, } from '~/invite_members/constants'; import eventHub from '~/invite_members/event_hub'; import ContentTransition from '~/vue_shared/components/content_transition.vue'; @@ -255,6 +256,8 @@ describe('InviteMembersModal', () => { it('tracks the submit for invite_members_for_task', async () => { await setupComponentWithTasks(); + await triggerMembersTokenSelect([user1]); + clickInviteButton(); expect(ExperimentTracking).toHaveBeenCalledWith(INVITE_MEMBERS_FOR_TASK.name, { @@ -265,6 +268,16 @@ describe('InviteMembersModal', () => { INVITE_MEMBERS_FOR_TASK.submit, ); }); + + 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, + ); + }); }); }); @@ -380,6 +393,25 @@ describe('InviteMembersModal', () => { "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups."; 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 () => { + createInviteMembersToGroupWrapper(); + }); + + it('displays an error', async () => { + clickInviteButton(); + + await waitForPromises(); + + expect(membersFormGroupInvalidFeedback()).toBe(EMPTY_INVITES_ERROR_TEXT); + expect(findMembersSelect().props('exceptionState')).toBe(false); + + await triggerMembersTokenSelect([user1]); + + expect(membersFormGroupInvalidFeedback()).toBe(''); + }); + }); + describe('when inviting an existing user to group by user ID', () => { const postData = { user_id: '1,2', diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js index 543fc28a342..1ff2e86412f 100644 --- a/spec/frontend/invite_members/components/user_limit_notification_spec.js +++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js @@ -1,12 +1,7 @@ import { GlAlert, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue'; - -import { - REACHED_LIMIT_MESSAGE, - REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE, -} from '~/invite_members/constants'; - +import { REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE } from '~/invite_members/constants'; import { freeUsersLimit, membersCount } from '../mock_data/member_modal'; const WARNING_ALERT_TITLE = 'You only have space for 2 more members in name'; @@ -52,22 +47,6 @@ describe('UserLimitNotification', () => { }); }); - describe('when close to limit within a personal namepace', () => { - beforeEach(() => { - createComponent(true, false, { membersCount: 3, userNamespace: true }); - }); - - it('renders the limit for a personal namespace', () => { - const alert = findAlert(); - - expect(alert.attributes('title')).toEqual(WARNING_ALERT_TITLE); - - expect(alert.text()).toEqual( - 'To make more space, you can remove members who no longer need access.', - ); - }); - }); - describe('when close to limit within a group', () => { it("renders user's limit notification", () => { createComponent(true, false, { membersCount: 3 }); @@ -91,19 +70,5 @@ describe('UserLimitNotification', () => { expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for name"); expect(alert.text()).toEqual(REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE); }); - - describe('when free user namespace', () => { - it("renders user's limit notification", () => { - createComponent(true, true, { userNamespace: true }); - - const alert = findAlert(); - - expect(alert.attributes('title')).toEqual( - "You've reached your 5 members limit for your personal projects", - ); - - expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE); - }); - }); }); }); diff --git a/spec/frontend/issuable/components/issue_assignees_spec.js b/spec/frontend/issuable/components/issue_assignees_spec.js index 713c8b1dfdd..9a33bfae240 100644 --- a/spec/frontend/issuable/components/issue_assignees_spec.js +++ b/spec/frontend/issuable/components/issue_assignees_spec.js @@ -27,7 +27,7 @@ describe('IssueAssigneesComponent', () => { }); const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text(); - const findAvatars = () => wrapper.findAll(UserAvatarLink); + const findAvatars = () => wrapper.findAllComponents(UserAvatarLink); const findOverflowCounter = () => wrapper.find('.avatar-counter'); it('returns default data props', () => { diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js index 9d67f602136..eac53c5f761 100644 --- a/spec/frontend/issuable/components/issue_milestone_spec.js +++ b/spec/frontend/issuable/components/issue_milestone_spec.js @@ -144,7 +144,7 @@ describe('IssueMilestoneComponent', () => { }); it('renders milestone icon', () => { - expect(wrapper.find(GlIcon).props('name')).toBe('clock'); + expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock'); }); it('renders milestone title', () => { diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js index d844f3394d5..5e67ea42b87 100644 --- a/spec/frontend/issuable/issuable_form_spec.js +++ b/spec/frontend/issuable/issuable_form_spec.js @@ -1,111 +1,200 @@ import $ from 'jquery'; +import Autosave from '~/autosave'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import IssuableForm from '~/issuable/issuable_form'; import setWindowLocation from 'helpers/set_window_location_helper'; +jest.mock('~/autosave'); + +const createIssuable = (form) => { + return new IssuableForm(form); +}; + describe('IssuableForm', () => { + let $form; let instance; - const createIssuable = (form) => { - instance = new IssuableForm(form); - }; - beforeEach(() => { setHTMLFixture(`
+
`); - createIssuable($('form')); + $form = $('form'); }); afterEach(() => { resetHTMLFixture(); + $form = null; + instance = null; }); - describe('initAutosave', () => { - it('creates autosave with the searchTerm included', () => { - setWindowLocation('https://gitlab.test/foo?bar=true'); - const autosave = instance.initAutosave(); + describe('autosave', () => { + let $title; + let $description; + + beforeEach(() => { + $title = $form.find('input[name*="[title]"]'); + $description = $form.find('textarea[name*="[description]"]'); + }); - expect(autosave.key.includes('bar=true')).toBe(true); + afterEach(() => { + $title = null; + $description = null; }); - it("creates autosave fields without the searchTerm if it's an issue new form", () => { - setHTMLFixture(` -
- -
- `); - createIssuable($('form')); + describe('initAutosave', () => { + it('calls initAutosave', () => { + const initAutosave = jest.spyOn(IssuableForm.prototype, 'initAutosave'); + createIssuable($form); + + expect(initAutosave).toHaveBeenCalledTimes(1); + }); + + it('creates autosave with the searchTerm included', () => { + setWindowLocation('https://gitlab.test/foo?bar=true'); + createIssuable($form); + + expect(Autosave).toHaveBeenCalledWith( + $title, + ['/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", () => { + setWindowLocation('https://gitlab.test/issues/new?bar=true'); + $form.attr('data-new-issue-path', '/issues/new'); + createIssuable($form); + + expect(Autosave).toHaveBeenCalledWith( + $title, + ['/issues/new', '', 'title'], + 'autosave//issues/new/bar=true=title', + ); + expect(Autosave).toHaveBeenCalledWith( + $description, + ['/issues/new', '', 'description'], + 'autosave//issues/new/bar=true=description', + ); + }); + + it.each([ + { + id: 'confidential', + input: '', + selector: 'input[name*="[confidential]"]', + }, + { + id: 'due_date', + input: '', + selector: 'input[name*="[due_date]"]', + }, + ])('creates $id autosave when $id input exist', ({ id, input, selector }) => { + $form.append(input); + const $input = $form.find(selector); + const totalAutosaveFormFields = $form.children().length; + createIssuable($form); + + expect(Autosave).toHaveBeenCalledTimes(totalAutosaveFormFields); + expect(Autosave).toHaveBeenLastCalledWith($input, ['/', '', id], `autosave///=${id}`); + }); + }); + + describe('resetAutosave', () => { + it('calls reset on title and description', () => { + instance = createIssuable($form); + + instance.resetAutosave(); + + expect(instance.autosaves.get('title').reset).toHaveBeenCalledTimes(1); + expect(instance.autosaves.get('description').reset).toHaveBeenCalledTimes(1); + }); - setWindowLocation('https://gitlab.test/issues/new?bar=true'); + it('resets autosave when submit', () => { + const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave'); + createIssuable($form); - const autosave = instance.initAutosave(); + $form.submit(); - expect(autosave.key.includes('bar=true')).toBe(false); + expect(resetAutosave).toHaveBeenCalledTimes(1); + }); + + it('resets autosave on elements with the .js-reset-autosave class', () => { + const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave'); + $form.append('Cancel'); + createIssuable($form); + + $form.find('.js-reset-autosave').trigger('click'); + + expect(resetAutosave).toHaveBeenCalledTimes(1); + }); + + it.each([ + { id: 'confidential', input: '' }, + { id: 'due_date', input: '' }, + ])('calls reset on autosave $id when $id input exist', ({ id, input }) => { + $form.append(input); + instance = createIssuable($form); + instance.resetAutosave(); + + expect(instance.autosaves.get(id).reset).toHaveBeenCalledTimes(1); + }); }); }); - describe('resetAutosave', () => { - it('resets autosave on elements with the .js-reset-autosave class', () => { - setHTMLFixture(` -
- - - Cancel -
- `); - const $form = $('form'); - const resetAutosave = jest.spyOn(IssuableForm.prototype, 'resetAutosave'); - createIssuable($form); - - $form.find('.js-reset-autosave').trigger('click'); - - expect(resetAutosave).toHaveBeenCalled(); + describe('wip', () => { + beforeEach(() => { + instance = createIssuable($form); }); - }); - describe('removeWip', () => { - it.each` - prefix - ${'draFT: '} - ${' [DRaft] '} - ${'drAft:'} - ${'[draFT]'} - ${'(draft) '} - ${' (DrafT)'} - ${'draft: [draft] (draft)'} - `('removes "$prefix" from the beginning of the title', ({ prefix }) => { - instance.titleField.val(`${prefix}The Issuable's Title Value`); - - instance.removeWip(); - - expect(instance.titleField.val()).toBe("The Issuable's Title Value"); + describe('removeWip', () => { + it.each` + prefix + ${'draFT: '} + ${' [DRaft] '} + ${'drAft:'} + ${'[draFT]'} + ${'(draft) '} + ${' (DrafT)'} + ${'draft: [draft] (draft)'} + `('removes "$prefix" from the beginning of the title', ({ prefix }) => { + instance.titleField.val(`${prefix}The Issuable's Title Value`); + + instance.removeWip(); + + expect(instance.titleField.val()).toBe("The Issuable's Title Value"); + }); }); - }); - describe('addWip', () => { - it("properly adds the work in progress prefix to the Issuable's title", () => { - instance.titleField.val("The Issuable's Title Value"); + describe('addWip', () => { + it("properly adds the work in progress prefix to the Issuable's title", () => { + instance.titleField.val("The Issuable's Title Value"); - instance.addWip(); + instance.addWip(); - expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value"); + expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value"); + }); }); - }); - describe('workInProgress', () => { - it.each` - title | expected - ${'draFT: something is happening'} | ${true} - ${'draft something is happening'} | ${false} - ${'something is happening to drafts'} | ${false} - ${'something is happening'} | ${false} - `('returns $expected with "$title"', ({ title, expected }) => { - instance.titleField.val(title); - - expect(instance.workInProgress()).toBe(expected); + describe('workInProgress', () => { + it.each` + title | expected + ${'draFT: something is happening'} | ${true} + ${'draft something is happening'} | ${false} + ${'something is happening to drafts'} | ${false} + ${'something is happening'} | ${false} + `('returns $expected with "$title"', ({ title, expected }) => { + instance.titleField.val(title); + + expect(instance.workInProgress()).toBe(expected); + }); }); }); }); 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 d6aeacfe07a..bacebbade7f 100644 --- a/spec/frontend/issuable/related_issues/components/issue_token_spec.js +++ b/spec/frontend/issuable/related_issues/components/issue_token_spec.js @@ -31,11 +31,11 @@ describe('IssueToken', () => { } }); - const findLink = () => wrapper.find({ ref: 'link' }); - const findReference = () => wrapper.find({ ref: 'reference' }); + const findLink = () => wrapper.findComponent({ ref: 'link' }); + const findReference = () => wrapper.findComponent({ ref: 'reference' }); const findReferenceIcon = () => wrapper.find('[data-testid="referenceIcon"]'); const findRemoveBtn = () => wrapper.find('[data-testid="removeBtn"]'); - const findTitle = () => wrapper.find({ ref: 'title' }); + const findTitle = () => wrapper.findComponent({ ref: 'title' }); describe('with reference supplied', () => { beforeEach(() => { 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 772cc75a205..1b2935ce5d1 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 @@ -153,7 +153,7 @@ describe('RelatedIssuesBlock', () => { }); it('sets `autoCompleteEpics` to false for add-issuable-form', () => { - expect(wrapper.find(AddIssuableForm).props('autoCompleteEpics')).toBe(false); + expect(wrapper.findComponent(AddIssuableForm).props('autoCompleteEpics')).toBe(false); }); }); @@ -227,7 +227,7 @@ describe('RelatedIssuesBlock', () => { }, }); - const iconComponent = wrapper.find(GlIcon); + const iconComponent = wrapper.findComponent(GlIcon); expect(iconComponent.exists()).toBe(true); expect(iconComponent.props('name')).toBe(icon); }); 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 fd623ad9a5f..9bb71ec3dcb 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 @@ -187,7 +187,9 @@ describe('RelatedIssuesList', () => { }); it('shows due date', () => { - expect(wrapper.find(IssueDueDate).find('.board-card-info-text').text()).toBe('Nov 22, 2010'); + expect(wrapper.findComponent(IssueDueDate).find('.board-card-info-text').text()).toBe( + 'Nov 22, 2010', + ); }); }); }); diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js index cb7173c56a8..cc2ee84348a 100644 --- a/spec/frontend/issues/create_merge_request_dropdown_spec.js +++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js @@ -106,7 +106,7 @@ describe('CreateMergeRequestDropdown', () => { loading | hasClass ${true} | ${false} ${false} | ${true} - `('it toggle loading spinner when loading is $loading', ({ loading, hasClass }) => { + `('toggle loading spinner when loading is $loading', ({ loading, hasClass }) => { dropdown.setLoading(loading); expect(document.querySelector('.js-spinner').classList.contains('gl-display-none')).toEqual( 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 c3f13ca6f9a..b0d3a63a8cf 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 @@ -21,7 +21,7 @@ describe('CE IssueCardTimeInfo component', () => { }; const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]'); - const findMilestoneTitle = () => findMilestone().find(GlLink).attributes('title'); + const findMilestoneTitle = () => findMilestone().findComponent(GlLink).attributes('title'); const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]'); const mountComponent = ({ @@ -56,8 +56,8 @@ describe('CE IssueCardTimeInfo component', () => { const milestone = findMilestone(); expect(milestone.text()).toBe(issue.milestone.title); - expect(milestone.find(GlIcon).props('name')).toBe('clock'); - expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath); + expect(milestone.findComponent(GlIcon).props('name')).toBe('clock'); + expect(milestone.findComponent(GlLink).attributes('href')).toBe(issue.milestone.webPath); }); describe.each` @@ -84,7 +84,7 @@ describe('CE IssueCardTimeInfo component', () => { expect(dueDate.text()).toBe('Dec 12, 2020'); expect(dueDate.attributes('title')).toBe('Due date'); - expect(dueDate.find(GlIcon).props('name')).toBe('calendar'); + expect(dueDate.findComponent(GlIcon).props('name')).toBe('calendar'); expect(dueDate.classes()).not.toContain('gl-text-red-500'); }); }); @@ -118,6 +118,6 @@ describe('CE IssueCardTimeInfo component', () => { expect(timeEstimate.text()).toBe(issue.humanTimeEstimate); expect(timeEstimate.attributes('title')).toBe('Estimate'); - expect(timeEstimate.find(GlIcon).props('name')).toBe('timer'); + expect(timeEstimate.findComponent(GlIcon).props('name')).toBe('timer'); }); }); diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index a39853fd29c..5133c02b190 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, GlEmptyState, GlLink } from '@gitlab/ui'; +import { GlButton, GlEmptyState } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import { mount, shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; @@ -29,7 +29,6 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; import IssuesListApp from '~/issues/list/components/issues_list_app.vue'; import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue'; - import { CREATED_DESC, RELATIVE_POSITION, @@ -58,6 +57,10 @@ import { WORK_ITEM_TYPE_ENUM_TASK, WORK_ITEM_TYPE_ENUM_TEST_CASE, } from '~/work_items/constants'; +import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; + +import('~/issuable/bulk_update_sidebar'); +import('~/users_select'); jest.mock('@sentry/browser'); jest.mock('~/flash'); @@ -122,7 +125,6 @@ describe('CE IssuesListApp component', () => { const findGlButtons = () => wrapper.findAllComponents(GlButton); const findGlButtonAt = (index) => findGlButtons().at(index); const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); - const findGlLink = () => wrapper.findComponent(GlLink); const findIssuableList = () => wrapper.findComponent(IssuableList); const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown); @@ -430,8 +432,9 @@ describe('CE IssuesListApp component', () => { }); }); - it('is not set from url params', () => { - expect(findIssuableList().props('initialFilterValue')).toEqual([]); + it('is set from url params and removes search terms', () => { + const expected = filteredTokens.filter((token) => token.type !== FILTERED_SEARCH_TERM); + expect(findIssuableList().props('initialFilterValue')).toEqual(expected); }); it('shows an alert to tell the user they must be signed in to search', () => { @@ -562,15 +565,16 @@ describe('CE IssuesListApp component', () => { it('shows Jira integration information', () => { const paragraphs = wrapper.findAll('p'); - expect(paragraphs.at(2).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle); - expect(paragraphs.at(3).text()).toContain( + const links = wrapper.findAll('.gl-link'); + expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle); + expect(paragraphs.at(2).text()).toContain( 'Enable the Jira integration to view your Jira issues in GitLab.', ); - expect(paragraphs.at(4).text()).toContain( + expect(paragraphs.at(3).text()).toContain( IssuesListApp.i18n.jiraIntegrationSecondaryMessage, ); - expect(findGlLink().text()).toBe('Enable the Jira integration'); - expect(findGlLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath); + expect(links.at(1).text()).toBe('Enable the Jira integration'); + expect(links.at(1).attributes('href')).toBe(defaultProvide.jiraIntegrationPath); }); }); @@ -1006,8 +1010,9 @@ describe('CE IssuesListApp component', () => { findIssuableList().vm.$emit('filter', filteredTokens); }); - it('does not update url params', () => { - expect(router.push).not.toHaveBeenCalled(); + it('removes search terms', () => { + const expected = filteredTokens.filter((token) => token.type !== FILTERED_SEARCH_TERM); + expect(findIssuableList().props('initialFilterValue')).toEqual(expected); }); it('shows an alert to tell the user they must be signed in to search', () => { 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 2d773e8bf56..406b1fbc1af 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 @@ -11,9 +11,9 @@ describe('JiraIssuesImportStatus', () => { }; let wrapper; - const findAlert = () => wrapper.find(GlAlert); + const findAlert = () => wrapper.findComponent(GlAlert); - const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel); + const findAlertLabel = () => wrapper.findComponent(GlAlert).findComponent(GlLabel); const mountComponent = ({ shouldShowFinishedAlert = false, @@ -49,7 +49,7 @@ describe('JiraIssuesImportStatus', () => { }); it('does not show an alert', () => { - expect(wrapper.find(GlAlert).exists()).toBe(false); + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); }); @@ -105,12 +105,12 @@ describe('JiraIssuesImportStatus', () => { shouldShowInProgressAlert: true, }); - expect(wrapper.find(GlAlert).exists()).toBe(true); + expect(wrapper.findComponent(GlAlert).exists()).toBe(true); findAlert().vm.$emit('dismiss'); await nextTick(); - expect(wrapper.find(GlAlert).exists()).toBe(false); + expect(wrapper.findComponent(GlAlert).exists()).toBe(false); }); }); }); 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 5eb30b52de5..c54a762440f 100644 --- a/spec/frontend/issues/new/components/title_suggestions_item_spec.js +++ b/spec/frontend/issues/new/components/title_suggestions_item_spec.js @@ -20,7 +20,7 @@ describe('Issue title suggestions item component', () => { } const findLink = () => wrapper.findComponent(GlLink); - const findAuthorLink = () => wrapper.findAll(GlLink).at(1); + const findAuthorLink = () => wrapper.findAllComponents(GlLink).at(1); const findIcon = () => wrapper.findComponent(GlIcon); const findTooltip = () => wrapper.findComponent(GlTooltip); const findUserAvatar = () => wrapper.findComponent(UserAvatarImage); @@ -105,7 +105,7 @@ describe('Issue title suggestions item component', () => { const count = wrapper.findAll('.suggestion-counts span').at(0); expect(count.text()).toContain('1'); - expect(count.find(GlIcon).props('name')).toBe('thumb-up'); + expect(count.findComponent(GlIcon).props('name')).toBe('thumb-up'); }); it('renders notes count', () => { @@ -114,7 +114,7 @@ describe('Issue title suggestions item component', () => { const count = wrapper.findAll('.suggestion-counts span').at(1); expect(count.text()).toContain('2'); - expect(count.find(GlIcon).props('name')).toBe('comment'); + expect(count.findComponent(GlIcon).props('name')).toBe('comment'); }); }); diff --git a/spec/frontend/issues/new/components/title_suggestions_spec.js b/spec/frontend/issues/new/components/title_suggestions_spec.js index 0a64890e4ca..1cd6576967a 100644 --- a/spec/frontend/issues/new/components/title_suggestions_spec.js +++ b/spec/frontend/issues/new/components/title_suggestions_spec.js @@ -83,7 +83,7 @@ describe('Issue title suggestions component', () => { wrapper.setData(data); await nextTick(); - expect(wrapper.findAll(TitleSuggestionsItem).length).toBe(2); + expect(wrapper.findAllComponents(TitleSuggestionsItem).length).toBe(2); }); it('adds margin class to first item', async () => { 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 4df04cd5257..d30a8c081cc 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 @@ -65,9 +65,9 @@ describe('RelatedMergeRequests', () => { describe('template', () => { it('should render related merge request items', () => { expect(wrapper.find('[data-testid="count"]').text()).toBe('2'); - expect(wrapper.findAll(RelatedIssuableItem)).toHaveLength(2); + expect(wrapper.findAllComponents(RelatedIssuableItem)).toHaveLength(2); - const props = wrapper.findAll(RelatedIssuableItem).at(1).props(); + const props = wrapper.findAllComponents(RelatedIssuableItem).at(1).props(); const data = mockData[1]; expect(props.idKey).toEqual(data.id); diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index 12f9707da04..3d027e2084c 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -461,7 +461,7 @@ describe('Issuable output', () => { describe('when title is not in view', () => { beforeEach(() => { wrapper.vm.state.titleText = 'Sticky header title'; - wrapper.find(GlIntersectionObserver).vm.$emit('disappear'); + wrapper.findComponent(GlIntersectionObserver).vm.$emit('disappear'); }); it('shows with title', () => { diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index bdb1448148e..9d9abce887b 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -12,6 +12,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createFlash from '~/flash'; import Description from '~/issues/show/components/description.vue'; import { updateHistory } from '~/lib/utils/url_utility'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; @@ -71,7 +72,11 @@ describe('Description component', () => { const findModal = () => wrapper.findComponent(GlModal); const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal); - function createComponent({ props = {}, provide } = {}) { + function createComponent({ + props = {}, + provide, + createWorkItemFromTaskHandler = createWorkItemFromTaskSuccessHandler, + } = {}) { wrapper = shallowMountExtended(Description, { propsData: { issueId: 1, @@ -85,7 +90,7 @@ describe('Description component', () => { apolloProvider: createMockApollo([ [workItemQuery, queryHandler], [workItemTypesQuery, workItemTypesQueryHandler], - [createWorkItemFromTaskMutation, createWorkItemFromTaskSuccessHandler], + [createWorkItemFromTaskMutation, createWorkItemFromTaskHandler], ]), mocks: { $toast, @@ -317,7 +322,28 @@ describe('Description component', () => { 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('creating work item from checklist item', () => { it('emits `updateDescription` after creating new work item', async () => { + createComponent({ + props: { + descriptionHtml: descriptionHtmlWithCheckboxes, + }, + provide: { + glFeatures: { + workItemsCreateFromMarkdown: true, + }, + }, + }); + const newDescription = `

New description

`; await findConvertToTaskButton().trigger('click'); @@ -327,12 +353,28 @@ describe('Description component', () => { expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]); }); - it('shows toast after delete success', async () => { - const newDesc = 'description'; - findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc); + it('shows flash message when creating task fails', async () => { + createComponent({ + props: { + descriptionHtml: descriptionHtmlWithCheckboxes, + }, + provide: { + glFeatures: { + workItemsCreateFromMarkdown: true, + }, + }, + createWorkItemFromTaskHandler: jest.fn().mockRejectedValue({}), + }); - expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]); - expect($toast.show).toHaveBeenCalledWith('Task deleted'); + await findConvertToTaskButton().trigger('click'); + + await waitForPromises(); + + expect(createFlash).toHaveBeenCalledWith( + expect.objectContaining({ + message: 'Something went wrong when creating task. Please try again.', + }), + ); }); }); diff --git a/spec/frontend/issues/show/components/edit_actions_spec.js b/spec/frontend/issues/show/components/edit_actions_spec.js index d58bf1be812..11c43ea4388 100644 --- a/spec/frontend/issues/show/components/edit_actions_spec.js +++ b/spec/frontend/issues/show/components/edit_actions_spec.js @@ -2,16 +2,9 @@ import { GlButton } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; 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 IssuableEditActions from '~/issues/show/components/edit_actions.vue'; -import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue'; import eventHub from '~/issues/show/event_hub'; -import { - getIssueStateQueryResponse, - updateIssueStateQueryResponse, -} from '../mock_data/apollo_mock'; describe('Edit Actions component', () => { let wrapper; @@ -31,8 +24,6 @@ describe('Edit Actions component', () => { }, }; - const modalId = 'delete-issuable-modal-1'; - const createComponent = ({ props, data } = {}) => { fakeApollo = createMockApollo([], mockResolvers); @@ -50,16 +41,13 @@ describe('Edit Actions component', () => { data() { return { issueState: {}, - modalId, ...data, }; }, }); }; - const findModal = () => wrapper.findComponent(DeleteIssueModal); const findEditButtons = () => wrapper.findAllComponents(GlButton); - const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button'); const findSaveButton = () => wrapper.findByTestId('issuable-save-button'); const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button'); @@ -79,23 +67,12 @@ describe('Edit Actions component', () => { }); }); - it('does not render the delete button if canDestroy is false', () => { - createComponent({ props: { canDestroy: false } }); - expect(findDeleteButton().exists()).toBe(false); - }); - it('disables save button when title is blank', () => { createComponent({ props: { formState: { title: '', issue_type: '' } } }); expect(findSaveButton().attributes('disabled')).toBe('true'); }); - it('does not render the delete button if showDeleteButton is false', () => { - createComponent({ props: { showDeleteButton: false } }); - - expect(findDeleteButton().exists()).toBe(false); - }); - describe('updateIssuable', () => { beforeEach(() => { jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); @@ -119,63 +96,4 @@ describe('Edit Actions component', () => { expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); }); }); - - describe('delete issue button', () => { - let trackingSpy; - - beforeEach(() => { - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - it('tracks clicking on button', () => { - findDeleteButton().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', { - label: 'delete_issue', - }); - }); - }); - - describe('delete issue modal', () => { - it('renders', () => { - expect(findModal().props()).toEqual({ - issuePath: 'gitlab-org/gitlab-test/-/issues/1', - issueType: 'Issue', - modalId, - title: 'Delete issue', - }); - }); - }); - - describe('deleteIssuable', () => { - beforeEach(() => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - }); - - it('does not send the `delete.issuable` event when clicking delete button', () => { - findDeleteButton().vm.$emit('click'); - expect(eventHub.$emit).not.toHaveBeenCalled(); - }); - - it('sends the `delete.issuable` event when clicking the delete confirm button', async () => { - expect(eventHub.$emit).toHaveBeenCalledTimes(0); - findModal().vm.$emit('delete'); - expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable'); - expect(eventHub.$emit).toHaveBeenCalledTimes(1); - }); - }); - - describe('with Apollo cache mock', () => { - it('renders the right delete button text per apollo cache type', async () => { - mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse); - await waitForPromises(); - expect(findDeleteButton().text()).toBe('Delete issue'); - }); - - it('should not change the delete button text per apollo cache mutation', async () => { - mockIssueStateData.mockResolvedValue(updateIssueStateQueryResponse); - await waitForPromises(); - expect(findDeleteButton().text()).toBe('Delete issue'); - }); - }); }); diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js index d0e33f0b980..61433607a2b 100644 --- a/spec/frontend/issues/show/components/fields/description_spec.js +++ b/spec/frontend/issues/show/components/fields/description_spec.js @@ -6,7 +6,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; describe('Description field component', () => { let wrapper; - const findTextarea = () => wrapper.find({ ref: 'textarea' }); + const findTextarea = () => wrapper.findComponent({ ref: 'textarea' }); const mountComponent = (description = 'test') => shallowMount(DescriptionField, { diff --git a/spec/frontend/issues/show/components/fields/title_spec.js b/spec/frontend/issues/show/components/fields/title_spec.js index de04405d89b..a5fa96d8d64 100644 --- a/spec/frontend/issues/show/components/fields/title_spec.js +++ b/spec/frontend/issues/show/components/fields/title_spec.js @@ -5,7 +5,7 @@ import eventHub from '~/issues/show/event_hub'; describe('Title field component', () => { let wrapper; - const findInput = () => wrapper.find({ ref: 'input' }); + const findInput = () => wrapper.findComponent({ ref: 'input' }); beforeEach(() => { jest.spyOn(eventHub, '$emit'); diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js index 329c4234f30..dc2b3c6fc48 100644 --- a/spec/frontend/issues/show/components/header_actions_spec.js +++ b/spec/frontend/issues/show/components/header_actions_spec.js @@ -65,17 +65,17 @@ describe('HeaderActions component', () => { }, }; - const findToggleIssueStateButton = () => wrapper.find(GlButton); + const findToggleIssueStateButton = () => wrapper.findComponent(GlButton); const findDropdownBy = (dataTestId) => wrapper.find(`[data-testid="${dataTestId}"]`); const findMobileDropdown = () => findDropdownBy('mobile-dropdown'); const findDesktopDropdown = () => findDropdownBy('desktop-dropdown'); - const findMobileDropdownItems = () => findMobileDropdown().findAll(GlDropdownItem); - const findDesktopDropdownItems = () => findDesktopDropdown().findAll(GlDropdownItem); + const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem); + const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem); - const findModal = () => wrapper.find(GlModal); + const findModal = () => wrapper.findComponent(GlModal); - const findModalLinkAt = (index) => findModal().findAll(GlLink).at(index); + const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index); const mountComponent = ({ props = {}, 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 3ab2bb3460b..1286617d64a 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 @@ -1,13 +1,13 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { GlDatepicker } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue'; import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue'; import createTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql'; 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 { useFakeDate } from 'helpers/fake_date'; @@ -35,24 +35,21 @@ describe('Create Timeline events', () => { let responseSpy; let apolloProvider; - const findSubmitButton = () => wrapper.findByText(__('Save')); - const findSubmitAndAddButton = () => - wrapper.findByText(s__('Incident|Save and add another event')); - const findCancelButton = () => wrapper.findByText(__('Cancel')); + const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save); + const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd); + const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel); const findDatePicker = () => wrapper.findComponent(GlDatepicker); const findNoteInput = () => wrapper.findByTestId('input-note'); const setNoteInput = () => { - const textarea = findNoteInput().element; - textarea.value = mockInputData.note; - textarea.dispatchEvent(new Event('input')); + findNoteInput().setValue(mockInputData.note); }; const findHourInput = () => wrapper.findByTestId('input-hours'); const findMinuteInput = () => wrapper.findByTestId('input-minutes'); const setDatetime = () => { const inputDate = new Date(mockInputData.occurredAt); findDatePicker().vm.$emit('input', inputDate); - findHourInput().vm.$emit('input', inputDate.getHours()); - findMinuteInput().vm.$emit('input', inputDate.getMinutes()); + findHourInput().setValue(inputDate.getHours()); + findMinuteInput().setValue(inputDate.getMinutes()); }; const fillForm = () => { setDatetime(); diff --git a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js new file mode 100644 index 00000000000..4c1638a9147 --- /dev/null +++ b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js @@ -0,0 +1,44 @@ +import EditTimelineEvent from '~/issues/show/components/incidents/edit_timeline_event.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue'; + +import { mockEvents, fakeEventData, mockInputData } from './mock_data'; + +describe('Edit Timeline events', () => { + let wrapper; + + const mountComponent = () => { + wrapper = mountExtended(EditTimelineEvent, { + propsData: { + event: mockEvents[0], + editTimelineEventActive: false, + }, + }); + }; + + beforeEach(() => { + mountComponent(); + }); + + const findTimelineEventsForm = () => wrapper.findComponent(TimelineEventsForm); + + const mockSaveData = { ...fakeEventData, ...mockInputData }; + + describe('editTimelineEvent', () => { + const saveEventEvent = { 'handle-save-edit': [[mockSaveData, false]] }; + + it('should call the mutation with the right variables', async () => { + await findTimelineEventsForm().vm.$emit('save-event', mockSaveData, false); + + expect(wrapper.emitted()).toEqual(saveEventEvent); + }); + + it('should close the form on cancel', async () => { + const cancelEvent = { 'hide-edit': [[]] }; + + await findTimelineEventsForm().vm.$emit('cancel'); + + expect(wrapper.emitted()).toEqual(cancelEvent); + }); + }); +}); 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 155ae703e48..1cfb7d12a91 100644 --- a/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js +++ b/spec/frontend/issues/show/components/incidents/highlight_bar_spec.js @@ -41,7 +41,7 @@ describe('Highlight Bar', () => { } }); - const findLink = () => wrapper.find(GlLink); + const findLink = () => wrapper.findComponent(GlLink); describe('empty state', () => { beforeEach(() => { 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 8e090645be2..d92aeabba0f 100644 --- a/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js +++ b/spec/frontend/issues/show/components/incidents/incident_tabs_spec.js @@ -61,12 +61,12 @@ describe('Incident Tabs component', () => { ); }; - const findTabs = () => wrapper.findAll(GlTab); + const findTabs = () => wrapper.findAllComponents(GlTab); const findSummaryTab = () => findTabs().at(0); const findAlertDetailsTab = () => wrapper.find('[data-testid="alert-details-tab"]'); - const findAlertDetailsComponent = () => wrapper.find(AlertDetailsTable); - const findDescriptionComponent = () => wrapper.find(DescriptionComponent); - const findHighlightBarComponent = () => wrapper.find(HighlightBar); + const findAlertDetailsComponent = () => wrapper.findComponent(AlertDetailsTable); + const findDescriptionComponent = () => wrapper.findComponent(DescriptionComponent); + const findHighlightBarComponent = () => wrapper.findComponent(HighlightBar); const findTimelineTab = () => wrapper.findComponent(TimelineTab); describe('empty state', () => { diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js index 75c0a7350ae..adea2b6df59 100644 --- a/spec/frontend/issues/show/components/incidents/mock_data.js +++ b/spec/frontend/issues/show/components/incidents/mock_data.js @@ -49,6 +49,15 @@ export const mockEvents = [ }, ]; +const mockUpdatedEvent = { + id: 'gid://gitlab/IncidentManagement::TimelineEvent/8', + note: 'another one23', + noteHtml: '

another one23

', + action: 'comment', + occurredAt: '2022-07-01T12:47:00Z', + createdAt: '2022-07-20T12:47:40Z', +}; + export const timelineEventsQueryListResponse = { data: { project: { @@ -93,6 +102,29 @@ export const timelineEventsCreateEventError = { }, }; +export const timelineEventsEditEventResponse = { + data: { + timelineEventUpdate: { + timelineEvent: { + ...mockUpdatedEvent, + }, + errors: [], + __typename: 'TimelineEventUpdatePayload', + }, + }, +}; + +export const timelineEventsEditEventError = { + data: { + timelineEventUpdate: { + timelineEvent: { + ...mockUpdatedEvent, + }, + errors: ['Create error'], + }, + }, +}; + const timelineEventDeleteData = (errors = []) => { return { data: { @@ -125,3 +157,13 @@ export const mockGetTimelineData = { }, }, }; + +export const fakeDate = '2020-07-08T00:00:00.000Z'; + +export const mockInputData = { + note: 'test', + occurredAt: '2020-08-10T02:30:00.000Z', +}; + +const { id, note, occurredAt } = mockEvents[0]; +export const fakeEventData = { id, note, occurredAt }; 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 cd2cbb63246..7f086a276f7 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 @@ -4,6 +4,8 @@ import { GlDatepicker } from '@gitlab/ui'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue'; +import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import { timelineFormI18n } from '~/issues/show/components/incidents/constants'; import { createAlert } from '~/flash'; import { useFakeDate } from 'helpers/fake_date'; @@ -13,6 +15,8 @@ jest.mock('~/flash'); const fakeDate = '2020-07-08T00:00:00.000Z'; +const mockInputDate = new Date('2021-08-12'); + describe('Timeline events form', () => { // July 8 2020 useFakeDate(fakeDate); @@ -21,7 +25,7 @@ describe('Timeline events form', () => { const mountComponent = ({ mountMethod = shallowMountExtended }) => { wrapper = mountMethod(TimelineEventsForm, { propsData: { - hasTimelineEvents: true, + showSaveAndAdd: true, isEventProcessed: false, }, }); @@ -32,17 +36,17 @@ describe('Timeline events form', () => { wrapper.destroy(); }); - const findSubmitButton = () => wrapper.findByText('Save'); - const findSubmitAndAddButton = () => wrapper.findByText('Save and add another event'); - const findCancelButton = () => wrapper.findByText('Cancel'); + const findMarkdownField = () => wrapper.findComponent(MarkdownField); + const findSubmitButton = () => wrapper.findByText(timelineFormI18n.save); + const findSubmitAndAddButton = () => wrapper.findByText(timelineFormI18n.saveAndAdd); + const findCancelButton = () => wrapper.findByText(timelineFormI18n.cancel); const findDatePicker = () => wrapper.findComponent(GlDatepicker); - const findDatePickerInput = () => wrapper.findByTestId('input-datepicker'); const findHourInput = () => wrapper.findByTestId('input-hours'); const findMinuteInput = () => wrapper.findByTestId('input-minutes'); const setDatetime = () => { - findDatePicker().vm.$emit('input', new Date('2021-08-12')); - findHourInput().vm.$emit('input', 5); - findMinuteInput().vm.$emit('input', 45); + findDatePicker().vm.$emit('input', mockInputDate); + findHourInput().setValue(5); + findMinuteInput().setValue(45); }; const submitForm = async () => { @@ -58,6 +62,22 @@ describe('Timeline events form', () => { await waitForPromises(); }; + it('renders markdown-field component with correct list of toolbar items', () => { + mountComponent({ mountMethod: mountExtended }); + + expect(findMarkdownField().props('restrictedToolBarItems')).toEqual([ + 'quote', + 'strikethrough', + 'bullet-list', + 'numbered-list', + 'task-list', + 'collapsible-section', + 'table', + 'attach-file', + 'full-screen', + ]); + }); + describe('form button behaviour', () => { beforeEach(() => { mountComponent({ mountMethod: mountExtended }); @@ -87,14 +107,14 @@ describe('Timeline events form', () => { setDatetime(); await nextTick(); - expect(findDatePickerInput().element.value).toBe('2021-08-12'); + expect(findDatePicker().props('value')).toBe(mockInputDate); expect(findHourInput().element.value).toBe('5'); expect(findMinuteInput().element.value).toBe('45'); wrapper.vm.clear(); await nextTick(); - expect(findDatePickerInput().element.value).toBe('2020-07-08'); + expect(findDatePicker().props('value')).toStrictEqual(new Date(fakeDate)); expect(findHourInput().element.value).toBe('0'); expect(findMinuteInput().element.value).toBe('0'); }); diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js index 90e55003ab3..1bf8d68efd4 100644 --- a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js +++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js @@ -1,6 +1,7 @@ import timezoneMock from 'timezone-mock'; import { GlIcon, GlDropdown } from '@gitlab/ui'; import { nextTick } from 'vue'; +import { timelineItemI18n } from '~/issues/show/components/incidents/constants'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue'; import { mockEvents } from './mock_data'; @@ -15,21 +16,19 @@ describe('IncidentTimelineEventList', () => { action, noteHtml, occurredAt, - isLastItem: false, ...propsData, }, provide: { - canUpdate: false, + canUpdateTimelineEvent: false, ...provide, }, }); }; const findCommentIcon = () => wrapper.findComponent(GlIcon); - const findTextContainer = () => wrapper.findByTestId('event-text-container'); const findEventTime = () => wrapper.findByTestId('event-time'); const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDeleteButton = () => wrapper.findByText('Delete'); + const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete); describe('template', () => { it('shows comment icon', () => { @@ -50,20 +49,6 @@ describe('IncidentTimelineEventList', () => { expect(findEventTime().text()).toBe('15:59 UTC'); }); - describe('last item in list', () => { - it('shows a bottom border when not the last item', () => { - mountComponent(); - - expect(findTextContainer().classes()).toContain('gl-border-1'); - }); - - it('does not show a bottom border when the last item', () => { - mountComponent({ propsData: { isLastItem: true } }); - - expect(wrapper.classes()).not.toContain('gl-border-1'); - }); - }); - describe.each` timezone ${'Europe/London'} @@ -96,20 +81,20 @@ describe('IncidentTimelineEventList', () => { }); it('shows dropdown and delete item when user has update permission', () => { - mountComponent({ provide: { canUpdate: true } }); + mountComponent({ provide: { canUpdateTimelineEvent: true } }); expect(findDropdown().exists()).toBe(true); expect(findDeleteButton().exists()).toBe(true); }); it('triggers a delete when the delete button is clicked', async () => { - mountComponent({ provide: { canUpdate: true } }); + mountComponent({ provide: { canUpdateTimelineEvent: true } }); findDeleteButton().trigger('click'); await nextTick(); - expect(wrapper.emitted().delete).toBeTruthy(); + expect(wrapper.emitted().delete).toHaveLength(1); }); }); }); 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 4d2d53c990e..dff1c429d07 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 @@ -3,16 +3,24 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue'; -import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_item.vue'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue'; +import EditTimelineEvent from '~/issues/show/components/incidents/edit_timeline_event.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import deleteTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/delete_timeline_event.mutation.graphql'; +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 { mockEvents, timelineEventsDeleteEventResponse, timelineEventsDeleteEventError, + timelineEventsEditEventResponse, + timelineEventsEditEventError, + fakeDate, + fakeEventData, + mockInputData, } from './mock_data'; Vue.use(VueApollo); @@ -20,83 +28,73 @@ Vue.use(VueApollo); jest.mock('~/flash'); jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); -const deleteEventResponse = jest.fn(); - -function createMockApolloProvider() { - deleteEventResponse.mockResolvedValue(timelineEventsDeleteEventResponse); - const requestHandlers = [[deleteTimelineEventMutation, deleteEventResponse]]; - return createMockApollo(requestHandlers); -} - const mockConfirmAction = ({ confirmed }) => { confirmAction.mockResolvedValueOnce(confirmed); }; describe('IncidentTimelineEventList', () => { + useFakeDate(fakeDate); let wrapper; + const deleteResponseSpy = jest.fn().mockResolvedValue(timelineEventsDeleteEventResponse); + const editResponseSpy = jest.fn().mockResolvedValue(timelineEventsEditEventResponse); - const mountComponent = (mockApollo) => { - const apollo = mockApollo ? { apolloProvider: mockApollo } : {}; + const requestHandlers = [ + [deleteTimelineEventMutation, deleteResponseSpy], + [editTimelineEventMutation, editResponseSpy], + ]; + const apolloProvider = createMockApollo(requestHandlers); - wrapper = shallowMountExtended(IncidentTimelineEventList, { + const mountComponent = () => { + wrapper = mountExtended(IncidentTimelineEventList, { + propsData: { + timelineEvents: mockEvents, + }, provide: { fullPath: 'group/project', issuableId: '1', + canUpdateTimelineEvent: true, }, - propsData: { - timelineEvents: mockEvents, - }, - ...apollo, + apolloProvider, }); }; const findTimelineEventGroups = () => wrapper.findAllByTestId('timeline-group'); - const findItems = (base = wrapper) => base.findAll(IncidentTimelineEventListItem); + const findItems = (base = wrapper) => base.findAllComponents(IncidentTimelineEventItem); const findFirstTimelineEventGroup = () => findTimelineEventGroups().at(0); const findSecondTimelineEventGroup = () => findTimelineEventGroups().at(1); const findDates = () => wrapper.findAllByTestId('event-date'); const clickFirstDeleteButton = async () => { - findItems() - .at(0) - .vm.$emit('delete', { ...mockEvents[0] }); + findItems().at(0).vm.$emit('delete', { fakeEventData }); await waitForPromises(); }; + const clickFirstEditButton = async () => { + findItems().at(0).vm.$emit('edit'); + await waitForPromises(); + }; + beforeEach(() => { + mountComponent(); + }); + afterEach(() => { - confirmAction.mockReset(); - deleteEventResponse.mockReset(); wrapper.destroy(); }); describe('template', () => { it('groups items correctly', () => { - mountComponent(); - expect(findTimelineEventGroups()).toHaveLength(2); expect(findItems(findFirstTimelineEventGroup())).toHaveLength(1); expect(findItems(findSecondTimelineEventGroup())).toHaveLength(2); }); - it('sets the isLastItem prop correctly', () => { - mountComponent(); - - expect(findItems().at(0).props('isLastItem')).toBe(false); - expect(findItems().at(1).props('isLastItem')).toBe(false); - expect(findItems().at(2).props('isLastItem')).toBe(true); - }); - it('sets the event props correctly', () => { - mountComponent(); - expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt); expect(findItems().at(1).props('action')).toBe(mockEvents[1].action); expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml); }); it('formats dates correctly', () => { - mountComponent(); - expect(findDates().at(0).text()).toBe('2022-03-22'); expect(findDates().at(1).text()).toBe('2022-03-23'); }); @@ -110,8 +108,6 @@ describe('IncidentTimelineEventList', () => { describe(timezone, () => { beforeEach(() => { timezoneMock.register(timezone); - - mountComponent(); }); afterEach(() => { @@ -131,12 +127,9 @@ describe('IncidentTimelineEventList', () => { it('should delete when button is clicked', async () => { const expectedVars = { input: { id: mockEvents[0].id } }; - - mountComponent(createMockApolloProvider()); - await clickFirstDeleteButton(); - expect(deleteEventResponse).toHaveBeenCalledWith(expectedVars); + expect(deleteResponseSpy).toHaveBeenCalledWith(expectedVars); }); it('should show an error when delete returns an error', async () => { @@ -144,8 +137,7 @@ describe('IncidentTimelineEventList', () => { message: 'Error deleting incident timeline event: Item does not exist', }; - mountComponent(createMockApolloProvider()); - deleteEventResponse.mockResolvedValue(timelineEventsDeleteEventError); + deleteResponseSpy.mockResolvedValue(timelineEventsDeleteEventError); await clickFirstDeleteButton(); @@ -158,8 +150,7 @@ describe('IncidentTimelineEventList', () => { error: new Error(), message: 'Something went wrong while deleting the incident timeline event.', }; - mountComponent(createMockApolloProvider()); - deleteEventResponse.mockRejectedValueOnce(); + deleteResponseSpy.mockRejectedValueOnce(); await clickFirstDeleteButton(); @@ -167,4 +158,76 @@ describe('IncidentTimelineEventList', () => { }); }); }); + + describe('Edit Functionality', () => { + beforeEach(() => { + mountComponent(); + clickFirstEditButton(); + }); + + const findEditEvent = () => wrapper.findComponent(EditTimelineEvent); + const mockSaveData = { ...fakeEventData, ...mockInputData }; + + describe('editTimelineEvent', () => { + it('should call the mutation with the right variables', async () => { + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(editResponseSpy).toHaveBeenCalledWith({ + input: mockSaveData, + }); + }); + + it('should close the form on successful addition', async () => { + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(findEditEvent().exists()).toBe(false); + }); + + it('should close the form on cancel', async () => { + await findEditEvent().vm.$emit('hide-edit'); + await waitForPromises(); + + expect(findEditEvent().exists()).toBe(false); + }); + }); + + describe('error handling', () => { + it('should show an error when submission returns an error', async () => { + const expectedAlertArgs = { + message: `Error updating incident timeline event: ${timelineEventsEditEventError.data.timelineEventUpdate.errors[0]}`, + }; + editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError); + + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs); + }); + + it('should show an error when submission fails', async () => { + const expectedAlertArgs = { + captureError: true, + error: new Error(), + message: 'Something went wrong while updating the incident timeline event.', + }; + editResponseSpy.mockRejectedValueOnce(); + + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs); + }); + + it('should keep the form open on failed addition', async () => { + editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError); + + await findEditEvent().vm.$emit('handle-save-edit', mockSaveData); + await waitForPromises(); + + expect(findEditEvent().exists()).toBe(true); + }); + }); + }); }); 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 2cdb971395d..5bac1d6e7ad 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 @@ -36,7 +36,7 @@ describe('TimelineEventsTab', () => { provide: { fullPath: 'group/project', issuableId: '1', - canUpdate: true, + canUpdateTimelineEvent: true, ...provide, }, apolloProvider: mockApollo, @@ -136,29 +136,20 @@ describe('TimelineEventsTab', () => { it('should not show a button when user cannot update', () => { mountComponent({ mockApollo: createMockApolloProvider(emptyResponse), - provide: { canUpdate: false }, + provide: { canUpdateTimelineEvent: false }, }); expect(findAddEventButton().exists()).toBe(false); }); it('should not show a form by default', () => { - expect(findCreateTimelineEvent().isVisible()).toBe(false); + expect(findCreateTimelineEvent().exists()).toBe(false); }); it('should show a form when button is clicked', async () => { await findAddEventButton().trigger('click'); - expect(findCreateTimelineEvent().isVisible()).toBe(true); - }); - - it('should clear the form when button is clicked', async () => { - const mockClear = jest.fn(); - wrapper.vm.$refs.createEventForm.clearForm = mockClear; - - await findAddEventButton().trigger('click'); - - expect(mockClear).toHaveBeenCalled(); + expect(findCreateTimelineEvent().exists()).toBe(true); }); it('should hide the form when the hide event is emitted', async () => { @@ -167,7 +158,7 @@ describe('TimelineEventsTab', () => { await findCreateTimelineEvent().vm.$emit('hide-new-timeline-events-form'); - expect(findCreateTimelineEvent().isVisible()).toBe(false); + expect(findCreateTimelineEvent().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js index d3a86680f14..f0494591e95 100644 --- a/spec/frontend/issues/show/components/incidents/utils_spec.js +++ b/spec/frontend/issues/show/components/incidents/utils_spec.js @@ -2,7 +2,7 @@ import timezoneMock from 'timezone-mock'; import { displayAndLogError, getEventIcon, - getUtcShiftedDateNow, + getUtcShiftedDate, } from '~/issues/show/components/incidents/utils'; import { createAlert } from '~/flash'; @@ -34,7 +34,7 @@ describe('incident utils', () => { }); }); - describe('getUtcShiftedDateNow', () => { + describe('getUtcShiftedDate', () => { beforeEach(() => { timezoneMock.register('US/Pacific'); }); @@ -46,7 +46,7 @@ describe('incident utils', () => { it('should shift the date by the timezone offset', () => { const date = new Date(); - const shiftedDate = getUtcShiftedDateNow(); + const shiftedDate = getUtcShiftedDate(); expect(shiftedDate > date).toBe(true); }); diff --git a/spec/frontend/issues/show/components/pinned_links_spec.js b/spec/frontend/issues/show/components/pinned_links_spec.js index aac720df6e9..208baac7124 100644 --- a/spec/frontend/issues/show/components/pinned_links_spec.js +++ b/spec/frontend/issues/show/components/pinned_links_spec.js @@ -9,7 +9,7 @@ const plainStatusUrl = 'https://status.com'; describe('PinnedLinks', () => { let wrapper; - const findButtons = () => wrapper.findAll(GlButton); + const findButtons = () => wrapper.findAllComponents(GlButton); const createComponent = (props) => { wrapper = shallowMount(PinnedLinks, { 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 b38d2b60057..d4202f4a6ab 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 @@ -62,8 +62,8 @@ describe('Sentry Error Stack Trace', () => { describe('loading', () => { it('should show spinner while loading', () => { mountComponent(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find(Stacktrace).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(Stacktrace).exists()).toBe(false); }); }); @@ -74,8 +74,8 @@ describe('Sentry Error Stack Trace', () => { it('should show stacktrace', () => { mountComponent({ stubs: {} }); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(Stacktrace).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(Stacktrace).exists()).toBe(true); }); }); }); 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 b9fed5f34f1..cc8346253ee 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 @@ -217,7 +217,7 @@ describe('NewBranchForm', () => { }); it('emits `success` event', () => { - expect(wrapper.emitted('success')).toBeTruthy(); + expect(wrapper.emitted('success')).toHaveLength(1); }); it('called `createBranch` mutation correctly', () => { diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js index 57b11bdbc27..cf496d5836a 100644 --- a/spec/frontend/jira_connect/subscriptions/api_spec.js +++ b/spec/frontend/jira_connect/subscriptions/api_spec.js @@ -1,7 +1,14 @@ import MockAdapter from 'axios-mock-adapter'; -import { addSubscription, removeSubscription, fetchGroups } from '~/jira_connect/subscriptions/api'; +import { + axiosInstance, + addSubscription, + removeSubscription, + fetchGroups, + getCurrentUser, + addJiraConnectSubscription, + updateInstallation, +} from '~/jira_connect/subscriptions/api'; import { getJwt } from '~/jira_connect/subscriptions/utils'; -import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; jest.mock('~/jira_connect/subscriptions/utils', () => ({ @@ -9,21 +16,26 @@ jest.mock('~/jira_connect/subscriptions/utils', () => ({ })); describe('JiraConnect API', () => { - let mock; + let axiosMock; + let originalGon; let response; const mockAddPath = 'addPath'; const mockRemovePath = 'removePath'; const mockNamespace = 'namespace'; const mockJwt = 'jwt'; + const mockAccessToken = 'accessToken'; const mockResponse = { success: true }; beforeEach(() => { - mock = new MockAdapter(axios); + axiosMock = new MockAdapter(axiosInstance); + originalGon = window.gon; + window.gon = { api_version: 'v4' }; }); afterEach(() => { - mock.restore(); + axiosMock.restore(); + window.gon = originalGon; response = null; }); @@ -31,8 +43,8 @@ describe('JiraConnect API', () => { const makeRequest = () => addSubscription(mockAddPath, mockNamespace); it('returns success response', async () => { - jest.spyOn(axios, 'post'); - mock + jest.spyOn(axiosInstance, 'post'); + axiosMock .onPost(mockAddPath, { jwt: mockJwt, namespace_path: mockNamespace, @@ -42,7 +54,7 @@ describe('JiraConnect API', () => { response = await makeRequest(); expect(getJwt).toHaveBeenCalled(); - expect(axios.post).toHaveBeenCalledWith(mockAddPath, { + expect(axiosInstance.post).toHaveBeenCalledWith(mockAddPath, { jwt: mockJwt, namespace_path: mockNamespace, }); @@ -54,13 +66,13 @@ describe('JiraConnect API', () => { const makeRequest = () => removeSubscription(mockRemovePath); it('returns success response', async () => { - jest.spyOn(axios, 'delete'); - mock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse); + jest.spyOn(axiosInstance, 'delete'); + axiosMock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse); response = await makeRequest(); expect(getJwt).toHaveBeenCalled(); - expect(axios.delete).toHaveBeenCalledWith(mockRemovePath, { + expect(axiosInstance.delete).toHaveBeenCalledWith(mockRemovePath, { params: { jwt: mockJwt, }, @@ -81,8 +93,8 @@ describe('JiraConnect API', () => { }); it('returns success response', async () => { - jest.spyOn(axios, 'get'); - mock + jest.spyOn(axiosInstance, 'get'); + axiosMock .onGet(mockGroupsPath, { page: mockPage, per_page: mockPerPage, @@ -91,7 +103,7 @@ describe('JiraConnect API', () => { response = await makeRequest(); - expect(axios.get).toHaveBeenCalledWith(mockGroupsPath, { + expect(axiosInstance.get).toHaveBeenCalledWith(mockGroupsPath, { params: { page: mockPage, per_page: mockPerPage, @@ -100,4 +112,82 @@ describe('JiraConnect API', () => { expect(response.data).toEqual(mockResponse); }); }); + + describe('getCurrentUser', () => { + const makeRequest = () => getCurrentUser(); + + it('returns success response', async () => { + const expectedUrl = '/api/v4/user'; + + jest.spyOn(axiosInstance, 'get'); + + axiosMock.onGet(expectedUrl).replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(axiosInstance.get).toHaveBeenCalledWith(expectedUrl, {}); + expect(response.data).toEqual(mockResponse); + }); + }); + + describe('addJiraConnectSubscription', () => { + const makeRequest = () => + addJiraConnectSubscription(mockNamespace, { jwt: mockJwt, accessToken: mockAccessToken }); + + it('returns success response', async () => { + const expectedUrl = '/api/v4/integrations/jira_connect/subscriptions'; + + jest.spyOn(axiosInstance, 'post'); + + axiosMock.onPost(expectedUrl).replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(axiosInstance.post).toHaveBeenCalledWith( + expectedUrl, + { + jwt: mockJwt, + namespace_path: mockNamespace, + }, + { headers: { Authorization: `Bearer ${mockAccessToken}` } }, + ); + expect(response.data).toEqual(mockResponse); + }); + }); + + describe('updateInstallation', () => { + const expectedUrl = '/-/jira_connect/installations'; + + it.each` + instanceUrl | expectedInstanceUrl + ${'https://gitlab.com'} | ${null} + ${'https://gitlab.mycompany.com'} | ${'https://gitlab.mycompany.com'} + `( + 'when instanceUrl is $instanceUrl, it passes `instance_url` as $expectedInstanceUrl', + async ({ instanceUrl, expectedInstanceUrl }) => { + const makeRequest = () => updateInstallation(instanceUrl); + + jest.spyOn(axiosInstance, 'put'); + axiosMock + .onPut(expectedUrl, { + jwt: mockJwt, + installation: { + instance_url: expectedInstanceUrl, + }, + }) + .replyOnce(httpStatus.OK, mockResponse); + + response = await makeRequest(); + + expect(getJwt).toHaveBeenCalled(); + expect(axiosInstance.put).toHaveBeenCalledWith(expectedUrl, { + jwt: mockJwt, + installation: { + instance_url: expectedInstanceUrl, + }, + }); + expect(response.data).toEqual(mockResponse); + }, + ); + }); }); 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 d871b1e1dcc..f1fc5e4d90b 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 @@ -50,7 +50,7 @@ describe('GroupsList', () => { const findGlAlert = () => wrapper.findComponent(GlAlert); const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findAllItems = () => wrapper.findAll(GroupsListItem); + const findAllItems = () => wrapper.findAllComponents(GroupsListItem); const findFirstItem = () => findAllItems().at(0); const findSecondItem = () => findAllItems().at(1); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js index 9894141be5a..369ddda8dbe 100644 --- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js @@ -31,8 +31,8 @@ describe('JiraConnectApp', () => { const findUserLink = () => wrapper.findComponent(UserLink); const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert); - const createComponent = ({ provide, mountFn = shallowMountExtended } = {}) => { - store = createStore({ subscriptions: [mockSubscription] }); + const createComponent = ({ provide, mountFn = shallowMountExtended, initialState = {} } = {}) => { + store = createStore({ ...initialState, subscriptions: [mockSubscription] }); jest.spyOn(store, 'dispatch').mockImplementation(); wrapper = mountFn(JiraConnectApp, { @@ -60,7 +60,7 @@ describe('JiraConnectApp', () => { }); it(`${shouldRenderSignInPage ? 'renders' : 'does not render'} sign in page`, () => { - expect(findSignInPage().exists()).toBe(shouldRenderSignInPage); + expect(findSignInPage().isVisible()).toBe(shouldRenderSignInPage); if (shouldRenderSignInPage) { expect(findSignInPage().props('hasSubscriptions')).toBe(true); } @@ -133,7 +133,7 @@ describe('JiraConnectApp', () => { }); it('renders link when `linkUrl` is set', async () => { - createComponent({ mountFn: mountExtended }); + createComponent({ provide: { usersPath: '' }, mountFn: mountExtended }); store.commit(SET_ALERT, { message: __('test message %{linkStart}test link%{linkEnd}'), @@ -211,21 +211,22 @@ describe('JiraConnectApp', () => { describe('when `jiraConnectOauth` feature flag is enabled', () => { const mockSubscriptionsPath = '/mockSubscriptionsPath'; - beforeEach(() => { + beforeEach(async () => { jest.spyOn(api, 'fetchSubscriptions').mockResolvedValue({ data: { subscriptions: [] } }); + jest.spyOn(AccessorUtilities, 'canUseCrypto').mockReturnValue(true); createComponent({ + initialState: { + currentUser: { name: 'root' }, + }, provide: { glFeatures: { jiraConnectOauth: true }, subscriptionsPath: mockSubscriptionsPath, }, }); - }); - describe('when component mounts', () => { - it('dispatches `fetchSubscriptions` action', async () => { - expect(store.dispatch).toHaveBeenCalledWith('fetchSubscriptions', mockSubscriptionsPath); - }); + findSignInPage().vm.$emit('sign-in-oauth'); + await nextTick(); }); describe('when oauth button emits `sign-in-oauth` event', () => { 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 ed0abaaf576..01317eb5dba 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 @@ -1,39 +1,41 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; + import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue'; import { I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, OAUTH_WINDOW_OPTIONS, } from '~/jira_connect/subscriptions/constants'; -import axios from '~/lib/utils/axios_utils'; import waitForPromises from 'helpers/wait_for_promises'; -import httpStatus from '~/lib/utils/http_status'; import AccessorUtilities from '~/lib/utils/accessor'; -import { getCurrentUser } from '~/rest_api'; +import { + getCurrentUser, + fetchOAuthApplicationId, + fetchOAuthToken, +} from '~/jira_connect/subscriptions/api'; import createStore from '~/jira_connect/subscriptions/store'; import { SET_ACCESS_TOKEN } from '~/jira_connect/subscriptions/store/mutation_types'; jest.mock('~/lib/utils/accessor'); jest.mock('~/jira_connect/subscriptions/utils'); jest.mock('~/jira_connect/subscriptions/api'); -jest.mock('~/rest_api'); jest.mock('~/jira_connect/subscriptions/pkce', () => ({ createCodeVerifier: jest.fn().mockReturnValue('mock-verifier'), createCodeChallenge: jest.fn().mockResolvedValue('mock-challenge'), })); -const mockOauthMetadata = { - oauth_authorize_url: 'https://gitlab.com/mockOauth', - oauth_token_url: 'https://gitlab.com/mockOauthToken', - state: 'good-state', -}; - describe('SignInOauthButton', () => { let wrapper; - let mockAxios; let store; + const mockOauthMetadata = { + oauth_authorize_url: 'https://gitlab.com/mockOauth', + oauth_token_path: 'https://gitlab.com/mockOauthToken', + oauth_token_payload: { + client_id: '543678901', + }, + state: 'good-state', + }; const createComponent = ({ slots, props } = {}) => { store = createStore(); @@ -50,13 +52,8 @@ describe('SignInOauthButton', () => { }); }; - beforeEach(() => { - mockAxios = new MockAdapter(axios); - }); - afterEach(() => { wrapper.destroy(); - mockAxios.restore(); }); const findButton = () => wrapper.findComponent(GlButton); @@ -69,6 +66,46 @@ describe('SignInOauthButton', () => { expect(findButton().props('category')).toBe('primary'); }); + describe('when `gitlabBasePath` is passed', () => { + const mockBasePath = 'https://gitlab.mycompany.com'; + + it('uses custom text for button', () => { + createComponent({ + props: { + gitlabBasePath: mockBasePath, + }, + }); + + expect(findButton().text()).toBe(`Sign in to ${mockBasePath}`); + }); + + describe('on click', () => { + const mockClientId = '798412381'; + + beforeEach(async () => { + fetchOAuthApplicationId.mockReturnValue({ data: { application_id: mockClientId } }); + jest.spyOn(window, 'open').mockReturnValue(); + createComponent({ + props: { + gitlabBasePath: mockBasePath, + }, + }); + + findButton().vm.$emit('click'); + + await nextTick(); + }); + + it('calls `window.open` with correct arguments', () => { + expect(window.open).toHaveBeenCalledWith( + `${mockBasePath}/mockOauth?code_challenge=mock-challenge&code_challenge_method=S256&client_id=${mockClientId}`, + I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, + OAUTH_WINDOW_OPTIONS, + ); + }); + }); + }); + it.each` scenario | cryptoAvailable ${'when crypto API is available'} | ${true} @@ -96,7 +133,7 @@ describe('SignInOauthButton', () => { it('calls `window.open` with correct arguments', () => { expect(window.open).toHaveBeenCalledWith( - `${mockOauthMetadata.oauth_authorize_url}?code_challenge=mock-challenge&code_challenge_method=S256`, + `${mockOauthMetadata.oauth_authorize_url}?code_challenge=mock-challenge&code_challenge_method=S256&client_id=${mockOauthMetadata.oauth_token_payload.client_id}`, I18N_DEFAULT_SIGN_IN_BUTTON_TEXT, OAUTH_WINDOW_OPTIONS, ); @@ -151,11 +188,7 @@ describe('SignInOauthButton', () => { describe('when API requests succeed', () => { beforeEach(async () => { - jest.spyOn(axios, 'post'); - jest.spyOn(axios, 'get'); - mockAxios - .onPost(mockOauthMetadata.oauth_token_url) - .replyOnce(httpStatus.OK, { access_token: mockAccessToken }); + fetchOAuthToken.mockResolvedValue({ data: { access_token: mockAccessToken } }); getCurrentUser.mockResolvedValue({ data: mockUser }); window.dispatchEvent(new MessageEvent('message', mockEvent)); @@ -164,9 +197,10 @@ describe('SignInOauthButton', () => { }); it('executes POST request to Oauth token endpoint', () => { - expect(axios.post).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_url, { + expect(fetchOAuthToken).toHaveBeenCalledWith(mockOauthMetadata.oauth_token_path, { code: '1234', code_verifier: 'mock-verifier', + client_id: mockOauthMetadata.oauth_token_payload.client_id, }); }); @@ -185,10 +219,7 @@ describe('SignInOauthButton', () => { describe('when API requests fail', () => { beforeEach(async () => { - jest.spyOn(axios, 'post'); - mockAxios - .onPost(mockOauthMetadata.oauth_token_url) - .replyOnce(httpStatus.INTERNAL_SERVER_ERROR); + fetchOAuthToken.mockRejectedValue(); window.dispatchEvent(new MessageEvent('message', mockEvent)); 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 1649920b48b..b9a8451f3b3 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 @@ -101,7 +101,7 @@ describe('SignInGitlabCom', () => { const button = findSignInOauthButton(); button.vm.$emit('error'); - expect(wrapper.emitted('error')).toBeTruthy(); + 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 f4be8bf121d..10696d25f17 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 @@ -5,9 +5,22 @@ import SignInGitlabMultiversion from '~/jira_connect/subscriptions/pages/sign_in import VersionSelectForm from '~/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue'; import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue'; +import { updateInstallation } from '~/jira_connect/subscriptions/api'; +import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils'; + +jest.mock('~/jira_connect/subscriptions/api', () => { + return { + updateInstallation: jest.fn(), + setApiBaseURL: jest.fn(), + }; +}); +jest.mock('~/jira_connect/subscriptions/utils'); + describe('SignInGitlabMultiversion', () => { let wrapper; + const mockBasePath = 'gitlab.mycompany.com'; + const findVersionSelectForm = () => wrapper.findComponent(VersionSelectForm); const findSignInOauthButton = () => wrapper.findComponent(SignInOauthButton); const findSubtitle = () => wrapper.findByTestId('subtitle'); @@ -29,30 +42,32 @@ describe('SignInGitlabMultiversion', () => { }); describe('when form emits "submit" event', () => { - it('hides the version select form and shows the sign in button', async () => { + it('updates the backend, then saves the baseUrl and reloads', async () => { + updateInstallation.mockResolvedValue({}); + createComponent(); - findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com'); + findVersionSelectForm().vm.$emit('submit', mockBasePath); await nextTick(); - expect(findVersionSelectForm().exists()).toBe(false); - expect(findSignInOauthButton().exists()).toBe(true); + expect(updateInstallation).toHaveBeenCalled(); + expect(persistBaseUrl).toHaveBeenCalledWith(mockBasePath); + expect(reloadPage).toHaveBeenCalled(); }); }); }); }); describe('when version is selected', () => { - beforeEach(async () => { + beforeEach(() => { + retrieveBaseUrl.mockReturnValue(mockBasePath); createComponent(); - - findVersionSelectForm().vm.$emit('submit', 'gitlab.mycompany.com'); - await nextTick(); }); describe('sign in button', () => { it('renders sign in button', () => { expect(findSignInOauthButton().exists()).toBe(true); + expect(findSignInOauthButton().props('gitlabBasePath')).toBe(mockBasePath); }); describe('when button emits `sign-in` event', () => { @@ -71,7 +86,7 @@ describe('SignInGitlabMultiversion', () => { const button = findSignInOauthButton(); button.vm.$emit('error'); - expect(wrapper.emitted('error')).toBeTruthy(); + expect(wrapper.emitted('error')).toHaveLength(1); }); }); }); diff --git a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js index 53b5d8e70af..5e3c30269b5 100644 --- a/spec/frontend/jira_connect/subscriptions/store/actions_spec.js +++ b/spec/frontend/jira_connect/subscriptions/store/actions_spec.js @@ -8,8 +8,6 @@ import { } from '~/jira_connect/subscriptions/store/actions'; import state from '~/jira_connect/subscriptions/store/state'; import * as api from '~/jira_connect/subscriptions/api'; -import * as userApi from '~/api/user_api'; -import * as integrationsApi from '~/api/integrations_api'; import { I18N_DEFAULT_SUBSCRIPTIONS_ERROR_MESSAGE, I18N_ADD_SUBSCRIPTION_SUCCESS_ALERT_TITLE, @@ -79,7 +77,7 @@ describe('JiraConnect actions', () => { describe('when API request succeeds', () => { it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => { const mockUser = { name: 'root' }; - jest.spyOn(userApi, 'getCurrentUser').mockResolvedValue({ data: mockUser }); + jest.spyOn(api, 'getCurrentUser').mockResolvedValue({ data: mockUser }); await testAction( loadCurrentUser, @@ -89,7 +87,7 @@ describe('JiraConnect actions', () => { [], ); - expect(userApi.getCurrentUser).toHaveBeenCalledWith({ + expect(api.getCurrentUser).toHaveBeenCalledWith({ headers: { Authorization: `Bearer ${mockAccessToken}` }, }); }); @@ -97,7 +95,7 @@ describe('JiraConnect actions', () => { describe('when API request fails', () => { it('commits the SET_CURRENT_USER_ERROR mutation', async () => { - jest.spyOn(userApi, 'getCurrentUser').mockRejectedValue(); + jest.spyOn(api, 'getCurrentUser').mockRejectedValue(); await testAction( loadCurrentUser, @@ -120,9 +118,7 @@ describe('JiraConnect actions', () => { describe('when API request succeeds', () => { it('commits the SET_ACCESS_TOKEN and SET_CURRENT_USER mutations', async () => { - jest - .spyOn(integrationsApi, 'addJiraConnectSubscription') - .mockResolvedValue({ success: true }); + jest.spyOn(api, 'addJiraConnectSubscription').mockResolvedValue({ success: true }); await testAction( addSubscription, @@ -144,7 +140,7 @@ describe('JiraConnect actions', () => { [{ type: 'fetchSubscriptions', payload: mockSubscriptionsPath }], ); - expect(integrationsApi.addJiraConnectSubscription).toHaveBeenCalledWith(mockNamespace, { + expect(api.addJiraConnectSubscription).toHaveBeenCalledWith(mockNamespace, { accessToken: null, jwt: '1234', }); @@ -153,7 +149,7 @@ describe('JiraConnect actions', () => { describe('when API request fails', () => { it('commits the SET_CURRENT_USER_ERROR mutation', async () => { - jest.spyOn(integrationsApi, 'addJiraConnectSubscription').mockRejectedValue(); + jest.spyOn(api, 'addJiraConnectSubscription').mockRejectedValue(); await testAction( addSubscription, 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 cd8024d4962..022a0f81aaa 100644 --- a/spec/frontend/jira_import/components/jira_import_app_spec.js +++ b/spec/frontend/jira_import/components/jira_import_app_spec.js @@ -21,15 +21,15 @@ describe('JiraImportApp', () => { const setupIllustration = 'setup-illustration.svg'; - const getFormComponent = () => wrapper.find(JiraImportForm); + const getFormComponent = () => wrapper.findComponent(JiraImportForm); - const getProgressComponent = () => wrapper.find(JiraImportProgress); + const getProgressComponent = () => wrapper.findComponent(JiraImportProgress); - const getSetupComponent = () => wrapper.find(JiraImportSetup); + const getSetupComponent = () => wrapper.findComponent(JiraImportSetup); - const getAlert = () => wrapper.find(GlAlert); + const getAlert = () => wrapper.findComponent(GlAlert); - const getLoadingIcon = () => wrapper.find(GlLoadingIcon); + const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const mountComponent = ({ isJiraConfigured = true, 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 41d3cd46d01..d43a9f8a145 100644 --- a/spec/frontend/jira_import/components/jira_import_form_spec.js +++ b/spec/frontend/jira_import/components/jira_import_form_spec.js @@ -164,8 +164,9 @@ describe('JiraImportForm', () => { it('shows a heading for the user mapping section', () => { expect( - getByRole(wrapper.element, 'heading', { name: 'Jira-GitLab user mapping template' }), - ).toBeTruthy(); + getByRole(wrapper.element, 'heading', { name: 'Jira-GitLab user mapping template' }) + .innerText, + ).toBe('Jira-GitLab user mapping template'); }); it('shows information to the user', () => { @@ -182,15 +183,15 @@ describe('JiraImportForm', () => { }); it('has a "Jira display name" column', () => { - expect(getHeader('Jira display name')).toBeTruthy(); + expect(getHeader('Jira display name').innerText).toBe('Jira display name'); }); it('has an "arrow" column', () => { - expect(getHeader('Arrow')).toBeTruthy(); + expect(getHeader('Arrow').getAttribute('aria-label')).toBe('Arrow'); }); it('has a "GitLab username" column', () => { - expect(getHeader('GitLab username')).toBeTruthy(); + expect(getHeader('GitLab username').innerText).toBe('GitLab username'); }); }); @@ -288,8 +289,8 @@ describe('JiraImportForm', () => { }); it('updates the user list', () => { - expect(getUserDropdown().findAll(GlDropdownItem)).toHaveLength(1); - expect(getUserDropdown().find(GlDropdownItem).text()).toContain( + expect(getUserDropdown().findAllComponents(GlDropdownItem)).toHaveLength(1); + expect(getUserDropdown().findComponent(GlDropdownItem).text()).toContain( 'fchopin (Frederic Chopin)', ); }); 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 04b2a2da622..42356763492 100644 --- a/spec/frontend/jira_import/components/jira_import_progress_spec.js +++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js @@ -8,7 +8,7 @@ describe('JiraImportProgress', () => { const importProject = 'JIRAPROJECT'; - const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute); + const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute); const getParagraphText = () => wrapper.find('p').text(); 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 320e270b493..0085a2b5572 100644 --- a/spec/frontend/jira_import/components/jira_import_setup_spec.js +++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js @@ -6,7 +6,7 @@ import { illustration, jiraIntegrationPath } from '../mock_data'; describe('JiraImportSetup', () => { let wrapper; - const getGlEmptyStateProp = (attribute) => wrapper.find(GlEmptyState).props(attribute); + const getGlEmptyStateProp = (attribute) => wrapper.findComponent(GlEmptyState).props(attribute); beforeEach(() => { wrapper = shallowMount(JiraImportSetup, { diff --git a/spec/frontend/jobs/components/artifacts_block_spec.js b/spec/frontend/jobs/components/artifacts_block_spec.js deleted file mode 100644 index 0c7c0a6c311..00000000000 --- a/spec/frontend/jobs/components/artifacts_block_spec.js +++ /dev/null @@ -1,176 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { trimText } from 'helpers/text_helper'; -import ArtifactsBlock from '~/jobs/components/artifacts_block.vue'; -import { getTimeago } from '~/lib/utils/datetime_utility'; - -describe('Artifacts block', () => { - let wrapper; - - const createWrapper = (propsData) => - mount(ArtifactsBlock, { - propsData: { - helpUrl: 'help-url', - ...propsData, - }, - }); - - const findArtifactRemoveElt = () => wrapper.find('[data-testid="artifacts-remove-timeline"]'); - const findJobLockedElt = () => wrapper.find('[data-testid="job-locked-message"]'); - const findKeepBtn = () => wrapper.find('[data-testid="keep-artifacts"]'); - const findDownloadBtn = () => wrapper.find('[data-testid="download-artifacts"]'); - const findBrowseBtn = () => wrapper.find('[data-testid="browse-artifacts"]'); - - const expireAt = '2018-08-14T09:38:49.157Z'; - const timeago = getTimeago(); - const formattedDate = timeago.format(expireAt); - const lockedText = - 'These artifacts are the latest. They will not be deleted (even if expired) until newer artifacts are available.'; - - const expiredArtifact = { - expire_at: expireAt, - expired: true, - locked: false, - }; - - const nonExpiredArtifact = { - download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download', - browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse', - keep_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/keep', - expire_at: expireAt, - expired: false, - locked: false, - }; - - const lockedExpiredArtifact = { - ...expiredArtifact, - download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download', - browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse', - expired: true, - locked: true, - }; - - const lockedNonExpiredArtifact = { - ...nonExpiredArtifact, - keep_path: undefined, - locked: true, - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('with expired artifacts that are not locked', () => { - beforeEach(() => { - wrapper = createWrapper({ - artifact: expiredArtifact, - }); - }); - - it('renders expired artifact date and info', () => { - expect(trimText(findArtifactRemoveElt().text())).toBe( - `The artifacts were removed ${formattedDate}`, - ); - - expect( - findArtifactRemoveElt() - .find('[data-testid="artifact-expired-help-link"]') - .attributes('href'), - ).toBe('help-url'); - }); - - it('does not show the keep button', () => { - expect(findKeepBtn().exists()).toBe(false); - }); - - it('does not show the download button', () => { - expect(findDownloadBtn().exists()).toBe(false); - }); - - it('does not show the browse button', () => { - expect(findBrowseBtn().exists()).toBe(false); - }); - }); - - describe('with artifacts that will expire', () => { - beforeEach(() => { - wrapper = createWrapper({ - artifact: nonExpiredArtifact, - }); - }); - - it('renders will expire artifact date and info', () => { - expect(trimText(findArtifactRemoveElt().text())).toBe( - `The artifacts will be removed ${formattedDate}`, - ); - - expect( - findArtifactRemoveElt() - .find('[data-testid="artifact-expired-help-link"]') - .attributes('href'), - ).toBe('help-url'); - }); - - it('renders the keep button', () => { - expect(findKeepBtn().exists()).toBe(true); - }); - - it('renders the download button', () => { - expect(findDownloadBtn().exists()).toBe(true); - }); - - it('renders the browse button', () => { - expect(findBrowseBtn().exists()).toBe(true); - }); - }); - - describe('with expired locked artifacts', () => { - beforeEach(() => { - wrapper = createWrapper({ - artifact: lockedExpiredArtifact, - }); - }); - - it('renders the information that the artefacts are locked', () => { - expect(findArtifactRemoveElt().exists()).toBe(false); - expect(trimText(findJobLockedElt().text())).toBe(lockedText); - }); - - it('does not render the keep button', () => { - expect(findKeepBtn().exists()).toBe(false); - }); - - it('renders the download button', () => { - expect(findDownloadBtn().exists()).toBe(true); - }); - - it('renders the browse button', () => { - expect(findBrowseBtn().exists()).toBe(true); - }); - }); - - describe('with non expired locked artifacts', () => { - beforeEach(() => { - wrapper = createWrapper({ - artifact: lockedNonExpiredArtifact, - }); - }); - - it('renders the information that the artefacts are locked', () => { - expect(findArtifactRemoveElt().exists()).toBe(false); - expect(trimText(findJobLockedElt().text())).toBe(lockedText); - }); - - it('does not render the keep button', () => { - expect(findKeepBtn().exists()).toBe(false); - }); - - it('renders the download button', () => { - expect(findDownloadBtn().exists()).toBe(true); - }); - - it('renders the browse button', () => { - expect(findBrowseBtn().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/jobs/components/commit_block_spec.js b/spec/frontend/jobs/components/commit_block_spec.js deleted file mode 100644 index 8a6d48cecb8..00000000000 --- a/spec/frontend/jobs/components/commit_block_spec.js +++ /dev/null @@ -1,70 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import CommitBlock from '~/jobs/components/commit_block.vue'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; - -describe('Commit block', () => { - let wrapper; - - const commit = { - short_id: '1f0fb84f', - id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c', - commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c', - title: 'Update README.md', - }; - - const mergeRequest = { - iid: '!21244', - path: 'merge_requests/21244', - }; - - const findCommitSha = () => wrapper.findByTestId('commit-sha'); - const findLinkSha = () => wrapper.findByTestId('link-commit'); - - const mountComponent = (props) => { - wrapper = extendedWrapper( - shallowMount(CommitBlock, { - propsData: { - commit, - ...props, - }, - }), - ); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('without merge request', () => { - beforeEach(() => { - mountComponent(); - }); - - it('renders pipeline short sha link', () => { - expect(findCommitSha().attributes('href')).toBe(commit.commit_path); - expect(findCommitSha().text()).toBe(commit.short_id); - }); - - it('renders clipboard button', () => { - expect(wrapper.findComponent(ClipboardButton).attributes('text')).toBe(commit.id); - }); - - it('renders git commit title', () => { - expect(wrapper.text()).toContain(commit.title); - }); - - it('does not render merge request', () => { - expect(findLinkSha().exists()).toBe(false); - }); - }); - - describe('with merge request', () => { - it('renders merge request link and reference', () => { - mountComponent({ mergeRequest }); - - expect(findLinkSha().attributes('href')).toBe(mergeRequest.path); - expect(findLinkSha().text()).toBe(`!${mergeRequest.iid}`); - }); - }); -}); diff --git a/spec/frontend/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js deleted file mode 100644 index 9738fd14275..00000000000 --- a/spec/frontend/jobs/components/empty_state_spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import { mount } from '@vue/test-utils'; -import EmptyState from '~/jobs/components/empty_state.vue'; - -describe('Empty State', () => { - let wrapper; - - const defaultProps = { - illustrationPath: 'illustrations/pending_job_empty.svg', - illustrationSizeClass: 'svg-430', - title: 'This job has not started yet', - playable: false, - }; - - const createWrapper = (props) => { - wrapper = mount(EmptyState, { - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - const content = 'This job is in pending state and is waiting to be picked by a runner'; - - const findEmptyStateImage = () => wrapper.find('img'); - const findTitle = () => wrapper.find('[data-testid="job-empty-state-title"]'); - const findContent = () => wrapper.find('[data-testid="job-empty-state-content"]'); - const findAction = () => wrapper.find('[data-testid="job-empty-state-action"]'); - const findManualVarsForm = () => wrapper.find('[data-testid="manual-vars-form"]'); - - afterEach(() => { - if (wrapper?.destroy) { - wrapper.destroy(); - wrapper = null; - } - }); - - describe('renders image and title', () => { - beforeEach(() => { - createWrapper(); - }); - - it('renders empty state image', () => { - expect(findEmptyStateImage().exists()).toBe(true); - }); - - it('renders provided title', () => { - expect(findTitle().text().trim()).toBe(defaultProps.title); - }); - }); - - describe('with content', () => { - beforeEach(() => { - createWrapper({ content }); - }); - - it('renders content', () => { - expect(findContent().text().trim()).toBe(content); - }); - }); - - describe('without content', () => { - beforeEach(() => { - createWrapper(); - }); - - it('does not render content', () => { - expect(findContent().exists()).toBe(false); - }); - }); - - describe('with action', () => { - beforeEach(() => { - createWrapper({ - action: { - path: 'runner', - button_title: 'Check runner', - method: 'post', - }, - }); - }); - - it('renders action', () => { - expect(findAction().attributes('href')).toBe('runner'); - }); - }); - - describe('without action', () => { - beforeEach(() => { - createWrapper({ - action: null, - }); - }); - - it('does not render action', () => { - expect(findAction().exists()).toBe(false); - }); - - it('does not render manual variables form', () => { - expect(findManualVarsForm().exists()).toBe(false); - }); - }); - - describe('with playable action and not scheduled job', () => { - beforeEach(() => { - createWrapper({ - content, - playable: true, - scheduled: false, - action: { - path: 'runner', - button_title: 'Check runner', - method: 'post', - }, - }); - }); - - it('renders manual variables form', () => { - expect(findManualVarsForm().exists()).toBe(true); - }); - - it('does not render the empty state action', () => { - expect(findAction().exists()).toBe(false); - }); - }); - - describe('with playable action and scheduled job', () => { - beforeEach(() => { - createWrapper({ - playable: true, - scheduled: true, - content, - }); - }); - - it('does not render manual variables form', () => { - expect(findManualVarsForm().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/jobs/components/environments_block_spec.js b/spec/frontend/jobs/components/environments_block_spec.js deleted file mode 100644 index d90c9137a8f..00000000000 --- a/spec/frontend/jobs/components/environments_block_spec.js +++ /dev/null @@ -1,265 +0,0 @@ -import { mount } from '@vue/test-utils'; -import EnvironmentsBlock from '~/jobs/components/environments_block.vue'; - -const TEST_CLUSTER_NAME = 'test_cluster'; -const TEST_CLUSTER_PATH = 'path/to/test_cluster'; -const TEST_KUBERNETES_NAMESPACE = 'this-is-a-kubernetes-namespace'; - -describe('Environments block', () => { - let wrapper; - - const status = { - group: 'success', - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed', - }; - - const environment = { - environment_path: '/environment', - name: 'environment', - }; - - const lastDeployment = { iid: 'deployment', deployable: { build_path: 'bar' } }; - - const createEnvironmentWithLastDeployment = () => ({ - ...environment, - last_deployment: { ...lastDeployment }, - }); - - const createDeploymentWithCluster = () => ({ name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH }); - - const createDeploymentWithClusterAndKubernetesNamespace = () => ({ - name: TEST_CLUSTER_NAME, - path: TEST_CLUSTER_PATH, - kubernetes_namespace: TEST_KUBERNETES_NAMESPACE, - }); - - const createComponent = (deploymentStatus = {}, deploymentCluster = {}) => { - wrapper = mount(EnvironmentsBlock, { - propsData: { - deploymentStatus, - deploymentCluster, - iconStatus: status, - }, - }); - }; - - const findText = () => wrapper.find(EnvironmentsBlock).text(); - const findJobDeploymentLink = () => wrapper.find('[data-testid="job-deployment-link"]'); - 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({ - status: 'last', - environment, - }); - - expect(findText()).toBe('This job is deployed to environment.'); - }); - - describe('when there is a cluster', () => { - it('renders info with cluster', () => { - createComponent( - { - status: 'last', - environment: createEnvironmentWithLastDeployment(), - }, - createDeploymentWithCluster(), - ); - - expect(findText()).toBe( - `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, - ); - }); - - describe('when there is a kubernetes namespace', () => { - it('renders info with cluster', () => { - createComponent( - { - status: 'last', - environment: createEnvironmentWithLastDeployment(), - }, - createDeploymentWithClusterAndKubernetesNamespace(), - ); - - expect(findText()).toBe( - `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}.`, - ); - }); - }); - }); - }); - - describe('with out of date deployment', () => { - describe('with last deployment', () => { - it('renders info for out date and most recent', () => { - createComponent({ - status: 'out_of_date', - environment: createEnvironmentWithLastDeployment(), - }); - - expect(findText()).toBe( - 'This job is an out-of-date deployment to environment. View the most recent deployment.', - ); - - expect(findJobDeploymentLink().attributes('href')).toBe('bar'); - }); - - describe('when there is a cluster', () => { - it('renders info with cluster', () => { - createComponent( - { - status: 'out_of_date', - environment: createEnvironmentWithLastDeployment(), - }, - createDeploymentWithCluster(), - ); - - expect(findText()).toBe( - `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`, - ); - }); - - describe('when there is a kubernetes namespace', () => { - it('renders info with cluster', () => { - createComponent( - { - status: 'out_of_date', - environment: createEnvironmentWithLastDeployment(), - }, - createDeploymentWithClusterAndKubernetesNamespace(), - ); - - expect(findText()).toBe( - `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}. View the most recent deployment.`, - ); - }); - }); - }); - }); - - describe('without last deployment', () => { - it('renders info about out of date deployment', () => { - createComponent({ - status: 'out_of_date', - environment, - }); - - expect(findText()).toBe('This job is an out-of-date deployment to environment.'); - }); - }); - }); - - describe('with failed deployment', () => { - it('renders info about failed deployment', () => { - createComponent({ - status: 'failed', - environment, - }); - - expect(findText()).toBe('The deployment of this job to environment did not succeed.'); - }); - }); - - describe('creating deployment', () => { - describe('with last deployment', () => { - it('renders info about creating deployment and overriding latest deployment', () => { - createComponent({ - status: 'creating', - environment: createEnvironmentWithLastDeployment(), - }); - - expect(findText()).toBe( - 'This job is creating a deployment to environment. This will overwrite the latest deployment.', - ); - - expect(findEnvironmentLink().attributes('href')).toBe(environment.environment_path); - - expect(findJobDeploymentLink().attributes('href')).toBe('bar'); - - expect(findClusterLink().exists()).toBe(false); - }); - }); - - describe('without last deployment', () => { - it('renders info about deployment being created', () => { - createComponent({ - status: 'creating', - environment, - }); - - expect(findText()).toBe('This job is creating a deployment to environment.'); - }); - - describe('when there is a cluster', () => { - it('inclues information about the cluster', () => { - createComponent( - { - status: 'creating', - environment, - }, - createDeploymentWithCluster(), - ); - - expect(findText()).toBe( - `This job is creating a deployment to environment using cluster ${TEST_CLUSTER_NAME}.`, - ); - }); - }); - }); - - describe('without environment', () => { - it('does not render environment link', () => { - createComponent({ - status: 'creating', - environment: null, - }); - - expect(findEnvironmentLink().exists()).toBe(false); - }); - }); - }); - - describe('with a cluster', () => { - it('renders the cluster link', () => { - createComponent( - { - status: 'last', - environment: createEnvironmentWithLastDeployment(), - }, - createDeploymentWithCluster(), - ); - - expect(findText()).toBe( - `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, - ); - - expect(findClusterLink().attributes('href')).toBe(TEST_CLUSTER_PATH); - }); - - describe('when the cluster is missing the path', () => { - it('renders the name without a link', () => { - createComponent( - { - status: 'last', - environment: createEnvironmentWithLastDeployment(), - }, - { name: 'the-cluster' }, - ); - - expect(findText()).toContain('using cluster the-cluster.'); - - expect(findClusterLink().exists()).toBe(false); - }); - }); - }); -}); diff --git a/spec/frontend/jobs/components/erased_block_spec.js b/spec/frontend/jobs/components/erased_block_spec.js deleted file mode 100644 index 057df20ccc2..00000000000 --- a/spec/frontend/jobs/components/erased_block_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import { GlLink } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import ErasedBlock from '~/jobs/components/erased_block.vue'; -import { getTimeago } from '~/lib/utils/datetime_utility'; - -describe('Erased block', () => { - let wrapper; - - const erasedAt = '2016-11-07T11:11:16.525Z'; - const timeago = getTimeago(); - const formattedDate = timeago.format(erasedAt); - - const findLink = () => wrapper.find(GlLink); - - const createComponent = (props) => { - wrapper = mount(ErasedBlock, { - propsData: props, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('with job erased by user', () => { - beforeEach(() => { - createComponent({ - user: { - username: 'root', - web_url: 'gitlab.com/root', - }, - erasedAt, - }); - }); - - it('renders username and link', () => { - expect(findLink().attributes('href')).toEqual('gitlab.com/root'); - - expect(wrapper.text().trim()).toContain('Job has been erased by'); - expect(wrapper.text().trim()).toContain('root'); - }); - - it('renders erasedAt', () => { - expect(wrapper.text().trim()).toContain(formattedDate); - }); - }); - - describe('with erased job', () => { - beforeEach(() => { - createComponent({ - erasedAt, - }); - }); - - it('renders username and link', () => { - expect(wrapper.text().trim()).toContain('Job has been erased'); - }); - - it('renders erasedAt', () => { - expect(wrapper.text().trim()).toContain(formattedDate); - }); - }); -}); 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 322cfa3ba1f..98bdfc3fcbc 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 @@ -15,23 +15,27 @@ describe('Jobs filtered search', () => { const findStatusToken = () => getSearchToken('status'); - const createComponent = () => { - wrapper = shallowMount(JobsFilteredSearch); + const createComponent = (props) => { + wrapper = shallowMount(JobsFilteredSearch, { + propsData: { + ...props, + }, + }); }; - beforeEach(() => { - createComponent(); - }); - afterEach(() => { wrapper.destroy(); }); it('displays filtered search', () => { + createComponent(); + expect(findFilteredSearch().exists()).toBe(true); }); it('displays status token', () => { + createComponent(); + expect(findStatusToken()).toMatchObject({ type: 'status', icon: 'status', @@ -42,8 +46,26 @@ describe('Jobs filtered search', () => { }); it('emits filter token to parent component', () => { + createComponent(); + findFilteredSearch().vm.$emit('submit', mockFailedSearchToken); expect(wrapper.emitted('filterJobsBySearch')).toEqual([[mockFailedSearchToken]]); }); + + it('filtered search value is empty array when no query string is passed', () => { + createComponent(); + + expect(findFilteredSearch().props('value')).toEqual([]); + }); + + it('filtered search returns correct data shape when passed query string', () => { + const value = 'SUCCESS'; + + createComponent({ queryString: { statuses: value } }); + + expect(findFilteredSearch().props('value')).toEqual([ + { type: 'status', value: { data: value, operator: '=' } }, + ]); + }); }); diff --git a/spec/frontend/jobs/components/filtered_search/utils_spec.js b/spec/frontend/jobs/components/filtered_search/utils_spec.js new file mode 100644 index 00000000000..8440ab42b86 --- /dev/null +++ b/spec/frontend/jobs/components/filtered_search/utils_spec.js @@ -0,0 +1,19 @@ +import { validateQueryString } from '~/jobs/components/filtered_search/utils'; + +describe('Filtered search utils', () => { + describe('validateQueryString', () => { + it.each` + queryStringObject | expected + ${{ statuses: 'SUCCESS' }} | ${{ statuses: 'SUCCESS' }} + ${{ statuses: 'failed' }} | ${{ statuses: 'FAILED' }} + ${{ wrong: 'SUCCESS' }} | ${null} + ${{ statuses: 'wrong' }} | ${null} + ${{ wrong: 'wrong' }} | ${null} + `( + 'when provided $queryStringObject, the expected result is $expected', + ({ queryStringObject, expected }) => { + expect(validateQueryString(queryStringObject)).toEqual(expected); + }, + ); + }); +}); diff --git a/spec/frontend/jobs/components/job/artifacts_block_spec.js b/spec/frontend/jobs/components/job/artifacts_block_spec.js new file mode 100644 index 00000000000..c75deb64d84 --- /dev/null +++ b/spec/frontend/jobs/components/job/artifacts_block_spec.js @@ -0,0 +1,176 @@ +import { mount } from '@vue/test-utils'; +import { trimText } from 'helpers/text_helper'; +import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; + +describe('Artifacts block', () => { + let wrapper; + + const createWrapper = (propsData) => + mount(ArtifactsBlock, { + propsData: { + helpUrl: 'help-url', + ...propsData, + }, + }); + + const findArtifactRemoveElt = () => wrapper.find('[data-testid="artifacts-remove-timeline"]'); + const findJobLockedElt = () => wrapper.find('[data-testid="job-locked-message"]'); + const findKeepBtn = () => wrapper.find('[data-testid="keep-artifacts"]'); + const findDownloadBtn = () => wrapper.find('[data-testid="download-artifacts"]'); + const findBrowseBtn = () => wrapper.find('[data-testid="browse-artifacts"]'); + + const expireAt = '2018-08-14T09:38:49.157Z'; + const timeago = getTimeago(); + const formattedDate = timeago.format(expireAt); + const lockedText = + 'These artifacts are the latest. They will not be deleted (even if expired) until newer artifacts are available.'; + + const expiredArtifact = { + expire_at: expireAt, + expired: true, + locked: false, + }; + + const nonExpiredArtifact = { + download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download', + browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse', + keep_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/keep', + expire_at: expireAt, + expired: false, + locked: false, + }; + + const lockedExpiredArtifact = { + ...expiredArtifact, + download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download', + browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse', + expired: true, + locked: true, + }; + + const lockedNonExpiredArtifact = { + ...nonExpiredArtifact, + keep_path: undefined, + locked: true, + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('with expired artifacts that are not locked', () => { + beforeEach(() => { + wrapper = createWrapper({ + artifact: expiredArtifact, + }); + }); + + it('renders expired artifact date and info', () => { + expect(trimText(findArtifactRemoveElt().text())).toBe( + `The artifacts were removed ${formattedDate}`, + ); + + expect( + findArtifactRemoveElt() + .find('[data-testid="artifact-expired-help-link"]') + .attributes('href'), + ).toBe('help-url'); + }); + + it('does not show the keep button', () => { + expect(findKeepBtn().exists()).toBe(false); + }); + + it('does not show the download button', () => { + expect(findDownloadBtn().exists()).toBe(false); + }); + + it('does not show the browse button', () => { + expect(findBrowseBtn().exists()).toBe(false); + }); + }); + + describe('with artifacts that will expire', () => { + beforeEach(() => { + wrapper = createWrapper({ + artifact: nonExpiredArtifact, + }); + }); + + it('renders will expire artifact date and info', () => { + expect(trimText(findArtifactRemoveElt().text())).toBe( + `The artifacts will be removed ${formattedDate}`, + ); + + expect( + findArtifactRemoveElt() + .find('[data-testid="artifact-expired-help-link"]') + .attributes('href'), + ).toBe('help-url'); + }); + + it('renders the keep button', () => { + expect(findKeepBtn().exists()).toBe(true); + }); + + it('renders the download button', () => { + expect(findDownloadBtn().exists()).toBe(true); + }); + + it('renders the browse button', () => { + expect(findBrowseBtn().exists()).toBe(true); + }); + }); + + describe('with expired locked artifacts', () => { + beforeEach(() => { + wrapper = createWrapper({ + artifact: lockedExpiredArtifact, + }); + }); + + it('renders the information that the artefacts are locked', () => { + expect(findArtifactRemoveElt().exists()).toBe(false); + expect(trimText(findJobLockedElt().text())).toBe(lockedText); + }); + + it('does not render the keep button', () => { + expect(findKeepBtn().exists()).toBe(false); + }); + + it('renders the download button', () => { + expect(findDownloadBtn().exists()).toBe(true); + }); + + it('renders the browse button', () => { + expect(findBrowseBtn().exists()).toBe(true); + }); + }); + + describe('with non expired locked artifacts', () => { + beforeEach(() => { + wrapper = createWrapper({ + artifact: lockedNonExpiredArtifact, + }); + }); + + it('renders the information that the artefacts are locked', () => { + expect(findArtifactRemoveElt().exists()).toBe(false); + expect(trimText(findJobLockedElt().text())).toBe(lockedText); + }); + + it('does not render the keep button', () => { + expect(findKeepBtn().exists()).toBe(false); + }); + + it('renders the download button', () => { + expect(findDownloadBtn().exists()).toBe(true); + }); + + it('renders the browse button', () => { + expect(findBrowseBtn().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/commit_block_spec.js b/spec/frontend/jobs/components/job/commit_block_spec.js new file mode 100644 index 00000000000..4fcc754c82c --- /dev/null +++ b/spec/frontend/jobs/components/job/commit_block_spec.js @@ -0,0 +1,70 @@ +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import CommitBlock from '~/jobs/components/job/sidebar/commit_block.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +describe('Commit block', () => { + let wrapper; + + const commit = { + short_id: '1f0fb84f', + id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c', + commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c', + title: 'Update README.md', + }; + + const mergeRequest = { + iid: '!21244', + path: 'merge_requests/21244', + }; + + const findCommitSha = () => wrapper.findByTestId('commit-sha'); + const findLinkSha = () => wrapper.findByTestId('link-commit'); + + const mountComponent = (props) => { + wrapper = extendedWrapper( + shallowMount(CommitBlock, { + propsData: { + commit, + ...props, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('without merge request', () => { + beforeEach(() => { + mountComponent(); + }); + + it('renders pipeline short sha link', () => { + expect(findCommitSha().attributes('href')).toBe(commit.commit_path); + expect(findCommitSha().text()).toBe(commit.short_id); + }); + + it('renders clipboard button', () => { + expect(wrapper.findComponent(ClipboardButton).attributes('text')).toBe(commit.id); + }); + + it('renders git commit title', () => { + expect(wrapper.text()).toContain(commit.title); + }); + + it('does not render merge request', () => { + expect(findLinkSha().exists()).toBe(false); + }); + }); + + describe('with merge request', () => { + it('renders merge request link and reference', () => { + mountComponent({ mergeRequest }); + + expect(findLinkSha().attributes('href')).toBe(mergeRequest.path); + expect(findLinkSha().text()).toBe(`!${mergeRequest.iid}`); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/empty_state_spec.js b/spec/frontend/jobs/components/job/empty_state_spec.js new file mode 100644 index 00000000000..299b607ad78 --- /dev/null +++ b/spec/frontend/jobs/components/job/empty_state_spec.js @@ -0,0 +1,140 @@ +import { mount } from '@vue/test-utils'; +import EmptyState from '~/jobs/components/job/empty_state.vue'; + +describe('Empty State', () => { + let wrapper; + + const defaultProps = { + illustrationPath: 'illustrations/pending_job_empty.svg', + illustrationSizeClass: 'svg-430', + title: 'This job has not started yet', + playable: false, + }; + + const createWrapper = (props) => { + wrapper = mount(EmptyState, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + const content = 'This job is in pending state and is waiting to be picked by a runner'; + + const findEmptyStateImage = () => wrapper.find('img'); + const findTitle = () => wrapper.find('[data-testid="job-empty-state-title"]'); + const findContent = () => wrapper.find('[data-testid="job-empty-state-content"]'); + const findAction = () => wrapper.find('[data-testid="job-empty-state-action"]'); + const findManualVarsForm = () => wrapper.find('[data-testid="manual-vars-form"]'); + + afterEach(() => { + if (wrapper?.destroy) { + wrapper.destroy(); + wrapper = null; + } + }); + + describe('renders image and title', () => { + beforeEach(() => { + createWrapper(); + }); + + it('renders empty state image', () => { + expect(findEmptyStateImage().exists()).toBe(true); + }); + + it('renders provided title', () => { + expect(findTitle().text().trim()).toBe(defaultProps.title); + }); + }); + + describe('with content', () => { + beforeEach(() => { + createWrapper({ content }); + }); + + it('renders content', () => { + expect(findContent().text().trim()).toBe(content); + }); + }); + + describe('without content', () => { + beforeEach(() => { + createWrapper(); + }); + + it('does not render content', () => { + expect(findContent().exists()).toBe(false); + }); + }); + + describe('with action', () => { + beforeEach(() => { + createWrapper({ + action: { + path: 'runner', + button_title: 'Check runner', + method: 'post', + }, + }); + }); + + it('renders action', () => { + expect(findAction().attributes('href')).toBe('runner'); + }); + }); + + describe('without action', () => { + beforeEach(() => { + createWrapper({ + action: null, + }); + }); + + it('does not render action', () => { + expect(findAction().exists()).toBe(false); + }); + + it('does not render manual variables form', () => { + expect(findManualVarsForm().exists()).toBe(false); + }); + }); + + describe('with playable action and not scheduled job', () => { + beforeEach(() => { + createWrapper({ + content, + playable: true, + scheduled: false, + action: { + path: 'runner', + button_title: 'Check runner', + method: 'post', + }, + }); + }); + + it('renders manual variables form', () => { + expect(findManualVarsForm().exists()).toBe(true); + }); + + it('does not render the empty state action', () => { + expect(findAction().exists()).toBe(false); + }); + }); + + describe('with playable action and scheduled job', () => { + beforeEach(() => { + createWrapper({ + playable: true, + scheduled: true, + content, + }); + }); + + it('does not render manual variables form', () => { + expect(findManualVarsForm().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/environments_block_spec.js b/spec/frontend/jobs/components/job/environments_block_spec.js new file mode 100644 index 00000000000..134533e2af8 --- /dev/null +++ b/spec/frontend/jobs/components/job/environments_block_spec.js @@ -0,0 +1,265 @@ +import { mount } from '@vue/test-utils'; +import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue'; + +const TEST_CLUSTER_NAME = 'test_cluster'; +const TEST_CLUSTER_PATH = 'path/to/test_cluster'; +const TEST_KUBERNETES_NAMESPACE = 'this-is-a-kubernetes-namespace'; + +describe('Environments block', () => { + let wrapper; + + const status = { + group: 'success', + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }; + + const environment = { + environment_path: '/environment', + name: 'environment', + }; + + const lastDeployment = { iid: 'deployment', deployable: { build_path: 'bar' } }; + + const createEnvironmentWithLastDeployment = () => ({ + ...environment, + last_deployment: { ...lastDeployment }, + }); + + const createDeploymentWithCluster = () => ({ name: TEST_CLUSTER_NAME, path: TEST_CLUSTER_PATH }); + + const createDeploymentWithClusterAndKubernetesNamespace = () => ({ + name: TEST_CLUSTER_NAME, + path: TEST_CLUSTER_PATH, + kubernetes_namespace: TEST_KUBERNETES_NAMESPACE, + }); + + const createComponent = (deploymentStatus = {}, deploymentCluster = {}) => { + wrapper = mount(EnvironmentsBlock, { + propsData: { + deploymentStatus, + deploymentCluster, + iconStatus: status, + }, + }); + }; + + const findText = () => wrapper.findComponent(EnvironmentsBlock).text(); + const findJobDeploymentLink = () => wrapper.find('[data-testid="job-deployment-link"]'); + 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({ + status: 'last', + environment, + }); + + expect(findText()).toBe('This job is deployed to environment.'); + }); + + describe('when there is a cluster', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toBe( + `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, + ); + }); + + describe('when there is a kubernetes namespace', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithClusterAndKubernetesNamespace(), + ); + + expect(findText()).toBe( + `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}.`, + ); + }); + }); + }); + }); + + describe('with out of date deployment', () => { + describe('with last deployment', () => { + it('renders info for out date and most recent', () => { + createComponent({ + status: 'out_of_date', + environment: createEnvironmentWithLastDeployment(), + }); + + expect(findText()).toBe( + 'This job is an out-of-date deployment to environment. View the most recent deployment.', + ); + + expect(findJobDeploymentLink().attributes('href')).toBe('bar'); + }); + + describe('when there is a cluster', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'out_of_date', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toBe( + `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME}. View the most recent deployment.`, + ); + }); + + describe('when there is a kubernetes namespace', () => { + it('renders info with cluster', () => { + createComponent( + { + status: 'out_of_date', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithClusterAndKubernetesNamespace(), + ); + + expect(findText()).toBe( + `This job is an out-of-date deployment to environment using cluster ${TEST_CLUSTER_NAME} and namespace ${TEST_KUBERNETES_NAMESPACE}. View the most recent deployment.`, + ); + }); + }); + }); + }); + + describe('without last deployment', () => { + it('renders info about out of date deployment', () => { + createComponent({ + status: 'out_of_date', + environment, + }); + + expect(findText()).toBe('This job is an out-of-date deployment to environment.'); + }); + }); + }); + + describe('with failed deployment', () => { + it('renders info about failed deployment', () => { + createComponent({ + status: 'failed', + environment, + }); + + expect(findText()).toBe('The deployment of this job to environment did not succeed.'); + }); + }); + + describe('creating deployment', () => { + describe('with last deployment', () => { + it('renders info about creating deployment and overriding latest deployment', () => { + createComponent({ + status: 'creating', + environment: createEnvironmentWithLastDeployment(), + }); + + expect(findText()).toBe( + 'This job is creating a deployment to environment. This will overwrite the latest deployment.', + ); + + expect(findEnvironmentLink().attributes('href')).toBe(environment.environment_path); + + expect(findJobDeploymentLink().attributes('href')).toBe('bar'); + + expect(findClusterLink().exists()).toBe(false); + }); + }); + + describe('without last deployment', () => { + it('renders info about deployment being created', () => { + createComponent({ + status: 'creating', + environment, + }); + + expect(findText()).toBe('This job is creating a deployment to environment.'); + }); + + describe('when there is a cluster', () => { + it('inclues information about the cluster', () => { + createComponent( + { + status: 'creating', + environment, + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toBe( + `This job is creating a deployment to environment using cluster ${TEST_CLUSTER_NAME}.`, + ); + }); + }); + }); + + describe('without environment', () => { + it('does not render environment link', () => { + createComponent({ + status: 'creating', + environment: null, + }); + + expect(findEnvironmentLink().exists()).toBe(false); + }); + }); + }); + + describe('with a cluster', () => { + it('renders the cluster link', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + createDeploymentWithCluster(), + ); + + expect(findText()).toBe( + `This job is deployed to environment using cluster ${TEST_CLUSTER_NAME}.`, + ); + + expect(findClusterLink().attributes('href')).toBe(TEST_CLUSTER_PATH); + }); + + describe('when the cluster is missing the path', () => { + it('renders the name without a link', () => { + createComponent( + { + status: 'last', + environment: createEnvironmentWithLastDeployment(), + }, + { name: 'the-cluster' }, + ); + + expect(findText()).toContain('using cluster the-cluster.'); + + expect(findClusterLink().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/erased_block_spec.js b/spec/frontend/jobs/components/job/erased_block_spec.js new file mode 100644 index 00000000000..c6aba01fa53 --- /dev/null +++ b/spec/frontend/jobs/components/job/erased_block_spec.js @@ -0,0 +1,63 @@ +import { GlLink } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import ErasedBlock from '~/jobs/components/job/erased_block.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; + +describe('Erased block', () => { + let wrapper; + + const erasedAt = '2016-11-07T11:11:16.525Z'; + const timeago = getTimeago(); + const formattedDate = timeago.format(erasedAt); + + const findLink = () => wrapper.findComponent(GlLink); + + const createComponent = (props) => { + wrapper = mount(ErasedBlock, { + propsData: props, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('with job erased by user', () => { + beforeEach(() => { + createComponent({ + user: { + username: 'root', + web_url: 'gitlab.com/root', + }, + erasedAt, + }); + }); + + it('renders username and link', () => { + expect(findLink().attributes('href')).toEqual('gitlab.com/root'); + + expect(wrapper.text().trim()).toContain('Job has been erased by'); + expect(wrapper.text().trim()).toContain('root'); + }); + + it('renders erasedAt', () => { + expect(wrapper.text().trim()).toContain(formattedDate); + }); + }); + + describe('with erased job', () => { + beforeEach(() => { + createComponent({ + erasedAt, + }); + }); + + it('renders username and link', () => { + expect(wrapper.text().trim()).toContain('Job has been erased'); + }); + + it('renders erasedAt', () => { + expect(wrapper.text().trim()).toContain(formattedDate); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js new file mode 100644 index 00000000000..822528403cf --- /dev/null +++ b/spec/frontend/jobs/components/job/job_app_spec.js @@ -0,0 +1,440 @@ +import { GlLoadingIcon } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import Vuex from 'vuex'; +import delayedJobFixture from 'test_fixtures/jobs/delayed.json'; +import { TEST_HOST } from 'helpers/test_constants'; +import EmptyState from '~/jobs/components/job/empty_state.vue'; +import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue'; +import ErasedBlock from '~/jobs/components/job/erased_block.vue'; +import JobApp from '~/jobs/components/job/job_app.vue'; +import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue'; +import StuckBlock from '~/jobs/components/job/stuck_block.vue'; +import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue'; +import createStore from '~/jobs/store'; +import axios from '~/lib/utils/axios_utils'; +import job from '../../mock_data'; + +describe('Job App', () => { + Vue.use(Vuex); + + let store; + let wrapper; + let mock; + + const initSettings = { + endpoint: `${TEST_HOST}jobs/123.json`, + pagePath: `${TEST_HOST}jobs/123`, + logState: + 'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D', + }; + + const props = { + artifactHelpUrl: 'help/artifact', + deploymentHelpUrl: 'help/deployment', + runnerSettingsUrl: 'settings/ci-cd/runners', + terminalPath: 'jobs/123/terminal', + projectPath: 'user-name/project-name', + subscriptionsMoreMinutesUrl: 'https://customers.gitlab.com/buy_pipeline_minutes', + }; + + const createComponent = () => { + wrapper = mount(JobApp, { propsData: { ...props }, store }); + }; + + const setupAndMount = async ({ jobData = {}, jobLogData = {} } = {}) => { + mock.onGet(initSettings.endpoint).replyOnce(200, { ...job, ...jobData }); + mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, jobLogData); + + const asyncInit = store.dispatch('init', initSettings); + + createComponent(); + + await asyncInit; + jest.runOnlyPendingTimers(); + await axios.waitForAll(); + await nextTick(); + }; + + const findLoadingComponent = () => wrapper.findComponent(GlLoadingIcon); + const findSidebar = () => wrapper.findComponent(Sidebar); + const findJobContent = () => wrapper.find('[data-testid="job-content"'); + const findStuckBlockComponent = () => wrapper.findComponent(StuckBlock); + const findStuckBlockWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"'); + const findStuckBlockNoActiveRunners = () => + wrapper.find('[data-testid="job-stuck-no-active-runners"'); + const findFailedJobComponent = () => wrapper.findComponent(UnmetPrerequisitesBlock); + const findEnvironmentsBlockComponent = () => wrapper.findComponent(EnvironmentsBlock); + const findErasedBlock = () => wrapper.findComponent(ErasedBlock); + const findArchivedJob = () => wrapper.find('[data-testid="archived-job"]'); + const findEmptyState = () => wrapper.findComponent(EmptyState); + const findJobNewIssueLink = () => wrapper.find('[data-testid="job-new-issue"]'); + const findJobEmptyStateTitle = () => wrapper.find('[data-testid="job-empty-state-title"]'); + const findJobLogScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]'); + const findJobLogScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); + const findJobLogController = () => wrapper.find('[data-testid="job-raw-link-controller"]'); + const findJobLogEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]'); + + beforeEach(() => { + mock = new MockAdapter(axios); + store = createStore(); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('while loading', () => { + beforeEach(() => { + store.state.isLoading = true; + createComponent(); + }); + + it('renders loading icon', () => { + expect(findLoadingComponent().exists()).toBe(true); + expect(findSidebar().exists()).toBe(false); + expect(findJobContent().exists()).toBe(false); + }); + }); + + describe('with successful request', () => { + describe('Header section', () => { + describe('job callout message', () => { + it('should not render the reason when reason is absent', () => + setupAndMount().then(() => { + expect(wrapper.vm.shouldRenderCalloutMessage).toBe(false); + })); + + it('should render the reason when reason is present', () => + setupAndMount({ + jobData: { + callout_message: 'There is an unkown failure, please try again', + }, + }).then(() => { + expect(wrapper.vm.shouldRenderCalloutMessage).toBe(true); + })); + }); + + describe('triggered job', () => { + beforeEach(() => { + const aYearAgo = new Date(); + aYearAgo.setFullYear(aYearAgo.getFullYear() - 1); + + return setupAndMount({ + jobData: { started: aYearAgo.toISOString(), started_at: aYearAgo.toISOString() }, + }); + }); + + it('should render provided job information', () => { + expect(wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim()).toContain( + 'passed Job test triggered 1 year ago by Root', + ); + }); + + it('should render new issue link', () => { + expect(findJobNewIssueLink().attributes('href')).toEqual(job.new_issue_path); + }); + }); + + describe('created job', () => { + it('should render created key', () => + setupAndMount().then(() => { + expect( + wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim(), + ).toContain('passed Job test created 3 weeks ago by Root'); + })); + }); + }); + + describe('stuck block', () => { + describe('without active runners available', () => { + it('renders stuck block when there are no runners', () => + setupAndMount({ + jobData: { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: false, + online: false, + }, + tags: [], + }, + }).then(() => { + expect(findStuckBlockComponent().exists()).toBe(true); + expect(findStuckBlockNoActiveRunners().exists()).toBe(true); + })); + }); + + describe('when available runners can not run specified tag', () => { + it('renders tags in stuck block when there are no runners', () => + setupAndMount({ + jobData: { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: false, + online: false, + }, + }, + }).then(() => { + expect(findStuckBlockComponent().text()).toContain(job.tags[0]); + expect(findStuckBlockWithTags().exists()).toBe(true); + })); + }); + + describe('when runners are offline and build has tags', () => { + it('renders message about job being stuck because of no runners with the specified tags', () => + setupAndMount({ + jobData: { + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + }, + stuck: true, + runners: { + available: true, + online: true, + }, + }, + }).then(() => { + expect(findStuckBlockComponent().text()).toContain(job.tags[0]); + expect(findStuckBlockWithTags().exists()).toBe(true); + })); + }); + + it('does not renders stuck block when there are no runners', () => + setupAndMount({ + jobData: { + runners: { available: true }, + }, + }).then(() => { + expect(findStuckBlockComponent().exists()).toBe(false); + })); + }); + + describe('unmet prerequisites block', () => { + it('renders unmet prerequisites block when there is an unmet prerequisites failure', () => + setupAndMount({ + jobData: { + status: { + group: 'failed', + icon: 'status_failed', + label: 'failed', + text: 'failed', + details_path: 'path', + illustration: { + content: 'Retry this job in order to create the necessary resources.', + image: 'path', + size: 'svg-430', + title: 'Failed to create resources', + }, + }, + failure_reason: 'unmet_prerequisites', + has_trace: false, + runners: { + available: true, + }, + tags: [], + }, + }).then(() => { + expect(findFailedJobComponent().exists()).toBe(true); + })); + }); + + describe('environments block', () => { + it('renders environment block when job has environment', () => + setupAndMount({ + jobData: { + deployment_status: { + environment: { + environment_path: '/path', + name: 'foo', + }, + }, + }, + }).then(() => { + expect(findEnvironmentsBlockComponent().exists()).toBe(true); + })); + + it('does not render environment block when job has environment', () => + setupAndMount().then(() => { + expect(findEnvironmentsBlockComponent().exists()).toBe(false); + })); + }); + + describe('erased block', () => { + it('renders erased block when `erased` is true', () => + setupAndMount({ + jobData: { + erased_by: { + username: 'root', + web_url: 'gitlab.com/root', + }, + erased_at: '2016-11-07T11:11:16.525Z', + }, + }).then(() => { + expect(findErasedBlock().exists()).toBe(true); + })); + + it('does not render erased block when `erased` is false', () => + setupAndMount({ + jobData: { + erased_at: null, + }, + }).then(() => { + expect(findErasedBlock().exists()).toBe(false); + })); + }); + + describe('empty states block', () => { + it('renders empty state when job does not have log and is not running', () => + setupAndMount({ + jobData: { + has_trace: false, + status: { + group: 'pending', + icon: 'status_pending', + label: 'pending', + text: 'pending', + details_path: 'path', + illustration: { + image: 'path', + size: '340', + title: 'Empty State', + content: 'This is an empty state', + }, + action: { + button_title: 'Retry job', + method: 'post', + path: '/path', + }, + }, + }, + }).then(() => { + expect(findEmptyState().exists()).toBe(true); + })); + + it('does not render empty state when job does not have log but it is running', () => + setupAndMount({ + jobData: { + has_trace: false, + status: { + group: 'running', + icon: 'status_running', + label: 'running', + text: 'running', + details_path: 'path', + }, + }, + }).then(() => { + expect(findEmptyState().exists()).toBe(false); + })); + + it('does not render empty state when job has log but it is not running', () => + setupAndMount({ jobData: { has_trace: true } }).then(() => { + expect(findEmptyState().exists()).toBe(false); + })); + + it('displays remaining time for a delayed job', () => { + const oneHourInMilliseconds = 3600000; + jest + .spyOn(Date, 'now') + .mockImplementation( + () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds, + ); + return setupAndMount({ jobData: delayedJobFixture }).then(() => { + expect(findEmptyState().exists()).toBe(true); + + const title = findJobEmptyStateTitle().text(); + + expect(title).toEqual('This is a delayed job to run in 01:00:00'); + }); + }); + }); + + describe('sidebar', () => { + it('has no blank blocks', async () => { + await setupAndMount({ + jobData: { + duration: null, + finished_at: null, + erased_at: null, + queued: null, + runner: null, + coverage: null, + tags: [], + cancel_path: null, + }, + }); + + const blocks = wrapper.findAll('.blocks-container > *').wrappers; + expect(blocks.length).toBeGreaterThan(0); + + blocks.forEach((block) => { + expect(block.text().trim()).not.toBe(''); + }); + }); + }); + }); + + describe('archived job', () => { + beforeEach(() => setupAndMount({ jobData: { archived: true } })); + + it('renders warning about job being archived', () => { + expect(findArchivedJob().exists()).toBe(true); + }); + }); + + describe('non-archived job', () => { + beforeEach(() => setupAndMount()); + + it('does not warning about job being archived', () => { + expect(findArchivedJob().exists()).toBe(false); + }); + }); + + describe('job log controls', () => { + beforeEach(() => + setupAndMount({ + jobLogData: { + html: 'Update', + status: 'success', + append: false, + size: 50, + total: 100, + complete: true, + }, + }), + ); + + it('should render scroll buttons', () => { + expect(findJobLogScrollTop().exists()).toBe(true); + expect(findJobLogScrollBottom().exists()).toBe(true); + }); + + it('should render link to raw ouput', () => { + expect(findJobLogController().exists()).toBe(true); + }); + + it('should render link to erase job', () => { + expect(findJobLogEraseLink().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/job_container_item_spec.js b/spec/frontend/jobs/components/job/job_container_item_spec.js new file mode 100644 index 00000000000..05c38dd74b7 --- /dev/null +++ b/spec/frontend/jobs/components/job/job_container_item_spec.js @@ -0,0 +1,98 @@ +import { GlIcon, GlLink } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import delayedJobFixture from 'test_fixtures/jobs/delayed.json'; +import JobContainerItem from '~/jobs/components/job/sidebar/job_container_item.vue'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import job from '../../mock_data'; + +describe('JobContainerItem', () => { + let wrapper; + + const findCiIconComponent = () => wrapper.findComponent(CiIcon); + const findGlIconComponent = () => wrapper.findComponent(GlIcon); + + function createComponent(jobData = {}, props = { isActive: false, retried: false }) { + wrapper = shallowMount(JobContainerItem, { + propsData: { + job: { + ...jobData, + retried: props.retried, + }, + isActive: props.isActive, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when a job is not active and not retried', () => { + beforeEach(() => { + createComponent(job); + }); + + it('displays a status icon', () => { + const ciIcon = findCiIconComponent(); + + expect(ciIcon.props('status')).toBe(job.status); + }); + + it('displays the job name', () => { + expect(wrapper.text()).toContain(job.name); + }); + + it('displays a link to the job', () => { + const link = wrapper.findComponent(GlLink); + + expect(link.attributes('href')).toBe(job.status.details_path); + }); + }); + + describe('when a job is active', () => { + beforeEach(() => { + createComponent(job, { isActive: true }); + }); + + it('displays an arrow sprite icon', () => { + const icon = findGlIconComponent(); + + expect(icon.props('name')).toBe('arrow-right'); + }); + }); + + describe('when a job is retried', () => { + beforeEach(() => { + createComponent(job, { isActive: false, retried: true }); + }); + + it('displays a retry icon', () => { + const icon = findGlIconComponent(); + + expect(icon.props('name')).toBe('retry'); + }); + }); + + describe('for a delayed job', () => { + beforeEach(() => { + const remainingMilliseconds = 1337000; + jest + .spyOn(Date, 'now') + .mockImplementation( + () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds, + ); + + createComponent(delayedJobFixture); + }); + + it('displays remaining time in tooltip', async () => { + await nextTick(); + + const link = wrapper.findComponent(GlLink); + + expect(link.attributes('title')).toMatch('delayed job - delayed manual action (00:22:17)'); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/job_log_controllers_spec.js b/spec/frontend/jobs/components/job/job_log_controllers_spec.js new file mode 100644 index 00000000000..5e9a73b4387 --- /dev/null +++ b/spec/frontend/jobs/components/job/job_log_controllers_spec.js @@ -0,0 +1,315 @@ +import { GlSearchBoxByClick } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import JobLogControllers from '~/jobs/components/job/job_log_controllers.vue'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; +import { backoffMockImplementation } from 'helpers/backoff_helper'; +import * as commonUtils from '~/lib/utils/common_utils'; +import { mockJobLog } from '../../mock_data'; + +const mockToastShow = jest.fn(); + +describe('Job log controllers', () => { + let wrapper; + + beforeEach(() => { + jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation); + }); + + afterEach(() => { + if (wrapper?.destroy) { + wrapper.destroy(); + } + commonUtils.backOff.mockReset(); + }); + + const defaultProps = { + rawPath: '/raw', + erasePath: '/erase', + size: 511952, + isScrollTopDisabled: false, + isScrollBottomDisabled: false, + isScrollingDown: true, + isJobLogSizeVisible: true, + isComplete: true, + jobLog: mockJobLog, + }; + + const createWrapper = (props, { jobLogJumpToFailures = false } = {}) => { + wrapper = mount(JobLogControllers, { + propsData: { + ...defaultProps, + ...props, + }, + provide: { + glFeatures: { + jobLogJumpToFailures, + }, + }, + data() { + return { + searchTerm: '82', + }; + }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, + }); + }; + + const findTruncatedInfo = () => wrapper.find('[data-testid="log-truncated-info"]'); + const findRawLink = () => wrapper.find('[data-testid="raw-link"]'); + const findRawLinkController = () => wrapper.find('[data-testid="job-raw-link-controller"]'); + const findScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]'); + const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); + const findJobLogSearch = () => wrapper.findComponent(GlSearchBoxByClick); + const findSearchHelp = () => wrapper.findComponent(HelpPopover); + const findScrollFailure = () => wrapper.find('[data-testid="job-controller-scroll-to-failure"]'); + + describe('Truncate information', () => { + describe('with isJobLogSizeVisible', () => { + beforeEach(() => { + createWrapper(); + }); + + it('renders size information', () => { + expect(findTruncatedInfo().text()).toMatch('499.95 KiB'); + }); + + it('renders link to raw job log', () => { + expect(findRawLink().attributes('href')).toBe(defaultProps.rawPath); + }); + }); + }); + + describe('links section', () => { + describe('with raw job log path', () => { + beforeEach(() => { + createWrapper(); + }); + + it('renders raw job log link', () => { + expect(findRawLinkController().attributes('href')).toBe(defaultProps.rawPath); + }); + }); + + describe('without raw job log path', () => { + beforeEach(() => { + createWrapper({ + rawPath: null, + }); + }); + + it('does not render raw job log link', () => { + expect(findRawLinkController().exists()).toBe(false); + }); + }); + }); + + describe('scroll buttons', () => { + describe('scroll top button', () => { + describe('when user can scroll top', () => { + beforeEach(() => { + createWrapper({ + isScrollTopDisabled: false, + }); + }); + + it('emits scrollJobLogTop event on click', async () => { + await findScrollTop().trigger('click'); + + expect(wrapper.emitted().scrollJobLogTop).toHaveLength(1); + }); + }); + + describe('when user can not scroll top', () => { + beforeEach(() => { + createWrapper({ + isScrollTopDisabled: true, + isScrollBottomDisabled: false, + isScrollingDown: false, + }); + }); + + it('renders disabled scroll top button', () => { + expect(findScrollTop().attributes('disabled')).toBe('disabled'); + }); + + it('does not emit scrollJobLogTop event on click', async () => { + await findScrollTop().trigger('click'); + + expect(wrapper.emitted().scrollJobLogTop).toBeUndefined(); + }); + }); + }); + + describe('scroll bottom button', () => { + describe('when user can scroll bottom', () => { + beforeEach(() => { + createWrapper(); + }); + + it('emits scrollJobLogBottom event on click', async () => { + await findScrollBottom().trigger('click'); + + expect(wrapper.emitted().scrollJobLogBottom).toHaveLength(1); + }); + }); + + describe('when user can not scroll bottom', () => { + beforeEach(() => { + createWrapper({ + isScrollTopDisabled: false, + isScrollBottomDisabled: true, + isScrollingDown: false, + }); + }); + + it('renders disabled scroll bottom button', () => { + expect(findScrollBottom().attributes('disabled')).toEqual('disabled'); + }); + + it('does not emit scrollJobLogBottom event on click', async () => { + await findScrollBottom().trigger('click'); + + expect(wrapper.emitted().scrollJobLogBottom).toBeUndefined(); + }); + }); + + describe('while isScrollingDown is true', () => { + beforeEach(() => { + createWrapper(); + }); + + it('renders animate class for the scroll down button', () => { + expect(findScrollBottom().classes()).toContain('animate'); + }); + }); + + describe('while isScrollingDown is false', () => { + beforeEach(() => { + createWrapper({ + isScrollTopDisabled: true, + isScrollBottomDisabled: false, + isScrollingDown: false, + }); + }); + + it('does not render animate class for the scroll down button', () => { + expect(findScrollBottom().classes()).not.toContain('animate'); + }); + }); + }); + + describe('scroll to failure button', () => { + describe('with feature flag disabled', () => { + it('does not display button', () => { + createWrapper(); + + expect(findScrollFailure().exists()).toBe(false); + }); + }); + + describe('with red text failures on the page', () => { + let firstFailure; + let secondFailure; + + beforeEach(() => { + jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce(['mock-element']); + + createWrapper({}, { jobLogJumpToFailures: true }); + + firstFailure = document.createElement('div'); + firstFailure.className = 'term-fg-l-red'; + document.body.appendChild(firstFailure); + + secondFailure = document.createElement('div'); + secondFailure.className = 'term-fg-l-red'; + document.body.appendChild(secondFailure); + }); + + afterEach(() => { + if (firstFailure) { + firstFailure.remove(); + firstFailure = null; + } + + if (secondFailure) { + secondFailure.remove(); + secondFailure = null; + } + }); + + it('is enabled', () => { + expect(findScrollFailure().props('disabled')).toBe(false); + }); + + it('scrolls to each failure', async () => { + jest.spyOn(firstFailure, 'scrollIntoView'); + + await findScrollFailure().trigger('click'); + + expect(firstFailure.scrollIntoView).toHaveBeenCalled(); + + await findScrollFailure().trigger('click'); + + expect(secondFailure.scrollIntoView).toHaveBeenCalled(); + + await findScrollFailure().trigger('click'); + + expect(firstFailure.scrollIntoView).toHaveBeenCalled(); + }); + }); + + describe('with no red text failures on the page', () => { + beforeEach(() => { + jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce([]); + + createWrapper({}, { jobLogJumpToFailures: true }); + }); + + it('is disabled', () => { + expect(findScrollFailure().props('disabled')).toBe(true); + }); + }); + + describe('when the job log is not complete', () => { + beforeEach(() => { + jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce(['mock-element']); + + createWrapper({ isComplete: false }, { jobLogJumpToFailures: true }); + }); + + it('is enabled', () => { + expect(findScrollFailure().props('disabled')).toBe(false); + }); + }); + }); + }); + + describe('Job log search', () => { + beforeEach(() => { + createWrapper(); + }); + + it('displays job log search', () => { + expect(findJobLogSearch().exists()).toBe(true); + expect(findSearchHelp().exists()).toBe(true); + }); + + it('emits search results', () => { + const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]]; + + findJobLogSearch().vm.$emit('submit'); + + expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults); + }); + + it('clears search results', () => { + findJobLogSearch().vm.$emit('clear'); + + expect(wrapper.emitted('searchResults')).toEqual([[[]]]); + }); + }); +}); 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 new file mode 100644 index 00000000000..d60043f33f7 --- /dev/null +++ b/spec/frontend/jobs/components/job/job_retry_forward_deployment_modal_spec.js @@ -0,0 +1,76 @@ +import { GlLink, GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue'; +import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '~/jobs/constants'; +import createStore from '~/jobs/store'; +import job from '../../mock_data'; + +describe('Job Retry Forward Deployment Modal', () => { + let store; + let wrapper; + + const retryOutdatedJobDocsUrl = 'url-to-docs'; + const findLink = () => wrapper.findComponent(GlLink); + const findModal = () => wrapper.findComponent(GlModal); + + const createWrapper = ({ props = {}, provide = {}, stubs = {} } = {}) => { + store = createStore(); + wrapper = shallowMount(JobRetryForwardDeploymentModal, { + propsData: { + modalId: 'modal-id', + href: job.retry_path, + ...props, + }, + provide, + store, + stubs, + }); + }; + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + beforeEach(createWrapper); + + describe('Modal configuration', () => { + it('should display the correct messages', () => { + const modal = findModal(); + expect(modal.attributes('title')).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.title); + expect(modal.text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.info); + expect(modal.text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.areYouSure); + }); + }); + + describe('Modal docs help link', () => { + it('should not display an info link when none is provided', () => { + createWrapper(); + + expect(findLink().exists()).toBe(false); + }); + + it('should display an info link when one is provided', () => { + createWrapper({ provide: { retryOutdatedJobDocsUrl } }); + + expect(findLink().attributes('href')).toBe(retryOutdatedJobDocsUrl); + expect(findLink().text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.moreInfo); + }); + }); + + describe('Modal actions', () => { + beforeEach(createWrapper); + + it('should correctly configure the primary action', () => { + 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 new file mode 100644 index 00000000000..4da17ed8366 --- /dev/null +++ b/spec/frontend/jobs/components/job/job_sidebar_details_container_spec.js @@ -0,0 +1,140 @@ +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import DetailRow from '~/jobs/components/job/sidebar/sidebar_detail_row.vue'; +import SidebarJobDetailsContainer from '~/jobs/components/job/sidebar/sidebar_job_details_container.vue'; +import createStore from '~/jobs/store'; +import job from '../../mock_data'; + +describe('Job Sidebar Details Container', () => { + let store; + let wrapper; + + const findJobTimeout = () => wrapper.findByTestId('job-timeout'); + const findJobTags = () => wrapper.findByTestId('job-tags'); + const findAllDetailsRow = () => wrapper.findAllComponents(DetailRow); + + const createWrapper = ({ props = {} } = {}) => { + store = createStore(); + wrapper = extendedWrapper( + shallowMount(SidebarJobDetailsContainer, { + propsData: props, + store, + stubs: { + DetailRow, + }, + }), + ); + }; + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + describe('when no details are available', () => { + beforeEach(() => { + createWrapper(); + }); + + it('should render an empty container', () => { + expect(wrapper.html()).toBe(''); + }); + + it.each(['duration', 'erased_at', 'finished_at', 'queued_at', 'runner', 'coverage'])( + 'should not render %s details when missing', + async (detail) => { + await store.dispatch('receiveJobSuccess', { [detail]: undefined }); + + expect(findAllDetailsRow()).toHaveLength(0); + }, + ); + }); + + describe('when some of the details are available', () => { + beforeEach(createWrapper); + + it.each([ + ['duration', 'Elapsed time: 6 seconds'], + ['erased_at', 'Erased: 3 weeks ago'], + ['finished_at', 'Finished: 3 weeks ago'], + ['queued_duration', 'Queued: 9 seconds'], + ['runner', 'Runner: #1 (ABCDEFGH) local ci runner'], + ['coverage', 'Coverage: 20%'], + ])('uses %s to render job-%s', async (detail, value) => { + await store.dispatch('receiveJobSuccess', { [detail]: job[detail] }); + const detailsRow = findAllDetailsRow(); + + expect(detailsRow).toHaveLength(1); + expect(detailsRow.at(0).text()).toBe(value); + }); + + it('only renders tags', async () => { + const { tags } = job; + await store.dispatch('receiveJobSuccess', { tags }); + const tagsComponent = findJobTags(); + + expect(tagsComponent.text()).toBe('Tags: tag'); + }); + }); + + describe('when all the info are available', () => { + it('renders all the details components', async () => { + createWrapper(); + await store.dispatch('receiveJobSuccess', job); + + expect(findAllDetailsRow()).toHaveLength(7); + }); + + describe('duration row', () => { + it('renders all the details components', async () => { + createWrapper(); + await store.dispatch('receiveJobSuccess', job); + + expect(findAllDetailsRow().at(0).text()).toBe('Duration: 6 seconds'); + }); + }); + }); + + describe('timeout', () => { + const { + metadata: { timeout_human_readable, timeout_source }, + } = job; + + beforeEach(createWrapper); + + it('does not render if metadata is empty', async () => { + const metadata = {}; + await store.dispatch('receiveJobSuccess', { metadata }); + const detailsRow = findAllDetailsRow(); + + expect(wrapper.html()).toBe(''); + expect(detailsRow.exists()).toBe(false); + }); + + it('uses metadata to render timeout', async () => { + const metadata = { timeout_human_readable }; + await store.dispatch('receiveJobSuccess', { metadata }); + const detailsRow = findAllDetailsRow(); + + expect(detailsRow).toHaveLength(1); + expect(detailsRow.at(0).text()).toBe('Timeout: 1m 40s'); + }); + + it('uses metadata to render timeout and the source', async () => { + const metadata = { timeout_human_readable, timeout_source }; + await store.dispatch('receiveJobSuccess', { metadata }); + const detailsRow = findAllDetailsRow(); + + expect(detailsRow.at(0).text()).toBe('Timeout: 1m 40s (from runner)'); + }); + + it('should not render when no time is provided', async () => { + const metadata = { timeout_source }; + await store.dispatch('receiveJobSuccess', { metadata }); + + expect(findJobTimeout().exists()).toBe(false); + }); + }); +}); 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 new file mode 100644 index 00000000000..18d5f35bde4 --- /dev/null +++ b/spec/frontend/jobs/components/job/job_sidebar_retry_button_spec.js @@ -0,0 +1,69 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import JobsSidebarRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue'; +import createStore from '~/jobs/store'; +import job from '../../mock_data'; + +describe('Job Sidebar Retry Button', () => { + let store; + let wrapper; + + const forwardDeploymentFailure = 'forward_deployment_failure'; + const findRetryButton = () => wrapper.findByTestId('retry-job-button'); + const findRetryLink = () => wrapper.findByTestId('retry-job-link'); + + const createWrapper = ({ props = {} } = {}) => { + store = createStore(); + wrapper = shallowMountExtended(JobsSidebarRetryButton, { + propsData: { + href: job.retry_path, + modalId: 'modal-id', + ...props, + }, + store, + }); + }; + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + beforeEach(createWrapper); + + it.each([ + [null, false, true], + ['unmet_prerequisites', false, true], + [forwardDeploymentFailure, true, false], + ])( + 'when error is: %s, should render button: %s | should render link: %s', + async (failureReason, buttonExists, linkExists) => { + await store.dispatch('receiveJobSuccess', { ...job, failure_reason: failureReason }); + + expect(findRetryButton().exists()).toBe(buttonExists); + expect(findRetryLink().exists()).toBe(linkExists); + }, + ); + + describe('Button', () => { + it('should have the correct configuration', async () => { + await store.dispatch('receiveJobSuccess', { failure_reason: forwardDeploymentFailure }); + + expect(findRetryButton().attributes()).toMatchObject({ + category: 'primary', + variant: 'confirm', + icon: 'retry', + }); + }); + }); + + describe('Link', () => { + it('should have the correct configuration', () => { + expect(findRetryLink().attributes()).toMatchObject({ + 'data-method': 'post', + href: job.retry_path, + icon: 'retry', + }); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/jobs_container_spec.js b/spec/frontend/jobs/components/job/jobs_container_spec.js new file mode 100644 index 00000000000..2fde4d3020b --- /dev/null +++ b/spec/frontend/jobs/components/job/jobs_container_spec.js @@ -0,0 +1,147 @@ +import { GlLink } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue'; + +describe('Jobs List block', () => { + let wrapper; + + const retried = { + status: { + details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + id: 233432756, + tooltip: 'build - passed', + retried: true, + }; + + const active = { + name: 'test', + status: { + details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + id: 2322756, + tooltip: 'build - passed', + active: true, + }; + + const job = { + name: 'build', + status: { + details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', + }, + id: 232153, + tooltip: 'build - passed', + }; + + const findAllJobs = () => wrapper.findAllComponents(GlLink); + const findJob = () => findAllJobs().at(0); + + const findArrowIcon = () => wrapper.findByTestId('arrow-right-icon'); + const findRetryIcon = () => wrapper.findByTestId('retry-icon'); + + const createComponent = (props) => { + wrapper = extendedWrapper( + mount(JobsContainer, { + propsData: { + ...props, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders a list of jobs', () => { + createComponent({ + jobs: [job, retried, active], + jobId: 12313, + }); + + expect(findAllJobs()).toHaveLength(3); + }); + + it('renders the arrow right icon when job id matches `jobId`', () => { + createComponent({ + jobs: [active], + jobId: active.id, + }); + + expect(findArrowIcon().exists()).toBe(true); + }); + + it('does not render the arrow right icon when the job is not active', () => { + createComponent({ + jobs: [job], + jobId: active.id, + }); + + expect(findArrowIcon().exists()).toBe(false); + }); + + it('renders the job name when present', () => { + createComponent({ + jobs: [job], + jobId: active.id, + }); + + expect(findJob().text()).toBe(job.name); + expect(findJob().text()).not.toContain(job.id.toString()); + }); + + it('renders job id when job name is not available', () => { + createComponent({ + jobs: [retried], + jobId: active.id, + }); + + expect(findJob().text()).toBe(retried.id.toString()); + }); + + it('links to the job page', () => { + createComponent({ + jobs: [job], + jobId: active.id, + }); + + expect(findJob().attributes('href')).toBe(job.status.details_path); + }); + + it('renders retry icon when job was retried', () => { + createComponent({ + jobs: [retried], + jobId: active.id, + }); + + expect(findRetryIcon().exists()).toBe(true); + }); + + it('does not render retry icon when job was not retried', () => { + createComponent({ + jobs: [job], + jobId: active.id, + }); + + expect(findRetryIcon().exists()).toBe(false); + }); +}); diff --git a/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js b/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js new file mode 100644 index 00000000000..184562b2968 --- /dev/null +++ b/spec/frontend/jobs/components/job/legacy_manual_variables_form_spec.js @@ -0,0 +1,156 @@ +import { GlSprintf, GlLink } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import LegacyManualVariablesForm from '~/jobs/components/job/legacy_manual_variables_form.vue'; + +Vue.use(Vuex); + +describe('Manual Variables Form', () => { + let wrapper; + let store; + + const requiredProps = { + action: { + path: '/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }; + + const createComponent = (props = {}) => { + store = new Vuex.Store({ + actions: { + triggerManualJob: jest.fn(), + }, + }); + + wrapper = extendedWrapper( + mount(LegacyManualVariablesForm, { + propsData: { ...requiredProps, ...props }, + store, + stubs: { + GlSprintf, + }, + }), + ); + }; + + const findHelpText = () => wrapper.findComponent(GlSprintf); + const findHelpLink = () => wrapper.findComponent(GlLink); + + const findTriggerBtn = () => wrapper.findByTestId('trigger-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'); + const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key'); + const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key'); + const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value'); + const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row'); + + const setCiVariableKey = () => { + findCiVariableKey().setValue('new key'); + findCiVariableKey().vm.$emit('change'); + nextTick(); + }; + + const setCiVariableKeyByPosition = (position, value) => { + findAllCiVariableKeys().at(position).setValue(value); + findAllCiVariableKeys().at(position).vm.$emit('change'); + nextTick(); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('creates a new variable when user enters a new key value', async () => { + expect(findAllVariables()).toHaveLength(1); + + await setCiVariableKey(); + + expect(findAllVariables()).toHaveLength(2); + }); + + it('does not create extra empty variables', async () => { + expect(findAllVariables()).toHaveLength(1); + + await setCiVariableKey(); + + expect(findAllVariables()).toHaveLength(2); + + await setCiVariableKey(); + + expect(findAllVariables()).toHaveLength(2); + }); + + it('removes the correct variable row', async () => { + const variableKeyNameOne = 'key-one'; + const variableKeyNameThree = 'key-three'; + + await setCiVariableKeyByPosition(0, variableKeyNameOne); + + await setCiVariableKeyByPosition(1, 'key-two'); + + await setCiVariableKeyByPosition(2, variableKeyNameThree); + + expect(findAllVariables()).toHaveLength(4); + + await findAllDeleteVarBtns().at(1).trigger('click'); + + expect(findAllVariables()).toHaveLength(3); + + expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne); + expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree); + expect(findAllCiVariableKeys().at(2).element.value).toBe(''); + }); + + it('trigger button is disabled after trigger action', async () => { + expect(findTriggerBtn().props('disabled')).toBe(false); + + await findTriggerBtn().trigger('click'); + + expect(findTriggerBtn().props('disabled')).toBe(true); + }); + + it('delete variable button should only show when there is more than one variable', async () => { + expect(findDeleteVarBtn().exists()).toBe(false); + + await setCiVariableKey(); + + expect(findDeleteVarBtn().exists()).toBe(true); + }); + + it('delete variable button placeholder should only exist when a user cannot remove', async () => { + expect(findDeleteVarBtnPlaceholder().exists()).toBe(true); + }); + + it('renders help text with provided link', () => { + expect(findHelpText().exists()).toBe(true); + expect(findHelpLink().attributes('href')).toBe( + '/help/ci/variables/index#add-a-cicd-variable-to-a-project', + ); + }); + + it('passes variables in correct format', async () => { + jest.spyOn(store, 'dispatch'); + + await setCiVariableKey(); + + await findCiVariableValue().setValue('new value'); + + await findTriggerBtn().trigger('click'); + + expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [ + { + key: 'new key', + secret_value: 'new value', + }, + ]); + }); +}); diff --git a/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js new file mode 100644 index 00000000000..cb32ca9d3dc --- /dev/null +++ b/spec/frontend/jobs/components/job/legacy_sidebar_header_spec.js @@ -0,0 +1,91 @@ +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue'; +import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue'; +import createStore from '~/jobs/store'; +import job from '../../mock_data'; + +describe('Legacy Sidebar Header', () => { + let store; + let wrapper; + + const findCancelButton = () => wrapper.findByTestId('cancel-button'); + const findRetryButton = () => wrapper.findComponent(JobRetryButton); + const findEraseLink = () => wrapper.findByTestId('job-log-erase-link'); + + const createWrapper = (props) => { + store = createStore(); + + wrapper = extendedWrapper( + shallowMount(LegacySidebarHeader, { + propsData: { + job, + ...props, + }, + store, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when job log is erasable', () => { + const path = '/root/ci-project/-/jobs/1447/erase'; + + beforeEach(() => { + createWrapper({ + erasePath: path, + }); + }); + + it('renders erase job link', () => { + expect(findEraseLink().exists()).toBe(true); + }); + + it('erase job link has correct path', () => { + expect(findEraseLink().attributes('href')).toBe(path); + }); + }); + + describe('when job log is not erasable', () => { + beforeEach(() => { + createWrapper(); + }); + + it('does not render erase button', () => { + expect(findEraseLink().exists()).toBe(false); + }); + }); + + describe('when the job is retryable', () => { + beforeEach(() => { + createWrapper(); + }); + + it('should render the retry button', () => { + expect(findRetryButton().props('href')).toBe(job.retry_path); + }); + }); + + describe('when there is no retry path', () => { + it('should not render a retry button', async () => { + const copy = { ...job, retry_path: null }; + createWrapper({ job: copy }); + + expect(findRetryButton().exists()).toBe(false); + }); + }); + + describe('when the job is cancelable', () => { + beforeEach(() => { + createWrapper(); + }); + + it('should render link to cancel job', () => { + expect(findCancelButton().props('icon')).toBe('cancel'); + expect(findCancelButton().attributes('href')).toBe(job.cancel_path); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js new file mode 100644 index 00000000000..5806f9f75f9 --- /dev/null +++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js @@ -0,0 +1,156 @@ +import { GlSprintf, GlLink } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import Vuex from 'vuex'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue'; + +Vue.use(Vuex); + +describe('Manual Variables Form', () => { + let wrapper; + let store; + + const requiredProps = { + action: { + path: '/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }; + + const createComponent = (props = {}) => { + store = new Vuex.Store({ + actions: { + triggerManualJob: jest.fn(), + }, + }); + + wrapper = extendedWrapper( + mount(ManualVariablesForm, { + propsData: { ...requiredProps, ...props }, + store, + stubs: { + GlSprintf, + }, + }), + ); + }; + + const findHelpText = () => wrapper.findComponent(GlSprintf); + const findHelpLink = () => wrapper.findComponent(GlLink); + + const findTriggerBtn = () => wrapper.findByTestId('trigger-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'); + const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key'); + const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key'); + const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value'); + const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row'); + + const setCiVariableKey = () => { + findCiVariableKey().setValue('new key'); + findCiVariableKey().vm.$emit('change'); + nextTick(); + }; + + const setCiVariableKeyByPosition = (position, value) => { + findAllCiVariableKeys().at(position).setValue(value); + findAllCiVariableKeys().at(position).vm.$emit('change'); + nextTick(); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('creates a new variable when user enters a new key value', async () => { + expect(findAllVariables()).toHaveLength(1); + + await setCiVariableKey(); + + expect(findAllVariables()).toHaveLength(2); + }); + + it('does not create extra empty variables', async () => { + expect(findAllVariables()).toHaveLength(1); + + await setCiVariableKey(); + + expect(findAllVariables()).toHaveLength(2); + + await setCiVariableKey(); + + expect(findAllVariables()).toHaveLength(2); + }); + + it('removes the correct variable row', async () => { + const variableKeyNameOne = 'key-one'; + const variableKeyNameThree = 'key-three'; + + await setCiVariableKeyByPosition(0, variableKeyNameOne); + + await setCiVariableKeyByPosition(1, 'key-two'); + + await setCiVariableKeyByPosition(2, variableKeyNameThree); + + expect(findAllVariables()).toHaveLength(4); + + await findAllDeleteVarBtns().at(1).trigger('click'); + + expect(findAllVariables()).toHaveLength(3); + + expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne); + expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree); + expect(findAllCiVariableKeys().at(2).element.value).toBe(''); + }); + + it('trigger button is disabled after trigger action', async () => { + expect(findTriggerBtn().props('disabled')).toBe(false); + + await findTriggerBtn().trigger('click'); + + expect(findTriggerBtn().props('disabled')).toBe(true); + }); + + it('delete variable button should only show when there is more than one variable', async () => { + expect(findDeleteVarBtn().exists()).toBe(false); + + await setCiVariableKey(); + + expect(findDeleteVarBtn().exists()).toBe(true); + }); + + it('delete variable button placeholder should only exist when a user cannot remove', async () => { + expect(findDeleteVarBtnPlaceholder().exists()).toBe(true); + }); + + it('renders help text with provided link', () => { + expect(findHelpText().exists()).toBe(true); + expect(findHelpLink().attributes('href')).toBe( + '/help/ci/variables/index#add-a-cicd-variable-to-a-project', + ); + }); + + it('passes variables in correct format', async () => { + jest.spyOn(store, 'dispatch'); + + await setCiVariableKey(); + + await findCiVariableValue().setValue('new value'); + + await findTriggerBtn().trigger('click'); + + expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [ + { + key: 'new key', + secret_value: 'new value', + }, + ]); + }); +}); diff --git a/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js new file mode 100644 index 00000000000..5c9c011b4ab --- /dev/null +++ b/spec/frontend/jobs/components/job/sidebar_detail_row_spec.js @@ -0,0 +1,55 @@ +import { GlLink } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SidebarDetailRow from '~/jobs/components/job/sidebar/sidebar_detail_row.vue'; + +describe('Sidebar detail row', () => { + let wrapper; + + const title = 'this is the title'; + const value = 'this is the value'; + const helpUrl = 'https://docs.gitlab.com/runner/register/index.html'; + + const findHelpLink = () => wrapper.findComponent(GlLink); + + const createComponent = (props) => { + wrapper = shallowMount(SidebarDetailRow, { + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('with title/value and without helpUrl', () => { + beforeEach(() => { + createComponent({ title, value }); + }); + + it('should render the provided title and value', () => { + expect(wrapper.text()).toBe(`${title}: ${value}`); + }); + + it('should not render the help link', () => { + expect(findHelpLink().exists()).toBe(false); + }); + }); + + describe('when helpUrl provided', () => { + beforeEach(() => { + createComponent({ + helpUrl, + title, + value, + }); + }); + + it('should render the help link', () => { + expect(findHelpLink().exists()).toBe(true); + expect(findHelpLink().attributes('href')).toBe(helpUrl); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/sidebar_header_spec.js b/spec/frontend/jobs/components/job/sidebar_header_spec.js new file mode 100644 index 00000000000..cb32ca9d3dc --- /dev/null +++ b/spec/frontend/jobs/components/job/sidebar_header_spec.js @@ -0,0 +1,91 @@ +import { shallowMount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import JobRetryButton from '~/jobs/components/job/sidebar/job_sidebar_retry_button.vue'; +import LegacySidebarHeader from '~/jobs/components/job/sidebar/legacy_sidebar_header.vue'; +import createStore from '~/jobs/store'; +import job from '../../mock_data'; + +describe('Legacy Sidebar Header', () => { + let store; + let wrapper; + + const findCancelButton = () => wrapper.findByTestId('cancel-button'); + const findRetryButton = () => wrapper.findComponent(JobRetryButton); + const findEraseLink = () => wrapper.findByTestId('job-log-erase-link'); + + const createWrapper = (props) => { + store = createStore(); + + wrapper = extendedWrapper( + shallowMount(LegacySidebarHeader, { + propsData: { + job, + ...props, + }, + store, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when job log is erasable', () => { + const path = '/root/ci-project/-/jobs/1447/erase'; + + beforeEach(() => { + createWrapper({ + erasePath: path, + }); + }); + + it('renders erase job link', () => { + expect(findEraseLink().exists()).toBe(true); + }); + + it('erase job link has correct path', () => { + expect(findEraseLink().attributes('href')).toBe(path); + }); + }); + + describe('when job log is not erasable', () => { + beforeEach(() => { + createWrapper(); + }); + + it('does not render erase button', () => { + expect(findEraseLink().exists()).toBe(false); + }); + }); + + describe('when the job is retryable', () => { + beforeEach(() => { + createWrapper(); + }); + + it('should render the retry button', () => { + expect(findRetryButton().props('href')).toBe(job.retry_path); + }); + }); + + describe('when there is no retry path', () => { + it('should not render a retry button', async () => { + const copy = { ...job, retry_path: null }; + createWrapper({ job: copy }); + + expect(findRetryButton().exists()).toBe(false); + }); + }); + + describe('when the job is cancelable', () => { + beforeEach(() => { + createWrapper(); + }); + + it('should render link to cancel job', () => { + expect(findCancelButton().props('icon')).toBe('cancel'); + expect(findCancelButton().attributes('href')).toBe(job.cancel_path); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js new file mode 100644 index 00000000000..dc1aa67489d --- /dev/null +++ b/spec/frontend/jobs/components/job/sidebar_spec.js @@ -0,0 +1,166 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue'; +import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue'; +import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue'; +import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue'; +import StagesDropdown from '~/jobs/components/job/sidebar/stages_dropdown.vue'; +import createStore from '~/jobs/store'; +import job, { jobsInStage } from '../../mock_data'; + +describe('Sidebar details block', () => { + let store; + let wrapper; + + const forwardDeploymentFailure = 'forward_deployment_failure'; + const findModal = () => wrapper.findComponent(JobRetryForwardDeploymentModal); + const findArtifactsBlock = () => wrapper.findComponent(ArtifactsBlock); + const findNewIssueButton = () => wrapper.findByTestId('job-new-issue'); + const findTerminalLink = () => wrapper.findByTestId('terminal-link'); + + const createWrapper = (props) => { + store = createStore(); + + store.state.job = job; + + wrapper = extendedWrapper( + shallowMount(Sidebar, { + propsData: { + ...props, + }, + + store, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('without terminal path', () => { + it('does not render terminal link', async () => { + createWrapper(); + await store.dispatch('receiveJobSuccess', job); + + expect(findTerminalLink().exists()).toBe(false); + }); + }); + + describe('with terminal path', () => { + it('renders terminal link', async () => { + createWrapper(); + await store.dispatch('receiveJobSuccess', { ...job, terminal_path: 'job/43123/terminal' }); + + expect(findTerminalLink().exists()).toBe(true); + }); + }); + + describe('actions', () => { + beforeEach(() => { + createWrapper(); + return store.dispatch('receiveJobSuccess', job); + }); + + it('should render link to new issue', () => { + expect(findNewIssueButton().attributes('href')).toBe(job.new_issue_path); + expect(findNewIssueButton().text()).toBe('New issue'); + }); + }); + + describe('forward deployment failure', () => { + describe('when the relevant data is missing', () => { + it.each` + retryPath | failureReason + ${null} | ${null} + ${''} | ${''} + ${job.retry_path} | ${''} + ${''} | ${forwardDeploymentFailure} + ${job.retry_path} | ${'unmet_prerequisites'} + `( + 'should not render the modal when path and failure are $retryPath, $failureReason', + async ({ retryPath, failureReason }) => { + createWrapper(); + await store.dispatch('receiveJobSuccess', { + ...job, + failure_reason: failureReason, + retry_path: retryPath, + }); + expect(findModal().exists()).toBe(false); + }, + ); + }); + + describe('when there is the relevant error', () => { + beforeEach(() => { + createWrapper(); + return store.dispatch('receiveJobSuccess', { + ...job, + failure_reason: forwardDeploymentFailure, + }); + }); + + it('should render the modal', () => { + expect(findModal().exists()).toBe(true); + }); + }); + }); + + describe('stages dropdown', () => { + beforeEach(() => { + createWrapper(); + return store.dispatch('receiveJobSuccess', { ...job, stage: 'aStage' }); + }); + + describe('with stages', () => { + it('renders value provided as selectedStage as selected', () => { + expect(wrapper.findComponent(StagesDropdown).props('selectedStage')).toBe('aStage'); + }); + }); + + describe('without jobs for stages', () => { + beforeEach(() => store.dispatch('receiveJobSuccess', job)); + + it('does not render jobs container', () => { + expect(wrapper.findComponent(JobsContainer).exists()).toBe(false); + }); + }); + + describe('with jobs for stages', () => { + beforeEach(async () => { + await store.dispatch('receiveJobSuccess', job); + await store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses); + }); + + it('renders list of jobs', () => { + expect(wrapper.findComponent(JobsContainer).exists()).toBe(true); + }); + }); + }); + + describe('artifacts', () => { + beforeEach(() => { + createWrapper(); + }); + + it('artifacts are not shown if there are no properties other than locked', () => { + expect(findArtifactsBlock().exists()).toBe(false); + }); + + it('artifacts are shown if present', async () => { + store.state.job.artifact = { + download_path: '/root/ci-project/-/jobs/1960/artifacts/download', + browse_path: '/root/ci-project/-/jobs/1960/artifacts/browse', + keep_path: '/root/ci-project/-/jobs/1960/artifacts/keep', + expire_at: '2021-03-23T17:57:11.211Z', + expired: false, + locked: false, + }; + + await nextTick(); + + expect(findArtifactsBlock().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/stages_dropdown_spec.js b/spec/frontend/jobs/components/job/stages_dropdown_spec.js new file mode 100644 index 00000000000..61dec585e82 --- /dev/null +++ b/spec/frontend/jobs/components/job/stages_dropdown_spec.js @@ -0,0 +1,192 @@ +import { GlDropdown, GlDropdownItem, GlLink, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import Mousetrap from 'mousetrap'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import StagesDropdown from '~/jobs/components/job/sidebar/stages_dropdown.vue'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import * as copyToClipboard from '~/behaviors/copy_to_clipboard'; +import { + mockPipelineWithoutRef, + mockPipelineWithoutMR, + mockPipelineWithAttachedMR, + mockPipelineDetached, +} from '../../mock_data'; + +describe('Stages Dropdown', () => { + let wrapper; + + const findStatus = () => wrapper.findComponent(CiIcon); + const findSelectedStageText = () => wrapper.findComponent(GlDropdown).props('text'); + const findStageItem = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); + + const findPipelineInfoText = () => wrapper.findByTestId('pipeline-info').text(); + + const createComponent = (props) => { + wrapper = extendedWrapper( + shallowMount(StagesDropdown, { + propsData: { + stages: [], + selectedStage: 'deploy', + ...props, + }, + stubs: { + GlSprintf, + GlLink, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('without a merge request pipeline', () => { + beforeEach(() => { + createComponent({ + pipeline: mockPipelineWithoutMR, + stages: [{ name: 'build' }, { name: 'test' }], + }); + }); + + it('renders pipeline status', () => { + expect(findStatus().exists()).toBe(true); + }); + + it('renders dropdown with stages', () => { + expect(findStageItem(0).text()).toBe('build'); + }); + + it('rendes selected stage', () => { + expect(findSelectedStageText()).toBe('deploy'); + }); + }); + + describe('pipelineInfo', () => { + const allElements = [ + 'pipeline-path', + 'mr-link', + 'source-ref-link', + 'copy-source-ref-link', + 'source-branch-link', + 'copy-source-branch-link', + 'target-branch-link', + 'copy-target-branch-link', + ]; + describe.each([ + [ + 'does not have a ref', + { + pipeline: mockPipelineWithoutRef, + text: `Pipeline #${mockPipelineWithoutRef.id}`, + foundElements: [ + { testId: 'pipeline-path', props: [{ href: mockPipelineWithoutRef.path }] }, + ], + }, + ], + [ + 'hasRef but not triggered by MR', + { + pipeline: mockPipelineWithoutMR, + text: `Pipeline #${mockPipelineWithoutMR.id} for ${mockPipelineWithoutMR.ref.name}`, + foundElements: [ + { testId: 'pipeline-path', props: [{ href: mockPipelineWithoutMR.path }] }, + { testId: 'source-ref-link', props: [{ href: mockPipelineWithoutMR.ref.path }] }, + { testId: 'copy-source-ref-link', props: [{ text: mockPipelineWithoutMR.ref.name }] }, + ], + }, + ], + [ + 'hasRef and MR but not MR pipeline', + { + pipeline: mockPipelineDetached, + text: `Pipeline #${mockPipelineDetached.id} for !${mockPipelineDetached.merge_request.iid} with ${mockPipelineDetached.merge_request.source_branch}`, + foundElements: [ + { testId: 'pipeline-path', props: [{ href: mockPipelineDetached.path }] }, + { testId: 'mr-link', props: [{ href: mockPipelineDetached.merge_request.path }] }, + { + testId: 'source-branch-link', + props: [{ href: mockPipelineDetached.merge_request.source_branch_path }], + }, + { + testId: 'copy-source-branch-link', + props: [{ text: mockPipelineDetached.merge_request.source_branch }], + }, + ], + }, + ], + [ + 'hasRef and MR and MR pipeline', + { + pipeline: mockPipelineWithAttachedMR, + text: `Pipeline #${mockPipelineWithAttachedMR.id} for !${mockPipelineWithAttachedMR.merge_request.iid} with ${mockPipelineWithAttachedMR.merge_request.source_branch} into ${mockPipelineWithAttachedMR.merge_request.target_branch}`, + foundElements: [ + { testId: 'pipeline-path', props: [{ href: mockPipelineWithAttachedMR.path }] }, + { testId: 'mr-link', props: [{ href: mockPipelineWithAttachedMR.merge_request.path }] }, + { + testId: 'source-branch-link', + props: [{ href: mockPipelineWithAttachedMR.merge_request.source_branch_path }], + }, + { + testId: 'copy-source-branch-link', + props: [{ text: mockPipelineWithAttachedMR.merge_request.source_branch }], + }, + { + testId: 'target-branch-link', + props: [{ href: mockPipelineWithAttachedMR.merge_request.target_branch_path }], + }, + { + testId: 'copy-target-branch-link', + props: [{ text: mockPipelineWithAttachedMR.merge_request.target_branch }], + }, + ], + }, + ], + ])('%s', (_, { pipeline, text, foundElements }) => { + beforeEach(() => { + createComponent({ + pipeline, + }); + }); + + it('should render the text', () => { + expect(findPipelineInfoText()).toMatchInterpolatedText(text); + }); + + it('should find components with props', () => { + foundElements.forEach((element) => { + element.props.forEach((prop) => { + const key = Object.keys(prop)[0]; + expect(wrapper.findByTestId(element.testId).attributes(key)).toBe(prop[key]); + }); + }); + }); + + it('should not find components', () => { + const foundTestIds = foundElements.map((element) => element.testId); + allElements + .filter((testId) => !foundTestIds.includes(testId)) + .forEach((testId) => { + expect(wrapper.findByTestId(testId).exists()).toBe(false); + }); + }); + }); + }); + + describe('mousetrap', () => { + it.each([ + ['copy-source-ref-link', mockPipelineWithoutMR], + ['copy-source-branch-link', mockPipelineWithAttachedMR], + ])( + 'calls clickCopyToClipboardButton with `%s` button when `b` is pressed', + (button, pipeline) => { + const copyToClipboardMock = jest.spyOn(copyToClipboard, 'clickCopyToClipboardButton'); + createComponent({ pipeline }); + + Mousetrap.trigger('b'); + + expect(copyToClipboardMock).toHaveBeenCalledWith(wrapper.findByTestId(button).element); + }, + ); + }); +}); diff --git a/spec/frontend/jobs/components/job/stuck_block_spec.js b/spec/frontend/jobs/components/job/stuck_block_spec.js new file mode 100644 index 00000000000..8dc570cce27 --- /dev/null +++ b/spec/frontend/jobs/components/job/stuck_block_spec.js @@ -0,0 +1,101 @@ +import { GlBadge, GlLink } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +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: { + ...props, + }, + }); + }; + + const tags = ['docker', 'gitlab-org']; + + const findStuckNoActiveRunners = () => + wrapper.find('[data-testid="job-stuck-no-active-runners"]'); + const findStuckNoRunners = () => wrapper.find('[data-testid="job-stuck-no-runners"]'); + const findStuckWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"]'); + const findRunnerPathLink = () => wrapper.findComponent(GlLink); + const findAllBadges = () => wrapper.findAllComponents(GlBadge); + + describe('with no runners for project', () => { + beforeEach(() => { + createWrapper({ + hasOfflineRunnersForProject: true, + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders only information about project not having runners', () => { + expect(findStuckNoRunners().exists()).toBe(true); + expect(findStuckWithTags().exists()).toBe(false); + expect(findStuckNoActiveRunners().exists()).toBe(false); + }); + + it('renders link to runners page', () => { + expect(findRunnerPathLink().attributes('href')).toBe( + '/root/project/runners#js-runners-settings', + ); + }); + }); + + describe('with tags', () => { + beforeEach(() => { + createWrapper({ + hasOfflineRunnersForProject: false, + tags, + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders information about the tags not being set', () => { + expect(findStuckWithTags().exists()).toBe(true); + expect(findStuckNoActiveRunners().exists()).toBe(false); + expect(findStuckNoRunners().exists()).toBe(false); + }); + + it('renders tags', () => { + findAllBadges().wrappers.forEach((badgeElt, index) => { + return expect(badgeElt.text()).toBe(tags[index]); + }); + }); + + it('renders link to runners page', () => { + expect(findRunnerPathLink().attributes('href')).toBe( + '/root/project/runners#js-runners-settings', + ); + }); + }); + + describe('without active runners', () => { + beforeEach(() => { + createWrapper({ + hasOfflineRunnersForProject: false, + runnersPath: '/root/project/runners#js-runners-settings', + }); + }); + + it('renders information about project not having runners', () => { + expect(findStuckNoActiveRunners().exists()).toBe(true); + expect(findStuckNoRunners().exists()).toBe(false); + expect(findStuckWithTags().exists()).toBe(false); + }); + + it('renders link to runners page', () => { + expect(findRunnerPathLink().attributes('href')).toBe( + '/root/project/runners#js-runners-settings', + ); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/trigger_block_spec.js b/spec/frontend/jobs/components/job/trigger_block_spec.js new file mode 100644 index 00000000000..a1de8fd143f --- /dev/null +++ b/spec/frontend/jobs/components/job/trigger_block_spec.js @@ -0,0 +1,85 @@ +import { GlButton, GlTableLite } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import TriggerBlock from '~/jobs/components/job/sidebar/trigger_block.vue'; + +describe('Trigger block', () => { + let wrapper; + + const findRevealButton = () => wrapper.findComponent(GlButton); + const findVariableTable = () => wrapper.findComponent(GlTableLite); + const findShortToken = () => wrapper.find('[data-testid="trigger-short-token"]'); + const findVariableValue = (index) => + wrapper.findAll('[data-testid="trigger-build-value"]').at(index); + const findVariableKey = (index) => wrapper.findAll('[data-testid="trigger-build-key"]').at(index); + + const createComponent = (props) => { + wrapper = mount(TriggerBlock, { + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('with short token and no variables', () => { + it('renders short token', () => { + createComponent({ + trigger: { + short_token: '0a666b2', + variables: [], + }, + }); + + expect(findShortToken().text()).toContain('0a666b2'); + }); + }); + + describe('without variables or short token', () => { + beforeEach(() => { + createComponent({ trigger: { variables: [] } }); + }); + + it('does not render short token', () => { + expect(findShortToken().exists()).toBe(false); + }); + + it('does not render variables', () => { + expect(findRevealButton().exists()).toBe(false); + expect(findVariableTable().exists()).toBe(false); + }); + }); + + describe('with variables', () => { + describe('hide/reveal variables', () => { + it('should toggle variables on click', async () => { + const hiddenValue = '••••••'; + const gcsVar = { key: 'UPLOAD_TO_GCS', value: 'false', public: false }; + const s3Var = { key: 'UPLOAD_TO_S3', value: 'true', public: false }; + + createComponent({ + trigger: { + variables: [gcsVar, s3Var], + }, + }); + + expect(findRevealButton().text()).toBe('Reveal values'); + + expect(findVariableValue(0).text()).toBe(hiddenValue); + expect(findVariableValue(1).text()).toBe(hiddenValue); + + expect(findVariableKey(0).text()).toBe(gcsVar.key); + expect(findVariableKey(1).text()).toBe(s3Var.key); + + await findRevealButton().trigger('click'); + + expect(findRevealButton().text()).toBe('Hide values'); + + expect(findVariableValue(0).text()).toBe(gcsVar.value); + expect(findVariableValue(1).text()).toBe(s3Var.value); + }); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js new file mode 100644 index 00000000000..fb7d389c4d6 --- /dev/null +++ b/spec/frontend/jobs/components/job/unmet_prerequisites_block_spec.js @@ -0,0 +1,41 @@ +import { GlAlert, GlLink } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue'; + +describe('Unmet Prerequisites Block Job component', () => { + let wrapper; + const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs'; + + const createComponent = () => { + wrapper = shallowMount(UnmetPrerequisitesBlock, { + propsData: { + helpPath, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders an alert with the correct message', () => { + const container = wrapper.findComponent(GlAlert); + const alertMessage = + 'This job failed because the necessary resources were not successfully created.'; + + expect(container).not.toBeNull(); + expect(container.text()).toContain(alertMessage); + }); + + it('renders link to help page', () => { + const helpLink = wrapper.findComponent(GlLink); + + expect(helpLink).not.toBeNull(); + expect(helpLink.text()).toContain('More information'); + expect(helpLink.attributes().href).toEqual(helpPath); + }); +}); diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js deleted file mode 100644 index b4b5bc4669d..00000000000 --- a/spec/frontend/jobs/components/job_app_spec.js +++ /dev/null @@ -1,440 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import MockAdapter from 'axios-mock-adapter'; -import Vuex from 'vuex'; -import delayedJobFixture from 'test_fixtures/jobs/delayed.json'; -import { TEST_HOST } from 'helpers/test_constants'; -import EmptyState from '~/jobs/components/empty_state.vue'; -import EnvironmentsBlock from '~/jobs/components/environments_block.vue'; -import ErasedBlock from '~/jobs/components/erased_block.vue'; -import JobApp from '~/jobs/components/job_app.vue'; -import Sidebar from '~/jobs/components/sidebar.vue'; -import StuckBlock from '~/jobs/components/stuck_block.vue'; -import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue'; -import createStore from '~/jobs/store'; -import axios from '~/lib/utils/axios_utils'; -import job from '../mock_data'; - -describe('Job App', () => { - Vue.use(Vuex); - - let store; - let wrapper; - let mock; - - const initSettings = { - endpoint: `${TEST_HOST}jobs/123.json`, - pagePath: `${TEST_HOST}jobs/123`, - logState: - 'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D', - }; - - const props = { - artifactHelpUrl: 'help/artifact', - deploymentHelpUrl: 'help/deployment', - runnerSettingsUrl: 'settings/ci-cd/runners', - terminalPath: 'jobs/123/terminal', - projectPath: 'user-name/project-name', - subscriptionsMoreMinutesUrl: 'https://customers.gitlab.com/buy_pipeline_minutes', - }; - - const createComponent = () => { - wrapper = mount(JobApp, { propsData: { ...props }, store }); - }; - - const setupAndMount = async ({ jobData = {}, jobLogData = {} } = {}) => { - mock.onGet(initSettings.endpoint).replyOnce(200, { ...job, ...jobData }); - mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, jobLogData); - - const asyncInit = store.dispatch('init', initSettings); - - createComponent(); - - await asyncInit; - jest.runOnlyPendingTimers(); - await axios.waitForAll(); - await nextTick(); - }; - - const findLoadingComponent = () => wrapper.find(GlLoadingIcon); - const findSidebar = () => wrapper.find(Sidebar); - const findJobContent = () => wrapper.find('[data-testid="job-content"'); - const findStuckBlockComponent = () => wrapper.find(StuckBlock); - const findStuckBlockWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"'); - const findStuckBlockNoActiveRunners = () => - wrapper.find('[data-testid="job-stuck-no-active-runners"'); - const findFailedJobComponent = () => wrapper.find(UnmetPrerequisitesBlock); - const findEnvironmentsBlockComponent = () => wrapper.find(EnvironmentsBlock); - const findErasedBlock = () => wrapper.find(ErasedBlock); - const findArchivedJob = () => wrapper.find('[data-testid="archived-job"]'); - const findEmptyState = () => wrapper.find(EmptyState); - const findJobNewIssueLink = () => wrapper.find('[data-testid="job-new-issue"]'); - const findJobEmptyStateTitle = () => wrapper.find('[data-testid="job-empty-state-title"]'); - const findJobLogScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]'); - const findJobLogScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); - const findJobLogController = () => wrapper.find('[data-testid="job-raw-link-controller"]'); - const findJobLogEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]'); - - beforeEach(() => { - mock = new MockAdapter(axios); - store = createStore(); - }); - - afterEach(() => { - wrapper.destroy(); - mock.restore(); - }); - - describe('while loading', () => { - beforeEach(() => { - store.state.isLoading = true; - createComponent(); - }); - - it('renders loading icon', () => { - expect(findLoadingComponent().exists()).toBe(true); - expect(findSidebar().exists()).toBe(false); - expect(findJobContent().exists()).toBe(false); - }); - }); - - describe('with successful request', () => { - describe('Header section', () => { - describe('job callout message', () => { - it('should not render the reason when reason is absent', () => - setupAndMount().then(() => { - expect(wrapper.vm.shouldRenderCalloutMessage).toBe(false); - })); - - it('should render the reason when reason is present', () => - setupAndMount({ - jobData: { - callout_message: 'There is an unkown failure, please try again', - }, - }).then(() => { - expect(wrapper.vm.shouldRenderCalloutMessage).toBe(true); - })); - }); - - describe('triggered job', () => { - beforeEach(() => { - const aYearAgo = new Date(); - aYearAgo.setFullYear(aYearAgo.getFullYear() - 1); - - return setupAndMount({ - jobData: { started: aYearAgo.toISOString(), started_at: aYearAgo.toISOString() }, - }); - }); - - it('should render provided job information', () => { - expect(wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim()).toContain( - 'passed Job test triggered 1 year ago by Root', - ); - }); - - it('should render new issue link', () => { - expect(findJobNewIssueLink().attributes('href')).toEqual(job.new_issue_path); - }); - }); - - describe('created job', () => { - it('should render created key', () => - setupAndMount().then(() => { - expect( - wrapper.find('.header-main-content').text().replace(/\s+/g, ' ').trim(), - ).toContain('passed Job test created 3 weeks ago by Root'); - })); - }); - }); - - describe('stuck block', () => { - describe('without active runners available', () => { - it('renders stuck block when there are no runners', () => - setupAndMount({ - jobData: { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - stuck: true, - runners: { - available: false, - online: false, - }, - tags: [], - }, - }).then(() => { - expect(findStuckBlockComponent().exists()).toBe(true); - expect(findStuckBlockNoActiveRunners().exists()).toBe(true); - })); - }); - - describe('when available runners can not run specified tag', () => { - it('renders tags in stuck block when there are no runners', () => - setupAndMount({ - jobData: { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - stuck: true, - runners: { - available: false, - online: false, - }, - }, - }).then(() => { - expect(findStuckBlockComponent().text()).toContain(job.tags[0]); - expect(findStuckBlockWithTags().exists()).toBe(true); - })); - }); - - describe('when runners are offline and build has tags', () => { - it('renders message about job being stuck because of no runners with the specified tags', () => - setupAndMount({ - jobData: { - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - }, - stuck: true, - runners: { - available: true, - online: true, - }, - }, - }).then(() => { - expect(findStuckBlockComponent().text()).toContain(job.tags[0]); - expect(findStuckBlockWithTags().exists()).toBe(true); - })); - }); - - it('does not renders stuck block when there are no runners', () => - setupAndMount({ - jobData: { - runners: { available: true }, - }, - }).then(() => { - expect(findStuckBlockComponent().exists()).toBe(false); - })); - }); - - describe('unmet prerequisites block', () => { - it('renders unmet prerequisites block when there is an unmet prerequisites failure', () => - setupAndMount({ - jobData: { - status: { - group: 'failed', - icon: 'status_failed', - label: 'failed', - text: 'failed', - details_path: 'path', - illustration: { - content: 'Retry this job in order to create the necessary resources.', - image: 'path', - size: 'svg-430', - title: 'Failed to create resources', - }, - }, - failure_reason: 'unmet_prerequisites', - has_trace: false, - runners: { - available: true, - }, - tags: [], - }, - }).then(() => { - expect(findFailedJobComponent().exists()).toBe(true); - })); - }); - - describe('environments block', () => { - it('renders environment block when job has environment', () => - setupAndMount({ - jobData: { - deployment_status: { - environment: { - environment_path: '/path', - name: 'foo', - }, - }, - }, - }).then(() => { - expect(findEnvironmentsBlockComponent().exists()).toBe(true); - })); - - it('does not render environment block when job has environment', () => - setupAndMount().then(() => { - expect(findEnvironmentsBlockComponent().exists()).toBe(false); - })); - }); - - describe('erased block', () => { - it('renders erased block when `erased` is true', () => - setupAndMount({ - jobData: { - erased_by: { - username: 'root', - web_url: 'gitlab.com/root', - }, - erased_at: '2016-11-07T11:11:16.525Z', - }, - }).then(() => { - expect(findErasedBlock().exists()).toBe(true); - })); - - it('does not render erased block when `erased` is false', () => - setupAndMount({ - jobData: { - erased_at: null, - }, - }).then(() => { - expect(findErasedBlock().exists()).toBe(false); - })); - }); - - describe('empty states block', () => { - it('renders empty state when job does not have log and is not running', () => - setupAndMount({ - jobData: { - has_trace: false, - status: { - group: 'pending', - icon: 'status_pending', - label: 'pending', - text: 'pending', - details_path: 'path', - illustration: { - image: 'path', - size: '340', - title: 'Empty State', - content: 'This is an empty state', - }, - action: { - button_title: 'Retry job', - method: 'post', - path: '/path', - }, - }, - }, - }).then(() => { - expect(findEmptyState().exists()).toBe(true); - })); - - it('does not render empty state when job does not have log but it is running', () => - setupAndMount({ - jobData: { - has_trace: false, - status: { - group: 'running', - icon: 'status_running', - label: 'running', - text: 'running', - details_path: 'path', - }, - }, - }).then(() => { - expect(findEmptyState().exists()).toBe(false); - })); - - it('does not render empty state when job has log but it is not running', () => - setupAndMount({ jobData: { has_trace: true } }).then(() => { - expect(findEmptyState().exists()).toBe(false); - })); - - it('displays remaining time for a delayed job', () => { - const oneHourInMilliseconds = 3600000; - jest - .spyOn(Date, 'now') - .mockImplementation( - () => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds, - ); - return setupAndMount({ jobData: delayedJobFixture }).then(() => { - expect(findEmptyState().exists()).toBe(true); - - const title = findJobEmptyStateTitle().text(); - - expect(title).toEqual('This is a delayed job to run in 01:00:00'); - }); - }); - }); - - describe('sidebar', () => { - it('has no blank blocks', async () => { - await setupAndMount({ - jobData: { - duration: null, - finished_at: null, - erased_at: null, - queued: null, - runner: null, - coverage: null, - tags: [], - cancel_path: null, - }, - }); - - const blocks = wrapper.findAll('.blocks-container > *').wrappers; - expect(blocks.length).toBeGreaterThan(0); - - blocks.forEach((block) => { - expect(block.text().trim()).not.toBe(''); - }); - }); - }); - }); - - describe('archived job', () => { - beforeEach(() => setupAndMount({ jobData: { archived: true } })); - - it('renders warning about job being archived', () => { - expect(findArchivedJob().exists()).toBe(true); - }); - }); - - describe('non-archived job', () => { - beforeEach(() => setupAndMount()); - - it('does not warning about job being archived', () => { - expect(findArchivedJob().exists()).toBe(false); - }); - }); - - describe('job log controls', () => { - beforeEach(() => - setupAndMount({ - jobLogData: { - html: 'Update', - status: 'success', - append: false, - size: 50, - total: 100, - complete: true, - }, - }), - ); - - it('should render scroll buttons', () => { - expect(findJobLogScrollTop().exists()).toBe(true); - expect(findJobLogScrollBottom().exists()).toBe(true); - }); - - it('should render link to raw ouput', () => { - expect(findJobLogController().exists()).toBe(true); - }); - - it('should render link to erase job', () => { - expect(findJobLogEraseLink().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/jobs/components/job_container_item_spec.js b/spec/frontend/jobs/components/job_container_item_spec.js deleted file mode 100644 index eb2b0184e5f..00000000000 --- a/spec/frontend/jobs/components/job_container_item_spec.js +++ /dev/null @@ -1,98 +0,0 @@ -import { GlIcon, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import delayedJobFixture from 'test_fixtures/jobs/delayed.json'; -import JobContainerItem from '~/jobs/components/job_container_item.vue'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import job from '../mock_data'; - -describe('JobContainerItem', () => { - let wrapper; - - const findCiIconComponent = () => wrapper.findComponent(CiIcon); - const findGlIconComponent = () => wrapper.findComponent(GlIcon); - - function createComponent(jobData = {}, props = { isActive: false, retried: false }) { - wrapper = shallowMount(JobContainerItem, { - propsData: { - job: { - ...jobData, - retried: props.retried, - }, - isActive: props.isActive, - }, - }); - } - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('when a job is not active and not retried', () => { - beforeEach(() => { - createComponent(job); - }); - - it('displays a status icon', () => { - const ciIcon = findCiIconComponent(); - - expect(ciIcon.props('status')).toBe(job.status); - }); - - it('displays the job name', () => { - expect(wrapper.text()).toContain(job.name); - }); - - it('displays a link to the job', () => { - const link = wrapper.findComponent(GlLink); - - expect(link.attributes('href')).toBe(job.status.details_path); - }); - }); - - describe('when a job is active', () => { - beforeEach(() => { - createComponent(job, { isActive: true }); - }); - - it('displays an arrow sprite icon', () => { - const icon = findGlIconComponent(); - - expect(icon.props('name')).toBe('arrow-right'); - }); - }); - - describe('when a job is retried', () => { - beforeEach(() => { - createComponent(job, { isActive: false, retried: true }); - }); - - it('displays a retry icon', () => { - const icon = findGlIconComponent(); - - expect(icon.props('name')).toBe('retry'); - }); - }); - - describe('for a delayed job', () => { - beforeEach(() => { - const remainingMilliseconds = 1337000; - jest - .spyOn(Date, 'now') - .mockImplementation( - () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds, - ); - - createComponent(delayedJobFixture); - }); - - it('displays remaining time in tooltip', async () => { - await nextTick(); - - const link = wrapper.findComponent(GlLink); - - expect(link.attributes('title')).toMatch('delayed job - delayed manual action (00:22:17)'); - }); - }); -}); diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js deleted file mode 100644 index aa85253a177..00000000000 --- a/spec/frontend/jobs/components/job_log_controllers_spec.js +++ /dev/null @@ -1,315 +0,0 @@ -import { GlSearchBoxByClick } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import JobLogControllers from '~/jobs/components/job_log_controllers.vue'; -import HelpPopover from '~/vue_shared/components/help_popover.vue'; -import { backoffMockImplementation } from 'helpers/backoff_helper'; -import * as commonUtils from '~/lib/utils/common_utils'; -import { mockJobLog } from '../mock_data'; - -const mockToastShow = jest.fn(); - -describe('Job log controllers', () => { - let wrapper; - - beforeEach(() => { - jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation); - }); - - afterEach(() => { - if (wrapper?.destroy) { - wrapper.destroy(); - } - commonUtils.backOff.mockReset(); - }); - - const defaultProps = { - rawPath: '/raw', - erasePath: '/erase', - size: 511952, - isScrollTopDisabled: false, - isScrollBottomDisabled: false, - isScrollingDown: true, - isJobLogSizeVisible: true, - isComplete: true, - jobLog: mockJobLog, - }; - - const createWrapper = (props, { jobLogJumpToFailures = false } = {}) => { - wrapper = mount(JobLogControllers, { - propsData: { - ...defaultProps, - ...props, - }, - provide: { - glFeatures: { - jobLogJumpToFailures, - }, - }, - data() { - return { - searchTerm: '82', - }; - }, - mocks: { - $toast: { - show: mockToastShow, - }, - }, - }); - }; - - const findTruncatedInfo = () => wrapper.find('[data-testid="log-truncated-info"]'); - const findRawLink = () => wrapper.find('[data-testid="raw-link"]'); - const findRawLinkController = () => wrapper.find('[data-testid="job-raw-link-controller"]'); - const findScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]'); - const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); - const findJobLogSearch = () => wrapper.findComponent(GlSearchBoxByClick); - const findSearchHelp = () => wrapper.findComponent(HelpPopover); - const findScrollFailure = () => wrapper.find('[data-testid="job-controller-scroll-to-failure"]'); - - describe('Truncate information', () => { - describe('with isJobLogSizeVisible', () => { - beforeEach(() => { - createWrapper(); - }); - - it('renders size information', () => { - expect(findTruncatedInfo().text()).toMatch('499.95 KiB'); - }); - - it('renders link to raw job log', () => { - expect(findRawLink().attributes('href')).toBe(defaultProps.rawPath); - }); - }); - }); - - describe('links section', () => { - describe('with raw job log path', () => { - beforeEach(() => { - createWrapper(); - }); - - it('renders raw job log link', () => { - expect(findRawLinkController().attributes('href')).toBe(defaultProps.rawPath); - }); - }); - - describe('without raw job log path', () => { - beforeEach(() => { - createWrapper({ - rawPath: null, - }); - }); - - it('does not render raw job log link', () => { - expect(findRawLinkController().exists()).toBe(false); - }); - }); - }); - - describe('scroll buttons', () => { - describe('scroll top button', () => { - describe('when user can scroll top', () => { - beforeEach(() => { - createWrapper({ - isScrollTopDisabled: false, - }); - }); - - it('emits scrollJobLogTop event on click', async () => { - await findScrollTop().trigger('click'); - - expect(wrapper.emitted().scrollJobLogTop).toHaveLength(1); - }); - }); - - describe('when user can not scroll top', () => { - beforeEach(() => { - createWrapper({ - isScrollTopDisabled: true, - isScrollBottomDisabled: false, - isScrollingDown: false, - }); - }); - - it('renders disabled scroll top button', () => { - expect(findScrollTop().attributes('disabled')).toBe('disabled'); - }); - - it('does not emit scrollJobLogTop event on click', async () => { - await findScrollTop().trigger('click'); - - expect(wrapper.emitted().scrollJobLogTop).toBeUndefined(); - }); - }); - }); - - describe('scroll bottom button', () => { - describe('when user can scroll bottom', () => { - beforeEach(() => { - createWrapper(); - }); - - it('emits scrollJobLogBottom event on click', async () => { - await findScrollBottom().trigger('click'); - - expect(wrapper.emitted().scrollJobLogBottom).toHaveLength(1); - }); - }); - - describe('when user can not scroll bottom', () => { - beforeEach(() => { - createWrapper({ - isScrollTopDisabled: false, - isScrollBottomDisabled: true, - isScrollingDown: false, - }); - }); - - it('renders disabled scroll bottom button', () => { - expect(findScrollBottom().attributes('disabled')).toEqual('disabled'); - }); - - it('does not emit scrollJobLogBottom event on click', async () => { - await findScrollBottom().trigger('click'); - - expect(wrapper.emitted().scrollJobLogBottom).toBeUndefined(); - }); - }); - - describe('while isScrollingDown is true', () => { - beforeEach(() => { - createWrapper(); - }); - - it('renders animate class for the scroll down button', () => { - expect(findScrollBottom().classes()).toContain('animate'); - }); - }); - - describe('while isScrollingDown is false', () => { - beforeEach(() => { - createWrapper({ - isScrollTopDisabled: true, - isScrollBottomDisabled: false, - isScrollingDown: false, - }); - }); - - it('does not render animate class for the scroll down button', () => { - expect(findScrollBottom().classes()).not.toContain('animate'); - }); - }); - }); - - describe('scroll to failure button', () => { - describe('with feature flag disabled', () => { - it('does not display button', () => { - createWrapper(); - - expect(findScrollFailure().exists()).toBe(false); - }); - }); - - describe('with red text failures on the page', () => { - let firstFailure; - let secondFailure; - - beforeEach(() => { - jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce(['mock-element']); - - createWrapper({}, { jobLogJumpToFailures: true }); - - firstFailure = document.createElement('div'); - firstFailure.className = 'term-fg-l-red'; - document.body.appendChild(firstFailure); - - secondFailure = document.createElement('div'); - secondFailure.className = 'term-fg-l-red'; - document.body.appendChild(secondFailure); - }); - - afterEach(() => { - if (firstFailure) { - firstFailure.remove(); - firstFailure = null; - } - - if (secondFailure) { - secondFailure.remove(); - secondFailure = null; - } - }); - - it('is enabled', () => { - expect(findScrollFailure().props('disabled')).toBe(false); - }); - - it('scrolls to each failure', async () => { - jest.spyOn(firstFailure, 'scrollIntoView'); - - await findScrollFailure().trigger('click'); - - expect(firstFailure.scrollIntoView).toHaveBeenCalled(); - - await findScrollFailure().trigger('click'); - - expect(secondFailure.scrollIntoView).toHaveBeenCalled(); - - await findScrollFailure().trigger('click'); - - expect(firstFailure.scrollIntoView).toHaveBeenCalled(); - }); - }); - - describe('with no red text failures on the page', () => { - beforeEach(() => { - jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce([]); - - createWrapper({}, { jobLogJumpToFailures: true }); - }); - - it('is disabled', () => { - expect(findScrollFailure().props('disabled')).toBe(true); - }); - }); - - describe('when the job log is not complete', () => { - beforeEach(() => { - jest.spyOn(document, 'querySelectorAll').mockReturnValueOnce(['mock-element']); - - createWrapper({ isComplete: false }, { jobLogJumpToFailures: true }); - }); - - it('is enabled', () => { - expect(findScrollFailure().props('disabled')).toBe(false); - }); - }); - }); - }); - - describe('Job log search', () => { - beforeEach(() => { - createWrapper(); - }); - - it('displays job log search', () => { - expect(findJobLogSearch().exists()).toBe(true); - expect(findSearchHelp().exists()).toBe(true); - }); - - it('emits search results', () => { - const expectedSearchResults = [[[mockJobLog[6].lines[1], mockJobLog[6].lines[2]]]]; - - findJobLogSearch().vm.$emit('submit'); - - expect(wrapper.emitted('searchResults')).toEqual(expectedSearchResults); - }); - - it('clears search results', () => { - findJobLogSearch().vm.$emit('clear'); - - expect(wrapper.emitted('searchResults')).toEqual([[[]]]); - }); - }); -}); diff --git a/spec/frontend/jobs/components/job_retry_forward_deployment_modal_spec.js b/spec/frontend/jobs/components/job_retry_forward_deployment_modal_spec.js deleted file mode 100644 index 08973223c08..00000000000 --- a/spec/frontend/jobs/components/job_retry_forward_deployment_modal_spec.js +++ /dev/null @@ -1,76 +0,0 @@ -import { GlLink, GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import JobRetryForwardDeploymentModal from '~/jobs/components/job_retry_forward_deployment_modal.vue'; -import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '~/jobs/constants'; -import createStore from '~/jobs/store'; -import job from '../mock_data'; - -describe('Job Retry Forward Deployment Modal', () => { - let store; - let wrapper; - - const retryOutdatedJobDocsUrl = 'url-to-docs'; - const findLink = () => wrapper.find(GlLink); - const findModal = () => wrapper.find(GlModal); - - const createWrapper = ({ props = {}, provide = {}, stubs = {} } = {}) => { - store = createStore(); - wrapper = shallowMount(JobRetryForwardDeploymentModal, { - propsData: { - modalId: 'modal-id', - href: job.retry_path, - ...props, - }, - provide, - store, - stubs, - }); - }; - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); - - beforeEach(createWrapper); - - describe('Modal configuration', () => { - it('should display the correct messages', () => { - const modal = findModal(); - expect(modal.attributes('title')).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.title); - expect(modal.text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.info); - expect(modal.text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.areYouSure); - }); - }); - - describe('Modal docs help link', () => { - it('should not display an info link when none is provided', () => { - createWrapper(); - - expect(findLink().exists()).toBe(false); - }); - - it('should display an info link when one is provided', () => { - createWrapper({ provide: { retryOutdatedJobDocsUrl } }); - - expect(findLink().attributes('href')).toBe(retryOutdatedJobDocsUrl); - expect(findLink().text()).toMatch(JOB_RETRY_FORWARD_DEPLOYMENT_MODAL.moreInfo); - }); - }); - - describe('Modal actions', () => { - beforeEach(createWrapper); - - it('should correctly configure the primary action', () => { - expect(findModal().props('actionPrimary').attributes).toMatchObject([ - { - 'data-method': 'post', - href: job.retry_path, - variant: 'danger', - }, - ]); - }); - }); -}); diff --git a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js b/spec/frontend/jobs/components/job_sidebar_details_container_spec.js deleted file mode 100644 index 4046f0269dd..00000000000 --- a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import DetailRow from '~/jobs/components/sidebar_detail_row.vue'; -import SidebarJobDetailsContainer from '~/jobs/components/sidebar_job_details_container.vue'; -import createStore from '~/jobs/store'; -import job from '../mock_data'; - -describe('Job Sidebar Details Container', () => { - let store; - let wrapper; - - const findJobTimeout = () => wrapper.findByTestId('job-timeout'); - const findJobTags = () => wrapper.findByTestId('job-tags'); - const findAllDetailsRow = () => wrapper.findAll(DetailRow); - - const createWrapper = ({ props = {} } = {}) => { - store = createStore(); - wrapper = extendedWrapper( - shallowMount(SidebarJobDetailsContainer, { - propsData: props, - store, - stubs: { - DetailRow, - }, - }), - ); - }; - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } - }); - - describe('when no details are available', () => { - beforeEach(() => { - createWrapper(); - }); - - it('should render an empty container', () => { - expect(wrapper.html()).toBe(''); - }); - - it.each(['duration', 'erased_at', 'finished_at', 'queued_at', 'runner', 'coverage'])( - 'should not render %s details when missing', - async (detail) => { - await store.dispatch('receiveJobSuccess', { [detail]: undefined }); - - expect(findAllDetailsRow()).toHaveLength(0); - }, - ); - }); - - describe('when some of the details are available', () => { - beforeEach(createWrapper); - - it.each([ - ['duration', 'Elapsed time: 6 seconds'], - ['erased_at', 'Erased: 3 weeks ago'], - ['finished_at', 'Finished: 3 weeks ago'], - ['queued_duration', 'Queued: 9 seconds'], - ['runner', 'Runner: #1 (ABCDEFGH) local ci runner'], - ['coverage', 'Coverage: 20%'], - ])('uses %s to render job-%s', async (detail, value) => { - await store.dispatch('receiveJobSuccess', { [detail]: job[detail] }); - const detailsRow = findAllDetailsRow(); - - expect(detailsRow).toHaveLength(1); - expect(detailsRow.at(0).text()).toBe(value); - }); - - it('only renders tags', async () => { - const { tags } = job; - await store.dispatch('receiveJobSuccess', { tags }); - const tagsComponent = findJobTags(); - - expect(tagsComponent.text()).toBe('Tags: tag'); - }); - }); - - describe('when all the info are available', () => { - it('renders all the details components', async () => { - createWrapper(); - await store.dispatch('receiveJobSuccess', job); - - expect(findAllDetailsRow()).toHaveLength(7); - }); - - describe('duration row', () => { - it('renders all the details components', async () => { - createWrapper(); - await store.dispatch('receiveJobSuccess', job); - - expect(findAllDetailsRow().at(0).text()).toBe('Duration: 6 seconds'); - }); - }); - }); - - describe('timeout', () => { - const { - metadata: { timeout_human_readable, timeout_source }, - } = job; - - beforeEach(createWrapper); - - it('does not render if metadata is empty', async () => { - const metadata = {}; - await store.dispatch('receiveJobSuccess', { metadata }); - const detailsRow = findAllDetailsRow(); - - expect(wrapper.html()).toBe(''); - expect(detailsRow.exists()).toBe(false); - }); - - it('uses metadata to render timeout', async () => { - const metadata = { timeout_human_readable }; - await store.dispatch('receiveJobSuccess', { metadata }); - const detailsRow = findAllDetailsRow(); - - expect(detailsRow).toHaveLength(1); - expect(detailsRow.at(0).text()).toBe('Timeout: 1m 40s'); - }); - - it('uses metadata to render timeout and the source', async () => { - const metadata = { timeout_human_readable, timeout_source }; - await store.dispatch('receiveJobSuccess', { metadata }); - const detailsRow = findAllDetailsRow(); - - expect(detailsRow.at(0).text()).toBe('Timeout: 1m 40s (from runner)'); - }); - - it('should not render when no time is provided', async () => { - const metadata = { timeout_source }; - await store.dispatch('receiveJobSuccess', { metadata }); - - expect(findJobTimeout().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js deleted file mode 100644 index ad72b9be261..00000000000 --- a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import JobsSidebarRetryButton from '~/jobs/components/job_sidebar_retry_button.vue'; -import createStore from '~/jobs/store'; -import job from '../mock_data'; - -describe('Job Sidebar Retry Button', () => { - let store; - let wrapper; - - const forwardDeploymentFailure = 'forward_deployment_failure'; - const findRetryButton = () => wrapper.findByTestId('retry-job-button'); - const findRetryLink = () => wrapper.findByTestId('retry-job-link'); - - const createWrapper = ({ props = {} } = {}) => { - store = createStore(); - wrapper = shallowMountExtended(JobsSidebarRetryButton, { - propsData: { - href: job.retry_path, - modalId: 'modal-id', - ...props, - }, - store, - }); - }; - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - beforeEach(createWrapper); - - it.each([ - [null, false, true], - ['unmet_prerequisites', false, true], - [forwardDeploymentFailure, true, false], - ])( - 'when error is: %s, should render button: %s | should render link: %s', - async (failureReason, buttonExists, linkExists) => { - await store.dispatch('receiveJobSuccess', { ...job, failure_reason: failureReason }); - - expect(findRetryButton().exists()).toBe(buttonExists); - expect(findRetryLink().exists()).toBe(linkExists); - }, - ); - - describe('Button', () => { - it('should have the correct configuration', async () => { - await store.dispatch('receiveJobSuccess', { failure_reason: forwardDeploymentFailure }); - - expect(findRetryButton().attributes()).toMatchObject({ - category: 'primary', - variant: 'confirm', - icon: 'retry', - }); - }); - }); - - describe('Link', () => { - it('should have the correct configuration', () => { - expect(findRetryLink().attributes()).toMatchObject({ - 'data-method': 'post', - href: job.retry_path, - icon: 'retry', - }); - }); - }); -}); diff --git a/spec/frontend/jobs/components/jobs_container_spec.js b/spec/frontend/jobs/components/jobs_container_spec.js deleted file mode 100644 index 127570b8184..00000000000 --- a/spec/frontend/jobs/components/jobs_container_spec.js +++ /dev/null @@ -1,147 +0,0 @@ -import { GlLink } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import JobsContainer from '~/jobs/components/jobs_container.vue'; - -describe('Jobs List block', () => { - let wrapper; - - const retried = { - status: { - details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', - group: 'success', - has_details: true, - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed', - }, - id: 233432756, - tooltip: 'build - passed', - retried: true, - }; - - const active = { - name: 'test', - status: { - details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', - group: 'success', - has_details: true, - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed', - }, - id: 2322756, - tooltip: 'build - passed', - active: true, - }; - - const job = { - name: 'build', - status: { - details_path: '/gitlab-org/gitlab-foss/pipelines/28029444', - group: 'success', - has_details: true, - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed', - }, - id: 232153, - tooltip: 'build - passed', - }; - - const findAllJobs = () => wrapper.findAllComponents(GlLink); - const findJob = () => findAllJobs().at(0); - - const findArrowIcon = () => wrapper.findByTestId('arrow-right-icon'); - const findRetryIcon = () => wrapper.findByTestId('retry-icon'); - - const createComponent = (props) => { - wrapper = extendedWrapper( - mount(JobsContainer, { - propsData: { - ...props, - }, - }), - ); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a list of jobs', () => { - createComponent({ - jobs: [job, retried, active], - jobId: 12313, - }); - - expect(findAllJobs()).toHaveLength(3); - }); - - it('renders the arrow right icon when job id matches `jobId`', () => { - createComponent({ - jobs: [active], - jobId: active.id, - }); - - expect(findArrowIcon().exists()).toBe(true); - }); - - it('does not render the arrow right icon when the job is not active', () => { - createComponent({ - jobs: [job], - jobId: active.id, - }); - - expect(findArrowIcon().exists()).toBe(false); - }); - - it('renders the job name when present', () => { - createComponent({ - jobs: [job], - jobId: active.id, - }); - - expect(findJob().text()).toBe(job.name); - expect(findJob().text()).not.toContain(job.id.toString()); - }); - - it('renders job id when job name is not available', () => { - createComponent({ - jobs: [retried], - jobId: active.id, - }); - - expect(findJob().text()).toBe(retried.id.toString()); - }); - - it('links to the job page', () => { - createComponent({ - jobs: [job], - jobId: active.id, - }); - - expect(findJob().attributes('href')).toBe(job.status.details_path); - }); - - it('renders retry icon when job was retried', () => { - createComponent({ - jobs: [retried], - jobId: active.id, - }); - - expect(findRetryIcon().exists()).toBe(true); - }); - - it('does not render retry icon when job was not retried', () => { - createComponent({ - jobs: [job], - jobId: active.id, - }); - - expect(findRetryIcon().exists()).toBe(false); - }); -}); diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js index bdc8ae0eef0..ec8e79bba13 100644 --- a/spec/frontend/jobs/components/log/line_header_spec.js +++ b/spec/frontend/jobs/components/log/line_header_spec.js @@ -39,7 +39,7 @@ describe('Job Log Header Line', () => { }); it('renders the line number component', () => { - expect(wrapper.find(LineNumber).exists()).toBe(true); + expect(wrapper.findComponent(LineNumber).exists()).toBe(true); }); it('renders a span the provided text', () => { @@ -90,7 +90,7 @@ describe('Job Log Header Line', () => { }); it('renders the duration badge', () => { - expect(wrapper.find(DurationBadge).exists()).toBe(true); + expect(wrapper.findComponent(DurationBadge).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/jobs/components/log/line_spec.js b/spec/frontend/jobs/components/log/line_spec.js index bf80d90e299..50ebd1610d2 100644 --- a/spec/frontend/jobs/components/log/line_spec.js +++ b/spec/frontend/jobs/components/log/line_spec.js @@ -42,7 +42,7 @@ describe('Job Log Line', () => { }); it('renders the line number component', () => { - expect(wrapper.find(LineNumber).exists()).toBe(true); + expect(wrapper.findComponent(LineNumber).exists()).toBe(true); }); it('renders a span the provided text', () => { diff --git a/spec/frontend/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js deleted file mode 100644 index 6faab3ddf31..00000000000 --- a/spec/frontend/jobs/components/manual_variables_form_spec.js +++ /dev/null @@ -1,156 +0,0 @@ -import { GlSprintf, GlLink } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -import Vuex from 'vuex'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import ManualVariablesForm from '~/jobs/components/manual_variables_form.vue'; - -Vue.use(Vuex); - -describe('Manual Variables Form', () => { - let wrapper; - let store; - - const requiredProps = { - action: { - path: '/play', - method: 'post', - button_title: 'Trigger this manual action', - }, - }; - - const createComponent = (props = {}) => { - store = new Vuex.Store({ - actions: { - triggerManualJob: jest.fn(), - }, - }); - - wrapper = extendedWrapper( - mount(ManualVariablesForm, { - propsData: { ...requiredProps, ...props }, - store, - stubs: { - GlSprintf, - }, - }), - ); - }; - - const findHelpText = () => wrapper.findComponent(GlSprintf); - const findHelpLink = () => wrapper.findComponent(GlLink); - - const findTriggerBtn = () => wrapper.findByTestId('trigger-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'); - const findCiVariableKey = () => wrapper.findByTestId('ci-variable-key'); - const findAllCiVariableKeys = () => wrapper.findAllByTestId('ci-variable-key'); - const findCiVariableValue = () => wrapper.findByTestId('ci-variable-value'); - const findAllVariables = () => wrapper.findAllByTestId('ci-variable-row'); - - const setCiVariableKey = () => { - findCiVariableKey().setValue('new key'); - findCiVariableKey().vm.$emit('change'); - nextTick(); - }; - - const setCiVariableKeyByPosition = (position, value) => { - findAllCiVariableKeys().at(position).setValue(value); - findAllCiVariableKeys().at(position).vm.$emit('change'); - nextTick(); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('creates a new variable when user enters a new key value', async () => { - expect(findAllVariables()).toHaveLength(1); - - await setCiVariableKey(); - - expect(findAllVariables()).toHaveLength(2); - }); - - it('does not create extra empty variables', async () => { - expect(findAllVariables()).toHaveLength(1); - - await setCiVariableKey(); - - expect(findAllVariables()).toHaveLength(2); - - await setCiVariableKey(); - - expect(findAllVariables()).toHaveLength(2); - }); - - it('removes the correct variable row', async () => { - const variableKeyNameOne = 'key-one'; - const variableKeyNameThree = 'key-three'; - - await setCiVariableKeyByPosition(0, variableKeyNameOne); - - await setCiVariableKeyByPosition(1, 'key-two'); - - await setCiVariableKeyByPosition(2, variableKeyNameThree); - - expect(findAllVariables()).toHaveLength(4); - - await findAllDeleteVarBtns().at(1).trigger('click'); - - expect(findAllVariables()).toHaveLength(3); - - expect(findAllCiVariableKeys().at(0).element.value).toBe(variableKeyNameOne); - expect(findAllCiVariableKeys().at(1).element.value).toBe(variableKeyNameThree); - expect(findAllCiVariableKeys().at(2).element.value).toBe(''); - }); - - it('trigger button is disabled after trigger action', async () => { - expect(findTriggerBtn().props('disabled')).toBe(false); - - await findTriggerBtn().trigger('click'); - - expect(findTriggerBtn().props('disabled')).toBe(true); - }); - - it('delete variable button should only show when there is more than one variable', async () => { - expect(findDeleteVarBtn().exists()).toBe(false); - - await setCiVariableKey(); - - expect(findDeleteVarBtn().exists()).toBe(true); - }); - - it('delete variable button placeholder should only exist when a user cannot remove', async () => { - expect(findDeleteVarBtnPlaceholder().exists()).toBe(true); - }); - - it('renders help text with provided link', () => { - expect(findHelpText().exists()).toBe(true); - expect(findHelpLink().attributes('href')).toBe( - '/help/ci/variables/index#add-a-cicd-variable-to-a-project', - ); - }); - - it('passes variables in correct format', async () => { - jest.spyOn(store, 'dispatch'); - - await setCiVariableKey(); - - await findCiVariableValue().setValue('new value'); - - await findTriggerBtn().trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith('triggerManualJob', [ - { - key: 'new key', - secret_value: 'new value', - }, - ]); - }); -}); diff --git a/spec/frontend/jobs/components/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/sidebar_detail_row_spec.js deleted file mode 100644 index 8d2680608ab..00000000000 --- a/spec/frontend/jobs/components/sidebar_detail_row_spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import { GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import SidebarDetailRow from '~/jobs/components/sidebar_detail_row.vue'; - -describe('Sidebar detail row', () => { - let wrapper; - - const title = 'this is the title'; - const value = 'this is the value'; - const helpUrl = 'https://docs.gitlab.com/runner/register/index.html'; - - const findHelpLink = () => wrapper.findComponent(GlLink); - - const createComponent = (props) => { - wrapper = shallowMount(SidebarDetailRow, { - propsData: { - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('with title/value and without helpUrl', () => { - beforeEach(() => { - createComponent({ title, value }); - }); - - it('should render the provided title and value', () => { - expect(wrapper.text()).toBe(`${title}: ${value}`); - }); - - it('should not render the help link', () => { - expect(findHelpLink().exists()).toBe(false); - }); - }); - - describe('when helpUrl provided', () => { - beforeEach(() => { - createComponent({ - helpUrl, - title, - value, - }); - }); - - it('should render the help link', () => { - expect(findHelpLink().exists()).toBe(true); - expect(findHelpLink().attributes('href')).toBe(helpUrl); - }); - }); -}); diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/sidebar_spec.js deleted file mode 100644 index 39c71986ce4..00000000000 --- a/spec/frontend/jobs/components/sidebar_spec.js +++ /dev/null @@ -1,227 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import ArtifactsBlock from '~/jobs/components/artifacts_block.vue'; -import JobRetryForwardDeploymentModal from '~/jobs/components/job_retry_forward_deployment_modal.vue'; -import JobRetryButton from '~/jobs/components/job_sidebar_retry_button.vue'; -import JobsContainer from '~/jobs/components/jobs_container.vue'; -import Sidebar, { forwardDeploymentFailureModalId } from '~/jobs/components/sidebar.vue'; -import StagesDropdown from '~/jobs/components/stages_dropdown.vue'; -import createStore from '~/jobs/store'; -import job, { jobsInStage } from '../mock_data'; - -describe('Sidebar details block', () => { - let store; - let wrapper; - - const forwardDeploymentFailure = 'forward_deployment_failure'; - const findModal = () => wrapper.find(JobRetryForwardDeploymentModal); - const findArtifactsBlock = () => wrapper.findComponent(ArtifactsBlock); - const findCancelButton = () => wrapper.findByTestId('cancel-button'); - const findNewIssueButton = () => wrapper.findByTestId('job-new-issue'); - const findRetryButton = () => wrapper.find(JobRetryButton); - const findTerminalLink = () => wrapper.findByTestId('terminal-link'); - const findEraseLink = () => wrapper.findByTestId('job-log-erase-link'); - - const createWrapper = (props) => { - store = createStore(); - - store.state.job = job; - - wrapper = extendedWrapper( - shallowMount(Sidebar, { - propsData: { - ...props, - }, - - store, - }), - ); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('when job log is erasable', () => { - const path = '/root/ci-project/-/jobs/1447/erase'; - - beforeEach(() => { - createWrapper({ - erasePath: path, - }); - }); - - it('renders erase job link', () => { - expect(findEraseLink().exists()).toBe(true); - }); - - it('erase job link has correct path', () => { - expect(findEraseLink().attributes('href')).toBe(path); - }); - }); - - describe('when job log is not erasable', () => { - beforeEach(() => { - createWrapper(); - }); - - it('does not render erase button', () => { - expect(findEraseLink().exists()).toBe(false); - }); - }); - - describe('when there is no retry path retry', () => { - it('should not render a retry button', async () => { - createWrapper(); - const copy = { ...job, retry_path: null }; - await store.dispatch('receiveJobSuccess', copy); - - expect(findRetryButton().exists()).toBe(false); - }); - }); - - describe('without terminal path', () => { - it('does not render terminal link', async () => { - createWrapper(); - await store.dispatch('receiveJobSuccess', job); - - expect(findTerminalLink().exists()).toBe(false); - }); - }); - - describe('with terminal path', () => { - it('renders terminal link', async () => { - createWrapper(); - await store.dispatch('receiveJobSuccess', { ...job, terminal_path: 'job/43123/terminal' }); - - expect(findTerminalLink().exists()).toBe(true); - }); - }); - - describe('actions', () => { - beforeEach(() => { - createWrapper(); - return store.dispatch('receiveJobSuccess', job); - }); - - it('should render link to new issue', () => { - expect(findNewIssueButton().attributes('href')).toBe(job.new_issue_path); - expect(findNewIssueButton().text()).toBe('New issue'); - }); - - it('should render the retry button', () => { - expect(findRetryButton().props('href')).toBe(job.retry_path); - }); - - it('should render link to cancel job', () => { - expect(findCancelButton().props('icon')).toBe('cancel'); - expect(findCancelButton().attributes('href')).toBe(job.cancel_path); - }); - }); - - describe('forward deployment failure', () => { - describe('when the relevant data is missing', () => { - it.each` - retryPath | failureReason - ${null} | ${null} - ${''} | ${''} - ${job.retry_path} | ${''} - ${''} | ${forwardDeploymentFailure} - ${job.retry_path} | ${'unmet_prerequisites'} - `( - 'should not render the modal when path and failure are $retryPath, $failureReason', - async ({ retryPath, failureReason }) => { - createWrapper(); - await store.dispatch('receiveJobSuccess', { - ...job, - failure_reason: failureReason, - retry_path: retryPath, - }); - expect(findModal().exists()).toBe(false); - }, - ); - }); - - describe('when there is the relevant error', () => { - beforeEach(() => { - createWrapper(); - return store.dispatch('receiveJobSuccess', { - ...job, - failure_reason: forwardDeploymentFailure, - }); - }); - - it('should render the modal', () => { - expect(findModal().exists()).toBe(true); - }); - - it('should provide the modal id to the button and modal', () => { - expect(findRetryButton().props('modalId')).toBe(forwardDeploymentFailureModalId); - expect(findModal().props('modalId')).toBe(forwardDeploymentFailureModalId); - }); - - it('should provide the retry path to the button and modal', () => { - expect(findRetryButton().props('href')).toBe(job.retry_path); - expect(findModal().props('href')).toBe(job.retry_path); - }); - }); - }); - - describe('stages dropdown', () => { - beforeEach(() => { - createWrapper(); - return store.dispatch('receiveJobSuccess', { ...job, stage: 'aStage' }); - }); - - describe('with stages', () => { - it('renders value provided as selectedStage as selected', () => { - expect(wrapper.find(StagesDropdown).props('selectedStage')).toBe('aStage'); - }); - }); - - describe('without jobs for stages', () => { - beforeEach(() => store.dispatch('receiveJobSuccess', job)); - - it('does not render jobs container', () => { - expect(wrapper.find(JobsContainer).exists()).toBe(false); - }); - }); - - describe('with jobs for stages', () => { - beforeEach(async () => { - await store.dispatch('receiveJobSuccess', job); - await store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses); - }); - - it('renders list of jobs', () => { - expect(wrapper.find(JobsContainer).exists()).toBe(true); - }); - }); - }); - - describe('artifacts', () => { - beforeEach(() => { - createWrapper(); - }); - - it('artifacts are not shown if there are no properties other than locked', () => { - expect(findArtifactsBlock().exists()).toBe(false); - }); - - it('artifacts are shown if present', async () => { - store.state.job.artifact = { - download_path: '/root/ci-project/-/jobs/1960/artifacts/download', - browse_path: '/root/ci-project/-/jobs/1960/artifacts/browse', - keep_path: '/root/ci-project/-/jobs/1960/artifacts/keep', - expire_at: '2021-03-23T17:57:11.211Z', - expired: false, - locked: false, - }; - - await nextTick(); - - expect(findArtifactsBlock().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/jobs/components/stages_dropdown_spec.js b/spec/frontend/jobs/components/stages_dropdown_spec.js deleted file mode 100644 index f638213ef0c..00000000000 --- a/spec/frontend/jobs/components/stages_dropdown_spec.js +++ /dev/null @@ -1,192 +0,0 @@ -import { GlDropdown, GlDropdownItem, GlLink, GlSprintf } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Mousetrap from 'mousetrap'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import StagesDropdown from '~/jobs/components/stages_dropdown.vue'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import * as copyToClipboard from '~/behaviors/copy_to_clipboard'; -import { - mockPipelineWithoutRef, - mockPipelineWithoutMR, - mockPipelineWithAttachedMR, - mockPipelineDetached, -} from '../mock_data'; - -describe('Stages Dropdown', () => { - let wrapper; - - const findStatus = () => wrapper.findComponent(CiIcon); - const findSelectedStageText = () => wrapper.findComponent(GlDropdown).props('text'); - const findStageItem = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); - - const findPipelineInfoText = () => wrapper.findByTestId('pipeline-info').text(); - - const createComponent = (props) => { - wrapper = extendedWrapper( - shallowMount(StagesDropdown, { - propsData: { - stages: [], - selectedStage: 'deploy', - ...props, - }, - stubs: { - GlSprintf, - GlLink, - }, - }), - ); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('without a merge request pipeline', () => { - beforeEach(() => { - createComponent({ - pipeline: mockPipelineWithoutMR, - stages: [{ name: 'build' }, { name: 'test' }], - }); - }); - - it('renders pipeline status', () => { - expect(findStatus().exists()).toBe(true); - }); - - it('renders dropdown with stages', () => { - expect(findStageItem(0).text()).toBe('build'); - }); - - it('rendes selected stage', () => { - expect(findSelectedStageText()).toBe('deploy'); - }); - }); - - describe('pipelineInfo', () => { - const allElements = [ - 'pipeline-path', - 'mr-link', - 'source-ref-link', - 'copy-source-ref-link', - 'source-branch-link', - 'copy-source-branch-link', - 'target-branch-link', - 'copy-target-branch-link', - ]; - describe.each([ - [ - 'does not have a ref', - { - pipeline: mockPipelineWithoutRef, - text: `Pipeline #${mockPipelineWithoutRef.id}`, - foundElements: [ - { testId: 'pipeline-path', props: [{ href: mockPipelineWithoutRef.path }] }, - ], - }, - ], - [ - 'hasRef but not triggered by MR', - { - pipeline: mockPipelineWithoutMR, - text: `Pipeline #${mockPipelineWithoutMR.id} for ${mockPipelineWithoutMR.ref.name}`, - foundElements: [ - { testId: 'pipeline-path', props: [{ href: mockPipelineWithoutMR.path }] }, - { testId: 'source-ref-link', props: [{ href: mockPipelineWithoutMR.ref.path }] }, - { testId: 'copy-source-ref-link', props: [{ text: mockPipelineWithoutMR.ref.name }] }, - ], - }, - ], - [ - 'hasRef and MR but not MR pipeline', - { - pipeline: mockPipelineDetached, - text: `Pipeline #${mockPipelineDetached.id} for !${mockPipelineDetached.merge_request.iid} with ${mockPipelineDetached.merge_request.source_branch}`, - foundElements: [ - { testId: 'pipeline-path', props: [{ href: mockPipelineDetached.path }] }, - { testId: 'mr-link', props: [{ href: mockPipelineDetached.merge_request.path }] }, - { - testId: 'source-branch-link', - props: [{ href: mockPipelineDetached.merge_request.source_branch_path }], - }, - { - testId: 'copy-source-branch-link', - props: [{ text: mockPipelineDetached.merge_request.source_branch }], - }, - ], - }, - ], - [ - 'hasRef and MR and MR pipeline', - { - pipeline: mockPipelineWithAttachedMR, - text: `Pipeline #${mockPipelineWithAttachedMR.id} for !${mockPipelineWithAttachedMR.merge_request.iid} with ${mockPipelineWithAttachedMR.merge_request.source_branch} into ${mockPipelineWithAttachedMR.merge_request.target_branch}`, - foundElements: [ - { testId: 'pipeline-path', props: [{ href: mockPipelineWithAttachedMR.path }] }, - { testId: 'mr-link', props: [{ href: mockPipelineWithAttachedMR.merge_request.path }] }, - { - testId: 'source-branch-link', - props: [{ href: mockPipelineWithAttachedMR.merge_request.source_branch_path }], - }, - { - testId: 'copy-source-branch-link', - props: [{ text: mockPipelineWithAttachedMR.merge_request.source_branch }], - }, - { - testId: 'target-branch-link', - props: [{ href: mockPipelineWithAttachedMR.merge_request.target_branch_path }], - }, - { - testId: 'copy-target-branch-link', - props: [{ text: mockPipelineWithAttachedMR.merge_request.target_branch }], - }, - ], - }, - ], - ])('%s', (_, { pipeline, text, foundElements }) => { - beforeEach(() => { - createComponent({ - pipeline, - }); - }); - - it('should render the text', () => { - expect(findPipelineInfoText()).toMatchInterpolatedText(text); - }); - - it('should find components with props', () => { - foundElements.forEach((element) => { - element.props.forEach((prop) => { - const key = Object.keys(prop)[0]; - expect(wrapper.findByTestId(element.testId).attributes(key)).toBe(prop[key]); - }); - }); - }); - - it('should not find components', () => { - const foundTestIds = foundElements.map((element) => element.testId); - allElements - .filter((testId) => !foundTestIds.includes(testId)) - .forEach((testId) => { - expect(wrapper.findByTestId(testId).exists()).toBe(false); - }); - }); - }); - }); - - describe('mousetrap', () => { - it.each([ - ['copy-source-ref-link', mockPipelineWithoutMR], - ['copy-source-branch-link', mockPipelineWithAttachedMR], - ])( - 'calls clickCopyToClipboardButton with `%s` button when `b` is pressed', - (button, pipeline) => { - const copyToClipboardMock = jest.spyOn(copyToClipboard, 'clickCopyToClipboardButton'); - createComponent({ pipeline }); - - Mousetrap.trigger('b'); - - expect(copyToClipboardMock).toHaveBeenCalledWith(wrapper.findByTestId(button).element); - }, - ); - }); -}); diff --git a/spec/frontend/jobs/components/stuck_block_spec.js b/spec/frontend/jobs/components/stuck_block_spec.js deleted file mode 100644 index 1580ed45e46..00000000000 --- a/spec/frontend/jobs/components/stuck_block_spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import { GlBadge, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import StuckBlock from '~/jobs/components/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: { - ...props, - }, - }); - }; - - const tags = ['docker', 'gitlab-org']; - - const findStuckNoActiveRunners = () => - wrapper.find('[data-testid="job-stuck-no-active-runners"]'); - const findStuckNoRunners = () => wrapper.find('[data-testid="job-stuck-no-runners"]'); - const findStuckWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"]'); - const findRunnerPathLink = () => wrapper.find(GlLink); - const findAllBadges = () => wrapper.findAll(GlBadge); - - describe('with no runners for project', () => { - beforeEach(() => { - createWrapper({ - hasOfflineRunnersForProject: true, - runnersPath: '/root/project/runners#js-runners-settings', - }); - }); - - it('renders only information about project not having runners', () => { - expect(findStuckNoRunners().exists()).toBe(true); - expect(findStuckWithTags().exists()).toBe(false); - expect(findStuckNoActiveRunners().exists()).toBe(false); - }); - - it('renders link to runners page', () => { - expect(findRunnerPathLink().attributes('href')).toBe( - '/root/project/runners#js-runners-settings', - ); - }); - }); - - describe('with tags', () => { - beforeEach(() => { - createWrapper({ - hasOfflineRunnersForProject: false, - tags, - runnersPath: '/root/project/runners#js-runners-settings', - }); - }); - - it('renders information about the tags not being set', () => { - expect(findStuckWithTags().exists()).toBe(true); - expect(findStuckNoActiveRunners().exists()).toBe(false); - expect(findStuckNoRunners().exists()).toBe(false); - }); - - it('renders tags', () => { - findAllBadges().wrappers.forEach((badgeElt, index) => { - return expect(badgeElt.text()).toBe(tags[index]); - }); - }); - - it('renders link to runners page', () => { - expect(findRunnerPathLink().attributes('href')).toBe( - '/root/project/runners#js-runners-settings', - ); - }); - }); - - describe('without active runners', () => { - beforeEach(() => { - createWrapper({ - hasOfflineRunnersForProject: false, - runnersPath: '/root/project/runners#js-runners-settings', - }); - }); - - it('renders information about project not having runners', () => { - expect(findStuckNoActiveRunners().exists()).toBe(true); - expect(findStuckNoRunners().exists()).toBe(false); - expect(findStuckWithTags().exists()).toBe(false); - }); - - it('renders link to runners page', () => { - expect(findRunnerPathLink().attributes('href')).toBe( - '/root/project/runners#js-runners-settings', - ); - }); - }); -}); 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 374768c3ee4..8c724a8030b 100644 --- a/spec/frontend/jobs/components/table/job_table_app_spec.js +++ b/spec/frontend/jobs/components/table/job_table_app_spec.js @@ -11,12 +11,14 @@ import VueApollo from 'vue-apollo'; 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 createFlash from '~/flash'; import getJobsQuery from '~/jobs/components/table/graphql/queries/get_jobs.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 * as urlUtils from '~/lib/utils/url_utility'; import { mockJobsResponsePaginated, mockJobsResponseEmpty, @@ -230,5 +232,17 @@ describe('Job table app', () => { expect(createFlash).toHaveBeenCalledWith(expectedWarning); expect(wrapper.vm.$apollo.queries.jobs.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`, + }); + }); }); }); diff --git a/spec/frontend/jobs/components/trigger_block_spec.js b/spec/frontend/jobs/components/trigger_block_spec.js deleted file mode 100644 index 78596612d23..00000000000 --- a/spec/frontend/jobs/components/trigger_block_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import { GlButton, GlTableLite } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import TriggerBlock from '~/jobs/components/trigger_block.vue'; - -describe('Trigger block', () => { - let wrapper; - - const findRevealButton = () => wrapper.findComponent(GlButton); - const findVariableTable = () => wrapper.findComponent(GlTableLite); - const findShortToken = () => wrapper.find('[data-testid="trigger-short-token"]'); - const findVariableValue = (index) => - wrapper.findAll('[data-testid="trigger-build-value"]').at(index); - const findVariableKey = (index) => wrapper.findAll('[data-testid="trigger-build-key"]').at(index); - - const createComponent = (props) => { - wrapper = mount(TriggerBlock, { - propsData: { - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('with short token and no variables', () => { - it('renders short token', () => { - createComponent({ - trigger: { - short_token: '0a666b2', - variables: [], - }, - }); - - expect(findShortToken().text()).toContain('0a666b2'); - }); - }); - - describe('without variables or short token', () => { - beforeEach(() => { - createComponent({ trigger: { variables: [] } }); - }); - - it('does not render short token', () => { - expect(findShortToken().exists()).toBe(false); - }); - - it('does not render variables', () => { - expect(findRevealButton().exists()).toBe(false); - expect(findVariableTable().exists()).toBe(false); - }); - }); - - describe('with variables', () => { - describe('hide/reveal variables', () => { - it('should toggle variables on click', async () => { - const hiddenValue = '••••••'; - const gcsVar = { key: 'UPLOAD_TO_GCS', value: 'false', public: false }; - const s3Var = { key: 'UPLOAD_TO_S3', value: 'true', public: false }; - - createComponent({ - trigger: { - variables: [gcsVar, s3Var], - }, - }); - - expect(findRevealButton().text()).toBe('Reveal values'); - - expect(findVariableValue(0).text()).toBe(hiddenValue); - expect(findVariableValue(1).text()).toBe(hiddenValue); - - expect(findVariableKey(0).text()).toBe(gcsVar.key); - expect(findVariableKey(1).text()).toBe(s3Var.key); - - await findRevealButton().trigger('click'); - - expect(findRevealButton().text()).toBe('Hide values'); - - expect(findVariableValue(0).text()).toBe(gcsVar.value); - expect(findVariableValue(1).text()).toBe(s3Var.value); - }); - }); - }); -}); diff --git a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js deleted file mode 100644 index aeb85694e60..00000000000 --- a/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import { GlAlert, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue'; - -describe('Unmet Prerequisites Block Job component', () => { - let wrapper; - const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs'; - - const createComponent = () => { - wrapper = shallowMount(UnmetPrerequisitesBlock, { - propsData: { - helpPath, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders an alert with the correct message', () => { - const container = wrapper.find(GlAlert); - const alertMessage = - 'This job failed because the necessary resources were not successfully created.'; - - expect(container).not.toBeNull(); - expect(container.text()).toContain(alertMessage); - }); - - it('renders link to help page', () => { - const helpLink = wrapper.find(GlLink); - - expect(helpLink).not.toBeNull(); - expect(helpLink.text()).toContain('More information'); - expect(helpLink.attributes().href).toEqual(helpPath); - }); -}); diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js index b9f97a3c3ae..0d11c4d56bf 100644 --- a/spec/frontend/jobs/store/actions_spec.js +++ b/spec/frontend/jobs/store/actions_spec.js @@ -111,7 +111,7 @@ describe('Job State actions', () => { }); describe('success', () => { - it('dispatches requestJob and receiveJobSuccess ', () => { + it('dispatches requestJob and receiveJobSuccess', () => { mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' }); return testAction( @@ -137,7 +137,7 @@ describe('Job State actions', () => { mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); }); - it('dispatches requestJob and receiveJobError ', () => { + it('dispatches requestJob and receiveJobError', () => { return testAction( fetchJob, null, @@ -291,7 +291,7 @@ describe('Job State actions', () => { mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500); }); - it('dispatches requestJobLog and receiveJobLogError ', () => { + it('dispatches requestJobLog and receiveJobLogError', () => { return testAction( fetchJobLog, null, @@ -355,7 +355,7 @@ describe('Job State actions', () => { window.clearTimeout = origTimeout; }); - it('should commit STOP_POLLING_JOB_LOG mutation ', async () => { + it('should commit STOP_POLLING_JOB_LOG mutation', async () => { const jobLogTimeout = 7; await testAction( @@ -370,7 +370,7 @@ describe('Job State actions', () => { }); describe('receiveJobLogSuccess', () => { - it('should commit RECEIVE_JOB_LOG_SUCCESS mutation ', () => { + it('should commit RECEIVE_JOB_LOG_SUCCESS mutation', () => { return testAction( receiveJobLogSuccess, 'hello world', @@ -388,7 +388,7 @@ describe('Job State actions', () => { }); describe('toggleCollapsibleLine', () => { - it('should commit TOGGLE_COLLAPSIBLE_LINE mutation ', () => { + it('should commit TOGGLE_COLLAPSIBLE_LINE mutation', () => { return testAction( toggleCollapsibleLine, { isClosed: true }, @@ -400,7 +400,7 @@ describe('Job State actions', () => { }); describe('requestJobsForStage', () => { - it('should commit REQUEST_JOBS_FOR_STAGE mutation ', () => { + it('should commit REQUEST_JOBS_FOR_STAGE mutation', () => { return testAction( requestJobsForStage, { name: 'deploy' }, @@ -423,7 +423,7 @@ describe('Job State actions', () => { }); describe('success', () => { - it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', () => { + it('dispatches requestJobsForStage and receiveJobsForStageSuccess', () => { mock .onGet(`${TEST_HOST}/jobs.json`) .replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] }); @@ -473,7 +473,7 @@ describe('Job State actions', () => { }); describe('receiveJobsForStageSuccess', () => { - it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', () => { + it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation', () => { return testAction( receiveJobsForStageSuccess, [{ id: 121212, name: 'karma' }], @@ -485,7 +485,7 @@ describe('Job State actions', () => { }); describe('receiveJobsForStageError', () => { - it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', () => { + it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation', () => { return testAction( receiveJobsForStageError, null, diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js index ea1ec383d6e..89cda3b0544 100644 --- a/spec/frontend/jobs/store/mutations_spec.js +++ b/spec/frontend/jobs/store/mutations_spec.js @@ -83,7 +83,7 @@ describe('Jobs Store Mutations', () => { describe('with new job log', () => { describe('log.lines', () => { describe('when append is true', () => { - it('sets the parsed log ', () => { + it('sets the parsed log', () => { mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { append: true, size: 511846, @@ -107,7 +107,7 @@ describe('Jobs Store Mutations', () => { }); describe('when it is defined', () => { - it('sets the parsed log ', () => { + it('sets the parsed log', () => { mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, { append: false, size: 511846, diff --git a/spec/frontend/labels/components/delete_label_modal_spec.js b/spec/frontend/labels/components/delete_label_modal_spec.js index 6204138f885..24a803d3f16 100644 --- a/spec/frontend/labels/components/delete_label_modal_spec.js +++ b/spec/frontend/labels/components/delete_label_modal_spec.js @@ -34,7 +34,7 @@ describe('~/labels/components/delete_label_modal', () => { wrapper.destroy(); }); - const findModal = () => wrapper.find(GlModal); + const findModal = () => wrapper.findComponent(GlModal); const findPrimaryModalButton = () => wrapper.findByTestId('delete-button'); describe('template', () => { diff --git a/spec/frontend/lib/dompurify_spec.js b/spec/frontend/lib/dompurify_spec.js index 29b927ef628..5523cc0606e 100644 --- a/spec/frontend/lib/dompurify_spec.js +++ b/spec/frontend/lib/dompurify_spec.js @@ -203,7 +203,7 @@ describe('~/lib/dompurify', () => { expect(el.getAttribute('rel')).toBe('noreferrer noopener'); }); - it('does not update `rel` values when target is not `_blank` ', () => { + it('does not update `rel` values when target is not `_blank`', () => { const html = `internal`; const el = getSanitizedNode(html); diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js index f53f809b799..7c383ae68a4 100644 --- a/spec/frontend/lib/gfm/index_spec.js +++ b/spec/frontend/lib/gfm/index_spec.js @@ -24,12 +24,6 @@ describe('gfm', () => { }; describe('render', () => { - it('processes Commonmark and provides an ast to the renderer function', async () => { - const result = await markdownToAST('This is text'); - - expect(result.type).toBe('root'); - }); - it('transforms raw HTML into individual nodes in the AST', async () => { const result = await markdownToAST('This is bold text'); @@ -46,216 +40,270 @@ describe('gfm', () => { ); }); - it('returns the result of executing the renderer function', async () => { - const rendered = { value: 'rendered tree' }; + describe('with custom renderer', () => { + it('processes Commonmark and provides an ast to the renderer function', async () => { + const result = await markdownToAST('This is text'); - const result = await render({ - markdown: 'This is bold text', - renderer: () => { - return rendered; - }, + expect(result.type).toBe('root'); }); - expect(result).toEqual(rendered); + it('returns the result of executing the renderer function', async () => { + const rendered = { value: 'rendered tree' }; + + const result = await render({ + markdown: 'This is bold text', + renderer: () => { + return rendered; + }, + }); + + expect(result).toEqual(rendered); + }); }); - describe('when skipping the rendering of footnote reference and definition nodes', () => { - it('transforms footnotes into footnotedefinition and footnotereference tags', async () => { - const result = await markdownToAST( - `footnote reference [^footnote] + describe('footnote references and footnote definitions', () => { + describe('when skipping the rendering of footnote reference and definition nodes', () => { + it('transforms footnotes into footnotedefinition and footnotereference tags', async () => { + const result = await markdownToAST( + `footnote reference [^footnote] [^footnote]: Footnote definition`, - ['footnoteReference', 'footnoteDefinition'], - ); + ['footnoteReference', 'footnoteDefinition'], + ); - expectInRoot( - result, - expect.objectContaining({ - children: expect.arrayContaining([ - expect.objectContaining({ - type: 'element', - tagName: 'footnotereference', - properties: { - identifier: 'footnote', - label: 'footnote', - }, - }), - ]), - }), - ); + expectInRoot( + result, + expect.objectContaining({ + children: expect.arrayContaining([ + expect.objectContaining({ + type: 'element', + tagName: 'footnotereference', + properties: { + identifier: 'footnote', + label: 'footnote', + }, + }), + ]), + }), + ); - expectInRoot( - result, - expect.objectContaining({ - tagName: 'footnotedefinition', - properties: { - identifier: 'footnote', - label: 'footnote', - }, - }), - ); + expectInRoot( + result, + expect.objectContaining({ + tagName: 'footnotedefinition', + properties: { + identifier: 'footnote', + label: 'footnote', + }, + }), + ); + }); }); }); - describe('when skipping the rendering of code blocks', () => { - it('transforms code nodes into codeblock html tags', async () => { - const result = await markdownToAST( - ` + describe('code blocks', () => { + describe('when skipping the rendering of code blocks', () => { + it('transforms code nodes into codeblock html tags', async () => { + const result = await markdownToAST( + ` \`\`\`javascript console.log('Hola'); \`\`\`\ `, - ['code'], - ); + ['code'], + ); - expectInRoot( - result, - expect.objectContaining({ - tagName: 'codeblock', - properties: { - language: 'javascript', - }, - }), - ); + expectInRoot( + result, + expect.objectContaining({ + tagName: 'codeblock', + properties: { + language: 'javascript', + }, + }), + ); + }); }); }); - describe('when skipping the rendering of reference definitions', () => { - it('transforms code nodes into codeblock html tags', async () => { - const result = await markdownToAST( - ` + describe('reference definitions', () => { + describe('when skipping the rendering of reference definitions', () => { + it('transforms code nodes into codeblock html tags', async () => { + const result = await markdownToAST( + ` [gitlab][gitlab] [gitlab]: https://gitlab.com "GitLab" `, - ['definition'], - ); + ['definition'], + ); - expectInRoot( - result, - expect.objectContaining({ - type: 'element', - tagName: 'referencedefinition', - properties: { - identifier: 'gitlab', - title: 'GitLab', - url: 'https://gitlab.com', - }, - children: [ - { - type: 'text', - value: '[gitlab]: https://gitlab.com "GitLab"', + expectInRoot( + result, + expect.objectContaining({ + type: 'element', + tagName: 'referencedefinition', + properties: { + identifier: 'gitlab', + title: 'GitLab', + url: 'https://gitlab.com', }, - ], - }), - ); + children: [ + { + type: 'text', + value: '[gitlab]: https://gitlab.com "GitLab"', + }, + ], + }), + ); + }); }); }); - describe('when skipping the rendering of link and image references', () => { - it('transforms linkReference and imageReference nodes into html tags', async () => { - const result = await markdownToAST( - ` + describe('link and image references', () => { + describe('when skipping the rendering of link and image references', () => { + it('transforms linkReference and imageReference nodes into html tags', async () => { + const result = await markdownToAST( + ` [gitlab][gitlab] and ![GitLab Logo][gitlab-logo] [gitlab]: https://gitlab.com "GitLab" [gitlab-logo]: https://gitlab.com/gitlab-logo.png "GitLab Logo" `, - ['linkReference', 'imageReference'], - ); + ['linkReference', 'imageReference'], + ); - expectInRoot( - result, - expect.objectContaining({ - tagName: 'p', - children: expect.arrayContaining([ - expect.objectContaining({ - type: 'element', - tagName: 'a', - properties: expect.objectContaining({ - href: 'https://gitlab.com', - isReference: 'true', - identifier: 'gitlab', - title: 'GitLab', + expectInRoot( + result, + expect.objectContaining({ + tagName: 'p', + children: expect.arrayContaining([ + expect.objectContaining({ + type: 'element', + tagName: 'a', + properties: expect.objectContaining({ + href: 'https://gitlab.com', + isReference: 'true', + identifier: 'gitlab', + title: 'GitLab', + }), }), - }), - expect.objectContaining({ - type: 'element', - tagName: 'img', - properties: expect.objectContaining({ - src: 'https://gitlab.com/gitlab-logo.png', - isReference: 'true', - identifier: 'gitlab-logo', - title: 'GitLab Logo', - alt: 'GitLab Logo', + expect.objectContaining({ + type: 'element', + tagName: 'img', + properties: expect.objectContaining({ + src: 'https://gitlab.com/gitlab-logo.png', + isReference: 'true', + identifier: 'gitlab-logo', + title: 'GitLab Logo', + alt: 'GitLab Logo', + }), }), - }), - ]), - }), - ); - }); + ]), + }), + ); + }); - it('normalizes the urls extracted from the reference definitions', async () => { - const result = await markdownToAST( - ` + it('normalizes the urls extracted from the reference definitions', async () => { + const result = await markdownToAST( + ` [gitlab][gitlab] and ![GitLab Logo][gitlab] [gitlab]: /url\\bar*baz `, - ['linkReference', 'imageReference'], - ); + ['linkReference', 'imageReference'], + ); + + expectInRoot( + result, + expect.objectContaining({ + tagName: 'p', + children: expect.arrayContaining([ + expect.objectContaining({ + type: 'element', + tagName: 'a', + properties: expect.objectContaining({ + href: '/url%5Cbar*baz', + }), + }), + expect.objectContaining({ + type: 'element', + tagName: 'img', + properties: expect.objectContaining({ + src: '/url%5Cbar*baz', + }), + }), + ]), + }), + ); + }); + }); + }); + + describe('frontmatter', () => { + describe('when skipping the rendering of frontmatter types', () => { + it.each` + type | input + ${'yaml'} | ${'---\ntitle: page\n---'} + ${'toml'} | ${'+++\ntitle: page\n+++'} + ${'json'} | ${';;;\ntitle: page\n;;;'} + `('transforms $type nodes into frontmatter html tags', async ({ input, type }) => { + const result = await markdownToAST(input, [type]); + + expectInRoot( + result, + expect.objectContaining({ + type: 'element', + tagName: 'frontmatter', + properties: { + language: type, + }, + children: [ + { + type: 'text', + value: 'title: page', + }, + ], + }), + ); + }); + }); + }); + + describe('table of contents', () => { + it.each` + markdown + ${'[[_TOC_]]'} + ${' [[_TOC_]]'} + ${'[[_TOC_]] '} + ${'[TOC]'} + ${' [TOC]'} + ${'[TOC] '} + `('parses $markdown and produces a table of contents section', async ({ markdown }) => { + const result = await markdownToAST(markdown); expectInRoot( result, expect.objectContaining({ - tagName: 'p', - children: expect.arrayContaining([ - expect.objectContaining({ - type: 'element', - tagName: 'a', - properties: expect.objectContaining({ - href: '/url%5Cbar*baz', - }), - }), - expect.objectContaining({ - type: 'element', - tagName: 'img', - properties: expect.objectContaining({ - src: '/url%5Cbar*baz', - }), - }), - ]), + type: 'element', + tagName: 'nav', }), ); }); }); - }); - describe('when skipping the rendering of frontmatter types', () => { - it.each` - type | input - ${'yaml'} | ${'---\ntitle: page\n---'} - ${'toml'} | ${'+++\ntitle: page\n+++'} - ${'json'} | ${';;;\ntitle: page\n;;;'} - `('transforms $type nodes into frontmatter html tags', async ({ input, type }) => { - const result = await markdownToAST(input, [type]); + describe('when skipping the rendering of table of contents', () => { + it('transforms table of contents nodes into html tableofcontents tags', async () => { + const result = await markdownToAST('[[_TOC_]]', ['tableOfContents']); - expectInRoot( - result, - expect.objectContaining({ - type: 'element', - tagName: 'frontmatter', - properties: { - language: type, - }, - children: [ - { - type: 'text', - value: 'title: page', - }, - ], - }), - ); + expectInRoot( + result, + expect.objectContaining({ + type: 'element', + tagName: 'tableofcontents', + }), + ); + }); }); }); }); diff --git a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js index 06573f346e0..b972f669ac4 100644 --- a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js +++ b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js @@ -84,7 +84,7 @@ describe('StartupJSLink', () => { }); }); - describe('variable match errors: ', () => { + describe('variable match errors:', () => { it('forwards requests if the variables are not matching', () => { window.gl = { startup_graphql_calls: [ diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js index a2ace8857ed..a0140d1d8a8 100644 --- a/spec/frontend/lib/utils/common_utils_spec.js +++ b/spec/frontend/lib/utils/common_utils_spec.js @@ -476,7 +476,7 @@ describe('common_utils', () => { }); }); - it('catches the rejected promise from the callback ', () => { + it('catches the rejected promise from the callback', () => { const errorMessage = 'Mistakes were made!'; return commonUtils .backOff((next, stop) => { diff --git a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js index 47bb512cbb5..59b3b4c02df 100644 --- a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js +++ b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js @@ -1,4 +1,4 @@ -import { newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility'; +import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility'; describe('newDateAsLocaleTime', () => { it.each` @@ -15,3 +15,19 @@ describe('newDateAsLocaleTime', () => { expect(newDateAsLocaleTime(string)).toEqual(expected); }); }); + +describe('getDateWithUTC', () => { + it.each` + date | expected + ${new Date('2022-03-22T01:23:45.678Z')} | ${new Date('2022-03-22T00:00:00.000Z')} + ${new Date('1999-12-31T23:59:59.999Z')} | ${new Date('1999-12-31T00:00:00.000Z')} + ${2022} | ${null} + ${[]} | ${null} + ${{}} | ${null} + ${true} | ${null} + ${null} | ${null} + ${undefined} | ${null} + `('returns $expected given $string', ({ date, expected }) => { + expect(getDateWithUTC(date)).toEqual(expected); + }); +}); diff --git a/spec/frontend/lib/utils/finite_state_machine_spec.js b/spec/frontend/lib/utils/finite_state_machine_spec.js index 441dd24c758..cfde3b8596e 100644 --- a/spec/frontend/lib/utils/finite_state_machine_spec.js +++ b/spec/frontend/lib/utils/finite_state_machine_spec.js @@ -50,13 +50,13 @@ describe('Finite State Machine', () => { }); it('throws an error if the machine definition is invalid', () => { - expect(() => machine(badDefinition)).toThrowError( + expect(() => machine(badDefinition)).toThrow( 'A state machine must have an initial state (`.initial`) and a dictionary of possible states (`.states`)', ); }); it('throws an error if the initial state is invalid', () => { - expect(() => machine(unstartableDefinition)).toThrowError( + expect(() => machine(unstartableDefinition)).toThrow( `Cannot initialize the state machine to state '${STATE_IMPOSSIBLE}'. Is that one of the machine's defined states?`, ); }); diff --git a/spec/frontend/lib/utils/is_navigating_away_spec.js b/spec/frontend/lib/utils/is_navigating_away_spec.js index e1230fe96bf..b8a01a1706c 100644 --- a/spec/frontend/lib/utils/is_navigating_away_spec.js +++ b/spec/frontend/lib/utils/is_navigating_away_spec.js @@ -6,7 +6,7 @@ describe('isNavigatingAway', () => { setNavigatingForTestsOnly(false); }); - it.each([false, true])('it returns the navigation flag with value %s', (flag) => { + it.each([false, true])('returns the navigation flag with value %s', (flag) => { setNavigatingForTestsOnly(flag); expect(isNavigatingAway()).toEqual(flag); }); diff --git a/spec/frontend/lib/utils/navigation_utility_spec.js b/spec/frontend/lib/utils/navigation_utility_spec.js index 6d3a871eb33..4dbd50223d5 100644 --- a/spec/frontend/lib/utils/navigation_utility_spec.js +++ b/spec/frontend/lib/utils/navigation_utility_spec.js @@ -63,7 +63,7 @@ describe('initPrefetchLinks', () => { expect(newLink.addEventListener).toHaveBeenCalled(); }); - it('it is not fired when less then 100ms over link', () => { + it('is not fired when less then 100ms over link', () => { const mouseOverEvent = new Event('mouseover'); const mouseOutEvent = new Event('mouseout'); diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js index 1f150599983..94a5f5385b7 100644 --- a/spec/frontend/lib/utils/poll_spec.js +++ b/spec/frontend/lib/utils/poll_spec.js @@ -128,9 +128,11 @@ describe('Poll', () => { errorCallback: callbacks.error, }); + expect(Polling.timeoutID).toBeNull(); + Polling.makeDelayedRequest(1); - expect(Polling.timeoutID).toBeTruthy(); + expect(Polling.timeoutID).not.toBeNull(); return waitForAllCallsToFinish(2, () => { Polling.stop(); diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 733d89fe08c..8d179baa505 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -586,6 +586,33 @@ describe('init markdown', () => { ); }); + it('only converts valid URLs', () => { + const notValidUrl = 'group::label'; + const expectedUrlValue = 'url'; + const expectedText = `other [${notValidUrl}](${expectedUrlValue}) text`; + const initialValue = `other ${notValidUrl} text`; + + textArea.value = initialValue; + selectedIndex = initialValue.indexOf(notValidUrl); + textArea.setSelectionRange(selectedIndex, selectedIndex + notValidUrl.length); + + insertMarkdownText({ + textArea, + text: textArea.value, + tag, + blockTag: null, + selected: notValidUrl, + wrap: false, + select, + }); + + expect(textArea.value).toEqual(expectedText); + expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedUrlValue, 1)); + expect(textArea.selectionEnd).toEqual( + expectedText.indexOf(expectedUrlValue, 1) + expectedUrlValue.length, + ); + }); + it('adds block tags on line above and below selection', () => { selected = 'this text\nis multiple\nlines'; text = `before \n${selected}\nafter `; diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 8e31fc792c5..49a160c9f23 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -45,29 +45,18 @@ describe('text_utility', () => { }); describe('slugify', () => { - it('should remove accents and convert to lower case', () => { - expect(textUtils.slugify('João')).toEqual('jo-o'); - }); - it('should replaces whitespaces with hyphens and convert to lower case', () => { - expect(textUtils.slugify('My Input String')).toEqual('my-input-string'); - }); - it('should remove trailing whitespace and replace whitespaces within string with a hyphen', () => { - expect(textUtils.slugify(' a new project ')).toEqual('a-new-project'); - }); - it('should only remove non-allowed special characters', () => { - expect(textUtils.slugify('test!_pro-ject~')).toEqual('test-_pro-ject'); - }); - it('should squash multiple hypens', () => { - expect(textUtils.slugify('test!!!!_pro-ject~')).toEqual('test-_pro-ject'); - }); - it('should return empty string if only non-allowed characters', () => { - expect(textUtils.slugify('здрасти')).toEqual(''); - }); - it('should squash multiple separators', () => { - expect(textUtils.slugify('Test:-)')).toEqual('test'); - }); - it('should trim any separators from the beginning and end of the slug', () => { - expect(textUtils.slugify('-Test:-)-')).toEqual('test'); + it.each` + title | input | output + ${'should remove accents and convert to lower case'} | ${'João'} | ${'jo-o'} + ${'should replaces whitespaces with hyphens and convert to lower case'} | ${'My Input String'} | ${'my-input-string'} + ${'should remove trailing whitespace and replace whitespaces within string with a hyphen'} | ${' a new project '} | ${'a-new-project'} + ${'should only remove non-allowed special characters'} | ${'test!_pro-ject~'} | ${'test-_pro-ject'} + ${'should squash to multiple non-allowed special characters'} | ${'test!!!!_pro-ject~'} | ${'test-_pro-ject'} + ${'should return empty string if only non-allowed characters'} | ${'дружба'} | ${''} + ${'should squash multiple separators'} | ${'Test:-)'} | ${'test'} + ${'should trim any separators from the beginning and end of the slug'} | ${'-Test:-)-'} | ${'test'} + `('$title', ({ input, output }) => { + expect(textUtils.slugify(input)).toBe(output); }); }); diff --git a/spec/frontend/lib/utils/vuex_module_mappers_spec.js b/spec/frontend/lib/utils/vuex_module_mappers_spec.js index 1821a15f677..d25a692dfea 100644 --- a/spec/frontend/lib/utils/vuex_module_mappers_spec.js +++ b/spec/frontend/lib/utils/vuex_module_mappers_spec.js @@ -128,7 +128,7 @@ describe('~/lib/utils/vuex_module_mappers', () => { describe('with non-string object value', () => { it('throws helpful error', () => { - expect(() => mapVuexModuleActions((vm) => vm.bogus, { foo: () => {} })).toThrowError( + expect(() => mapVuexModuleActions((vm) => vm.bogus, { foo: () => {} })).toThrow( REQUIRE_STRING_ERROR_MESSAGE, ); }); diff --git a/spec/frontend/locale/sprintf_spec.js b/spec/frontend/locale/sprintf_spec.js index 52e903b819f..e0d0e117ea4 100644 --- a/spec/frontend/locale/sprintf_spec.js +++ b/spec/frontend/locale/sprintf_spec.js @@ -63,12 +63,26 @@ describe('locale', () => { it('does not escape parameters for escapeParameters = false', () => { const input = 'contains %{safeContent}'; const parameters = { - safeContent: 'bold attempt', + safeContent: '15', }; const output = sprintf(input, parameters, false); - expect(output).toBe('contains bold attempt'); + expect(output).toBe('contains 15'); + }); + + describe('replaces duplicated % in input', () => { + it('removes duplicated percentage signs', () => { + const input = 'contains duplicated %{safeContent}%%'; + + const parameters = { + safeContent: '15', + }; + + const output = sprintf(input, parameters, false); + + expect(output).toBe('contains duplicated 15%'); + }); }); }); }); diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js index 9b908e5b6f0..9172876e76f 100644 --- a/spec/frontend/members/components/avatars/user_avatar_spec.js +++ b/spec/frontend/members/components/avatars/user_avatar_spec.js @@ -1,7 +1,7 @@ import { GlAvatarLink, GlBadge } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import UserAvatar from '~/members/components/avatars/user_avatar.vue'; -import { AVAILABILITY_STATUS } from '~/set_status_modal/utils'; +import { AVAILABILITY_STATUS } from '~/set_status_modal/constants'; import { member as memberMock, member2faEnabled, orphanedMember } from '../../mock_data'; diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js index 06ccd107ce3..49c4c46c3ac 100644 --- a/spec/frontend/members/mock_data.js +++ b/spec/frontend/members/mock_data.js @@ -23,6 +23,7 @@ export const member = { webUrl: 'https://gitlab.com/root', avatarUrl: 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon', blocked: false, + isBot: false, twoFactorEnabled: false, oncallSchedules: [{ name: 'schedule 1' }], escalationPolicies: [{ name: 'policy 1' }], diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js index d37e6871387..20dce639177 100644 --- a/spec/frontend/members/store/actions_spec.js +++ b/spec/frontend/members/store/actions_spec.js @@ -69,7 +69,7 @@ describe('Vuex members actions', () => { payload: { error }, }, ]), - ).rejects.toThrowError(error); + ).rejects.toThrow(error); }); }); }); @@ -122,7 +122,7 @@ describe('Vuex members actions', () => { payload: { error }, }, ]), - ).rejects.toThrowError(error); + ).rejects.toThrow(error); }); }); }); diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js index b0c9459ff4f..0271483801c 100644 --- a/spec/frontend/members/utils_spec.js +++ b/spec/frontend/members/utils_spec.js @@ -1,5 +1,12 @@ import setWindowLocation from 'helpers/set_window_location_helper'; -import { DEFAULT_SORT, MEMBER_TYPES } from '~/members/constants'; +import { + DEFAULT_SORT, + MEMBER_TYPES, + I18N_USER_YOU, + I18N_USER_BLOCKED, + I18N_USER_BOT, + I188N_USER_2FA, +} from '~/members/constants'; import { generateBadges, isGroup, @@ -52,9 +59,10 @@ describe('Members Utils', () => { it.each` member | expected - ${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }} - ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }} - ${member2faEnabled} | ${{ show: true, text: '2FA', variant: 'info' }} + ${memberMock} | ${{ show: true, text: I18N_USER_YOU, variant: 'success' }} + ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: I18N_USER_BLOCKED, variant: 'danger' }} + ${{ ...memberMock, user: { ...memberMock.user, isBot: true } }} | ${{ show: true, text: I18N_USER_BOT, variant: 'muted' }} + ${member2faEnabled} | ${{ show: true, text: I188N_USER_2FA, variant: 'info' }} `('returns expected output for "$expected.text" badge', ({ member, expected }) => { expect( generateBadges({ member, isCurrentUser: true, canManageMembers: true }), 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 4fdc4024e10..9b5641ef7b3 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 @@ -49,8 +49,8 @@ describe('Merge Conflict Resolver App', () => { extendedWrapper(w).findByTestId('interactive-button'); const findFileInlineButton = (w = wrapper) => extendedWrapper(w).findByTestId('inline-button'); const findSideBySideButton = () => wrapper.findByTestId('side-by-side'); - const findInlineConflictLines = (w = wrapper) => w.find(InlineConflictLines); - const findParallelConflictLines = (w = wrapper) => w.find(ParallelConflictLines); + const findInlineConflictLines = (w = wrapper) => w.findComponent(InlineConflictLines); + const findParallelConflictLines = (w = wrapper) => w.findComponent(ParallelConflictLines); const findCommitMessageTextarea = () => wrapper.findByTestId('commit-message'); it('shows the amount of conflicts', () => { diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js index 7cee6576b53..e73769cba51 100644 --- a/spec/frontend/merge_conflicts/store/actions_spec.js +++ b/spec/frontend/merge_conflicts/store/actions_spec.js @@ -48,7 +48,7 @@ describe('merge conflicts actions', () => { ); }); - it('when data has type equal to error ', () => { + it('when data has type equal to error', () => { mock.onGet(conflictsPath).reply(200, { type: 'error', message: 'error message' }); return testAction( actions.fetchConflictsData, @@ -63,7 +63,7 @@ describe('merge conflicts actions', () => { ); }); - it('when request fails ', () => { + it('when request fails', () => { mock.onGet(conflictsPath).reply(400); return testAction( actions.fetchConflictsData, @@ -80,7 +80,7 @@ describe('merge conflicts actions', () => { }); describe('setConflictsData', () => { - it('INTERACTIVE_RESOLVE_MODE updates the correct file ', () => { + it('INTERACTIVE_RESOLVE_MODE updates the correct file', () => { decorateFiles.mockReturnValue([{ bar: 'baz' }]); return testAction( actions.setConflictsData, @@ -239,7 +239,7 @@ describe('merge conflicts actions', () => { }); describe('setFileResolveMode', () => { - it('INTERACTIVE_RESOLVE_MODE updates the correct file ', () => { + it('INTERACTIVE_RESOLVE_MODE updates the correct file', () => { return testAction( actions.setFileResolveMode, { file: files[0], mode: INTERACTIVE_RESOLVE_MODE }, @@ -257,7 +257,7 @@ describe('merge conflicts actions', () => { ); }); - it('EDIT_RESOLVE_MODE updates the correct file ', async () => { + it('EDIT_RESOLVE_MODE updates the correct file', async () => { restoreFileLinesState.mockReturnValue([]); const file = { ...files[0], @@ -286,7 +286,7 @@ describe('merge conflicts actions', () => { }); describe('setPromptConfirmationState', () => { - it('updates the correct file ', () => { + it('updates the correct file', () => { return testAction( actions.setPromptConfirmationState, { file: files[0], promptDiscardConfirmation: true }, @@ -315,7 +315,7 @@ describe('merge conflicts actions', () => { ], }; - it('updates the correct file ', async () => { + it('updates the correct file', async () => { const marLikeMockReturn = { foo: 'bar' }; markLine.mockReturnValue(marLikeMockReturn); diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js index 2001bb5f95e..c6e90a4b20d 100644 --- a/spec/frontend/merge_request_tabs_spec.js +++ b/spec/frontend/merge_request_tabs_spec.js @@ -333,7 +333,7 @@ describe('MergeRequestTabs', () => { ${'show'} | ${false} | ${'shows'} ${'diffs'} | ${true} | ${'hides'} ${'commits'} | ${true} | ${'hides'} - `('it $hidesText expand button on $tab tab', ({ tab, hides }) => { + `('$hidesText expand button on $tab tab', ({ tab, hides }) => { window.gon = { features: { movedMrSidebar: true } }; const expandButton = document.createElement('div'); diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js index a8e3d13dca0..ce5b2a1000b 100644 --- a/spec/frontend/milestones/components/milestone_combobox_spec.js +++ b/spec/frontend/milestones/components/milestone_combobox_spec.js @@ -96,19 +96,19 @@ describe('Milestone combobox component', () => { const findNoResults = () => wrapper.find('[data-testid="milestone-combobox-no-results"]'); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findSearchBox = () => wrapper.find(GlSearchBoxByType); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); const findProjectMilestonesSection = () => wrapper.find('[data-testid="project-milestones-section"]'); const findProjectMilestonesDropdownItems = () => - findProjectMilestonesSection().findAll(GlDropdownItem); + findProjectMilestonesSection().findAllComponents(GlDropdownItem); const findFirstProjectMilestonesDropdownItem = () => findProjectMilestonesDropdownItems().at(0); const findGroupMilestonesSection = () => wrapper.find('[data-testid="group-milestones-section"]'); const findGroupMilestonesDropdownItems = () => - findGroupMilestonesSection().findAll(GlDropdownItem); + findGroupMilestonesSection().findAllComponents(GlDropdownItem); const findFirstGroupMilestonesDropdownItem = () => findGroupMilestonesDropdownItems().at(0); // diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index 14f04d9b767..263d6225a9f 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -3,7 +3,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
{ ]); }); - it('chart options should configure data zoom and axis label ', () => { + it('chart options should configure data zoom and axis label', () => { const chartOptions = findChart().props('option'); const xAxisType = findChart().props('xAxisType'); diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js index d797d9e2ad0..339c1710a9e 100644 --- a/spec/frontend/monitoring/components/dashboard_panel_spec.js +++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js @@ -430,7 +430,7 @@ describe('Dashboard Panel', () => { expect(findTimeChart().props().projectPath).toBe(mockProjectPath); }); - it('it renders a time series chart with no errors', () => { + it('renders a time series chart with no errors', () => { expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true); }); }); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 608404e5c5b..1de6b6e3e98 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -407,7 +407,7 @@ describe('Dashboard', () => { await nextTick(); }); - it('it does not show loading icons in any group', async () => { + it('does not show loading icons in any group', async () => { setupStoreWithData(store); await nextTick(); @@ -614,11 +614,11 @@ describe('Dashboard', () => { const findFirstDraggableRemoveButton = () => findDraggablePanels().at(0).find('.js-draggable-remove'); - it('it enables draggables', async () => { + it('enables draggables', async () => { findRearrangeButton().vm.$emit('click'); await nextTick(); - expect(findRearrangeButton().attributes('pressed')).toBeTruthy(); + expect(findRearrangeButton().attributes('pressed')).toBe('true'); expect(findEnabledDraggables().wrappers).toEqual(findDraggables().wrappers); }); @@ -656,13 +656,13 @@ describe('Dashboard', () => { expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount - 1); }); - it('it disables draggables when clicked again', async () => { + it('disables draggables when clicked again', async () => { findRearrangeButton().vm.$emit('click'); await nextTick(); findRearrangeButton().vm.$emit('click'); await nextTick(); - expect(findRearrangeButton().attributes('pressed')).toBeFalsy(); + expect(findRearrangeButton().attributes('pressed')).toBeUndefined(); expect(findEnabledDraggables().length).toBe(0); }); }); diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js index 721992e710a..3ccaa2d28ac 100644 --- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js +++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js @@ -163,9 +163,6 @@ describe('DashboardsDropdown', () => { findItemAt(1).vm.$emit('click'); }); - it('emits a "selectDashboard" event', () => { - expect(wrapper.emitted().selectDashboard).toBeTruthy(); - }); it('emits a "selectDashboard" event with dashboard information', () => { expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[0]]); }); diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js index 755204dc721..b54ca926dae 100644 --- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js +++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js @@ -68,7 +68,7 @@ describe('DuplicateDashboardForm', () => { await nextTick(); expect(findByRef('fileNameFormGroup').classes()).toContain('is-invalid'); - expect(findInvalidFeedback().text()).toBeTruthy(); + expect(findInvalidFeedback().text()).toBe('The file name should have a .yml extension'); }); }); diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js index 3032c236741..d83a9192876 100644 --- a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js +++ b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js @@ -72,7 +72,7 @@ describe('duplicate dashboard modal', () => { await waitForPromises(); expect(okEvent.preventDefault).toHaveBeenCalled(); - expect(wrapper.emitted().dashboardDuplicated).toBeTruthy(); + expect(wrapper.emitted('dashboardDuplicated')).toHaveLength(1); expect(wrapper.emitted().dashboardDuplicated[0]).toEqual([dashboardGitResponse[0]]); expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.vm.$refs.duplicateDashboardModal.hide).toHaveBeenCalled(); diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index d1a13fbf9cd..a872a7780eb 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -855,7 +855,7 @@ describe('Monitoring store actions', () => { ); }); - it('dispatches receiveDashboardValidationWarningsSuccess with false payload when the response is empty ', () => { + it('dispatches receiveDashboardValidationWarningsSuccess with false payload when the response is empty', () => { mockMutate.mockResolvedValue({ data: { project: null, diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js index 1d6ea99155b..745707c1d28 100644 --- a/spec/frontend/nav/components/top_nav_app_spec.js +++ b/spec/frontend/nav/components/top_nav_app_spec.js @@ -30,9 +30,10 @@ describe('~/nav/components/top_nav_app.vue', () => { it('renders nav item dropdown', () => { expect(findNavItemDropdown().attributes('href')).toBeUndefined(); expect(findNavItemDropdown().attributes()).toMatchObject({ - icon: 'hamburger', - text: TEST_NAV_DATA.activeTitle, + icon: '', + text: '', 'no-flip': '', + 'no-caret': '', }); }); 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 6cfbdb16111..048fca846ad 100644 --- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js +++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js @@ -25,7 +25,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => { }; const findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem); - const findMenuSections = () => wrapper.find(TopNavMenuSections); + const findMenuSections = () => wrapper.findComponent(TopNavMenuSections); const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]'); const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots); const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full'); diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js index a7430d8c73f..b9cf39b8c1d 100644 --- a/spec/frontend/nav/components/top_nav_menu_item_spec.js +++ b/spec/frontend/nav/components/top_nav_menu_item_spec.js @@ -26,7 +26,7 @@ describe('~/nav/components/top_nav_menu_item.vue', () => { }); }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); const findButtonIcons = () => findButton() .findAllComponents(GlIcon) 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 d56542fe572..0ed5cffd93f 100644 --- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js +++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js @@ -4,11 +4,20 @@ import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue'; const TEST_SECTIONS = [ { id: 'primary', - menuItems: [{ id: 'test', href: '/test/href' }, { id: 'foo' }, { id: 'bar' }], + menuItems: [ + { type: 'header', title: 'Heading' }, + { type: 'item', id: 'test', href: '/test/href' }, + { type: 'header', title: 'Another Heading' }, + { type: 'item', id: 'foo' }, + { type: 'item', id: 'bar' }, + ], }, { id: 'secondary', - menuItems: [{ id: 'lorem' }, { id: 'ipsum' }], + menuItems: [ + { type: 'item', id: 'lorem' }, + { type: 'item', id: 'ipsum' }, + ], }, ]; @@ -25,10 +34,20 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { }; const findMenuItemModels = (parent) => - parent.findAll('[data-testid="menu-item"]').wrappers.map((x) => ({ - menuItem: x.props('menuItem'), - classes: x.classes(), - })); + parent.findAll('[data-testid="menu-header"],[data-testid="menu-item"]').wrappers.map((x) => { + return { + menuItem: x.vm + ? { + type: 'item', + ...x.props('menuItem'), + } + : { + type: 'header', + title: x.text(), + }, + classes: x.classes(), + }; + }); const findSectionModels = () => wrapper.findAll('[data-testid="menu-section"]').wrappers.map((x) => ({ classes: x.classes(), @@ -45,32 +64,31 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { }); it('renders sections with menu items', () => { + const headerClasses = ['gl-px-4', 'gl-py-2', 'gl-text-gray-900', 'gl-display-block']; + const itemClasses = ['gl-w-full']; + expect(findSectionModels()).toEqual([ { classes: [], - menuItems: [ - { - menuItem: TEST_SECTIONS[0].menuItems[0], - classes: ['gl-w-full'], - }, - ...TEST_SECTIONS[0].menuItems.slice(1).map((menuItem) => ({ + menuItems: TEST_SECTIONS[0].menuItems.map((menuItem, index) => { + const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses]; + if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1'); + return { menuItem, - classes: ['gl-w-full', 'gl-mt-1'], - })), - ], + classes, + }; + }), }, { classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'], - menuItems: [ - { - menuItem: TEST_SECTIONS[1].menuItems[0], - classes: ['gl-w-full'], - }, - ...TEST_SECTIONS[1].menuItems.slice(1).map((menuItem) => ({ + menuItems: TEST_SECTIONS[1].menuItems.map((menuItem, index) => { + const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses]; + if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1'); + return { menuItem, - classes: ['gl-w-full', 'gl-mt-1'], - })), - ], + classes, + }; + }), }, ]); }); @@ -88,7 +106,7 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => { menuItem.vm.$emit('click'); - expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[1]]]); + expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[3]]]); }); }); diff --git a/spec/frontend/nav/mock_data.js b/spec/frontend/nav/mock_data.js index c2ad86a4605..2052acfe001 100644 --- a/spec/frontend/nav/mock_data.js +++ b/spec/frontend/nav/mock_data.js @@ -1,7 +1,7 @@ import { range } from 'lodash'; export const TEST_NAV_DATA = { - activeTitle: 'Test Active Title', + menuTitle: 'Test Menu Title', primary: [ ...['projects', 'groups'].map((view) => ({ id: view, diff --git a/spec/frontend/notebook/cells/output/latex_spec.js b/spec/frontend/notebook/cells/output/latex_spec.js index 848d2069421..ed3b63be50a 100644 --- a/spec/frontend/notebook/cells/output/latex_spec.js +++ b/spec/frontend/notebook/cells/output/latex_spec.js @@ -27,7 +27,7 @@ describe('LaTeX output cell', () => { ${1} | ${false} `('sets `Prompt.show-output` to $expectation when index is $index', ({ index, expectation }) => { const wrapper = createComponent(inlineLatex, index); - const prompt = wrapper.find(Prompt); + const prompt = wrapper.findComponent(Prompt); expect(prompt.props().count).toEqual(count); expect(prompt.props().showOutput).toEqual(expectation); diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js index 475c41a72f6..b79000a3505 100644 --- a/spec/frontend/notebook/index_spec.js +++ b/spec/frontend/notebook/index_spec.js @@ -11,7 +11,7 @@ describe('Notebook component', () => { function buildComponent(notebook) { return mount(Component, { - propsData: { notebook, codeCssClass: 'js-code-class' }, + propsData: { notebook }, provide: { relativeRawPath: '' }, }).vm; } @@ -46,10 +46,6 @@ describe('Notebook component', () => { it('renders code cell', () => { expect(vm.$el.querySelector('pre')).not.toBeNull(); }); - - it('add code class to code blocks', () => { - expect(vm.$el.querySelector('.js-code-class')).not.toBeNull(); - }); }); describe('with worksheets', () => { @@ -72,9 +68,5 @@ describe('Notebook component', () => { it('renders code cell', () => { expect(vm.$el.querySelector('pre')).not.toBeNull(); }); - - it('add code class to code blocks', () => { - expect(vm.$el.querySelector('.js-code-class')).not.toBeNull(); - }); }); }); diff --git a/spec/frontend/notebook/lib/highlight_spec.js b/spec/frontend/notebook/lib/highlight_spec.js deleted file mode 100644 index 944ccd6aa9f..00000000000 --- a/spec/frontend/notebook/lib/highlight_spec.js +++ /dev/null @@ -1,15 +0,0 @@ -import Prism from '~/notebook/lib/highlight'; - -describe('Highlight library', () => { - it('imports python language', () => { - expect(Prism.languages.python).toBeDefined(); - }); - - it('uses custom CSS classes', () => { - const el = document.createElement('div'); - el.innerHTML = Prism.highlight('console.log("a");', Prism.languages.javascript); - - expect(el.querySelector('.string')).not.toBeNull(); - expect(el.querySelector('.function')).not.toBeNull(); - }); -}); diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 463787c148b..55e4ef42e37 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -586,10 +586,10 @@ describe('issue_comment_form component', () => { ${true} ${false} `('when checkbox value is `$shouldCheckboxBeChecked`', ({ shouldCheckboxBeChecked }) => { - it(`sets \`confidential\` to \`${shouldCheckboxBeChecked}\``, async () => { + it(`sets \`internal\` to \`${shouldCheckboxBeChecked}\``, async () => { mountComponent({ mountFunction: mount, - initialData: { note: 'confidential note' }, + initialData: { note: 'internal note' }, noteableData: { ...notableDataMockCanUpdateIssuable }, }); @@ -606,7 +606,7 @@ describe('issue_comment_form component', () => { findCommentButton().trigger('click'); const [providedData] = wrapper.vm.saveNote.mock.calls[0]; - expect(providedData.data.note.confidential).toBe(shouldCheckboxBeChecked); + expect(providedData.data.note.internal).toBe(shouldCheckboxBeChecked); }); }); @@ -679,7 +679,7 @@ describe('issue_comment_form component', () => { ); }); - it('clicking `add comment now`, should call note endpoint, set `isDraft` false ', () => { + it('clicking `add comment now`, should call note endpoint, set `isDraft` false', () => { mountComponent({ mountFunction: mount, initialData: { note: 'a comment' } }); jest.spyOn(store, 'dispatch').mockResolvedValue(); diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js index a7e2f1efa09..f4ec7f835bb 100644 --- a/spec/frontend/notes/components/discussion_counter_spec.js +++ b/spec/frontend/notes/components/discussion_counter_spec.js @@ -1,5 +1,5 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlDropdownItem } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import DiscussionCounter from '~/notes/components/discussion_counter.vue'; @@ -45,7 +45,7 @@ describe('DiscussionCounter component', () => { describe('has no discussions', () => { it('does not render', () => { - wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); + wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false); }); @@ -55,7 +55,7 @@ describe('DiscussionCounter component', () => { it('does not render', () => { store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [{ ...discussionMock, resolvable: false }]); store.dispatch('updateResolvableDiscussionsCounts'); - wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); + wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(false); }); @@ -75,7 +75,7 @@ describe('DiscussionCounter component', () => { it('renders', () => { updateStore(); - wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); + wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); expect(wrapper.findComponent({ ref: 'discussionCounter' }).exists()).toBe(true); }); @@ -89,7 +89,7 @@ describe('DiscussionCounter component', () => { ({ blocksMerge, color }) => { updateStore(); store.state.unresolvedDiscussionsCount = 1; - wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge } }); + wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge } }); expect(wrapper.find('[data-testid="discussions-counter-text"]').classes()).toContain(color); }, @@ -97,60 +97,58 @@ describe('DiscussionCounter component', () => { it.each` title | resolved | groupLength - ${'not allResolved'} | ${false} | ${4} + ${'not allResolved'} | ${false} | ${2} ${'allResolved'} | ${true} | ${1} - `('renders correctly if $title', ({ resolved, groupLength }) => { + `('renders correctly if $title', async ({ resolved, groupLength }) => { updateStore({ resolvable: true, resolved }); - wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); + wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); + await wrapper.find('.dropdown-toggle').trigger('click'); - expect(wrapper.findAllComponents(GlButton)).toHaveLength(groupLength); + expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(groupLength); }); }); describe('toggle all threads button', () => { let toggleAllButton; - const updateStoreWithExpanded = (expanded) => { + const updateStoreWithExpanded = async (expanded) => { const discussion = { ...discussionMock, expanded }; store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]); store.dispatch('updateResolvableDiscussionsCounts'); - wrapper = shallowMount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); - toggleAllButton = wrapper.find('.toggle-all-discussions-btn'); + wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } }); + await wrapper.find('.dropdown-toggle').trigger('click'); + toggleAllButton = wrapper.find('[data-testid="toggle-all-discussions-btn"]'); }; afterEach(() => wrapper.destroy()); - it('calls button handler when clicked', () => { - updateStoreWithExpanded(true); + it('calls button handler when clicked', async () => { + await updateStoreWithExpanded(true); - toggleAllButton.vm.$emit('click'); + toggleAllButton.trigger('click'); expect(setExpandDiscussionsFn).toHaveBeenCalledTimes(1); }); it('collapses all discussions if expanded', async () => { - updateStoreWithExpanded(true); + await updateStoreWithExpanded(true); expect(wrapper.vm.allExpanded).toBe(true); - expect(toggleAllButton.props('icon')).toBe('collapse'); - toggleAllButton.vm.$emit('click'); + toggleAllButton.trigger('click'); await nextTick(); expect(wrapper.vm.allExpanded).toBe(false); - expect(toggleAllButton.props('icon')).toBe('expand'); }); it('expands all discussions if collapsed', async () => { - updateStoreWithExpanded(false); + await updateStoreWithExpanded(false); expect(wrapper.vm.allExpanded).toBe(false); - expect(toggleAllButton.props('icon')).toBe('expand'); - toggleAllButton.vm.$emit('click'); + toggleAllButton.trigger('click'); await nextTick(); expect(wrapper.vm.allExpanded).toBe(true); - expect(toggleAllButton.props('icon')).toBe('collapse'); }); }); }); diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js index 27206bddbfc..ed9fc47540d 100644 --- a/spec/frontend/notes/components/discussion_filter_spec.js +++ b/spec/frontend/notes/components/discussion_filter_spec.js @@ -8,7 +8,14 @@ import createEventHub from '~/helpers/event_hub_factory'; import axios from '~/lib/utils/axios_utils'; import DiscussionFilter from '~/notes/components/discussion_filter.vue'; -import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import Tracking from '~/tracking'; +import { + DISCUSSION_FILTERS_DEFAULT_VALUE, + DISCUSSION_FILTER_TYPES, + ASC, + DESC, +} from '~/notes/constants'; import notesModule from '~/notes/stores/modules'; import { discussionFiltersMock, discussionMock } from '../mock_data'; @@ -28,6 +35,8 @@ describe('DiscussionFilter component', () => { const findFilter = (filterType) => wrapper.find(`.dropdown-item[data-filter-type="${filterType}"]`); + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const mountComponent = () => { const discussions = [ { @@ -68,6 +77,7 @@ describe('DiscussionFilter component', () => { mock.onGet(DISCUSSION_PATH).reply(200, ''); window.mrTabs = undefined; wrapper = mountComponent(); + jest.spyOn(Tracking, 'event'); }); afterEach(() => { @@ -75,6 +85,65 @@ describe('DiscussionFilter component', () => { mock.restore(); }); + describe('default', () => { + beforeEach(() => { + jest.spyOn(store, 'dispatch').mockImplementation(); + }); + + it('has local storage sync with the correct props', () => { + expect(findLocalStorageSync().props('asString')).toBe(true); + }); + + it('calls setDiscussionSortDirection when update is emitted', () => { + findLocalStorageSync().vm.$emit('input', ASC); + + expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { direction: ASC }); + }); + }); + + describe('when asc', () => { + beforeEach(() => { + jest.spyOn(store, 'dispatch').mockImplementation(); + }); + + describe('when the dropdown is clicked', () => { + it('calls the right actions', () => { + wrapper.find('.js-newest-first').vm.$emit('click'); + + expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { + direction: DESC, + }); + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', { + property: DESC, + }); + }); + }); + }); + + describe('when desc', () => { + beforeEach(() => { + 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'); + + expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { + direction: ASC, + }); + expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', { + property: ASC, + }); + }); + + it('sets is-checked to true on the active button in the dropdown', () => { + expect(wrapper.find('.js-newest-first').props('isChecked')).toBe(true); + }); + }); + }); + it('renders the all filters', () => { expect(wrapper.findAll('.discussion-filter-container .dropdown-item').length).toBe( discussionFiltersMock.length, @@ -82,7 +151,7 @@ describe('DiscussionFilter component', () => { }); it('renders the default selected item', () => { - expect(wrapper.find('#discussion-filter-dropdown .dropdown-item').text().trim()).toBe( + expect(wrapper.find('.discussion-filter-container .dropdown-item').text().trim()).toBe( discussionFiltersMock[0].title, ); }); @@ -127,14 +196,6 @@ describe('DiscussionFilter component', () => { expect(wrapper.vm.$store.state.commentsDisabled).toBe(false); }); - it('renders a dropdown divider for the default filter', () => { - const defaultFilter = wrapper.findAll( - `.discussion-filter-container .dropdown-item-wrapper > *`, - ); - - expect(defaultFilter.at(1).classes('gl-new-dropdown-divider')).toBe(true); - }); - describe('Merge request tabs', () => { eventHub = createEventHub(); diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index 1b8b6bec490..a74d709ed3a 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -140,21 +140,21 @@ describe('DiscussionNotes', () => { findNoteAtIndex(0).vm.$emit('handleDeleteNote'); await nextTick(); - expect(wrapper.emitted().deleteNote).toBeTruthy(); + expect(wrapper.emitted().deleteNote).toHaveLength(1); }); it('emits startReplying when first note emits startReplying', async () => { findNoteAtIndex(0).vm.$emit('startReplying'); await nextTick(); - expect(wrapper.emitted().startReplying).toBeTruthy(); + expect(wrapper.emitted().startReplying).toHaveLength(1); }); it('emits deleteNote when second note emits handleDeleteNote', async () => { findNoteAtIndex(1).vm.$emit('handleDeleteNote'); await nextTick(); - expect(wrapper.emitted().deleteNote).toBeTruthy(); + expect(wrapper.emitted().deleteNote).toHaveLength(1); }); }); @@ -169,7 +169,7 @@ describe('DiscussionNotes', () => { note.vm.$emit('handleDeleteNote'); await nextTick(); - expect(wrapper.emitted().deleteNote).toBeTruthy(); + expect(wrapper.emitted().deleteNote).toHaveLength(1); }); }); }); 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 71406eeb7b4..a185f11ffaa 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 @@ -19,7 +19,7 @@ describe('ResolveWithIssueButton', () => { wrapper.destroy(); }); - it('it should have a link with the provided link property as href', () => { + it('should have a link with the provided link property as href', () => { const button = wrapper.findComponent(GlButton); expect(button.attributes().href).toBe(url); diff --git a/spec/frontend/notes/components/multiline_comment_form_spec.js b/spec/frontend/notes/components/multiline_comment_form_spec.js index b027a261c15..8446bba340f 100644 --- a/spec/frontend/notes/components/multiline_comment_form_spec.js +++ b/spec/frontend/notes/components/multiline_comment_form_spec.js @@ -70,7 +70,7 @@ describe('MultilineCommentForm', () => { glSelect.vm.$emit('change', { ...testLine }); expect(wrapper.vm.commentLineStart).toEqual(line); - expect(wrapper.emitted('input')).toBeTruthy(); + expect(wrapper.emitted('input')).toHaveLength(1); // Once during created, once during updateCommentLineStart expect(setSelectedCommentPosition).toHaveBeenCalledTimes(2); }); 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 new file mode 100644 index 00000000000..658e844a9b1 --- /dev/null +++ b/spec/frontend/notes/components/note_actions/timeline_event_button_spec.js @@ -0,0 +1,35 @@ +import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue'; + +const emitData = { + noteId: '1', + addError: 'Error promoting the note to timeline event: %{error}', + addGenericError: 'Something went wrong while promoting the note to timeline event.', +}; + +describe('NoteTimelineEventButton', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(TimelineEventButton, { + propsData: { + noteId: '1', + isPromotionInProgress: true, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + const findTimelineButton = () => wrapper.findComponent(GlButton); + + it('emits click-promote-comment-to-event', async () => { + findTimelineButton().vm.$emit('click'); + + expect(wrapper.emitted('click-promote-comment-to-event')).toEqual([[emitData]]); + expect(findTimelineButton().props('disabled')).toEqual(true); + }); +}); diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js index c2e56d3e7a7..3b5313744ff 100644 --- a/spec/frontend/notes/components/note_body_spec.js +++ b/spec/frontend/notes/components/note_body_spec.js @@ -74,11 +74,11 @@ describe('issue_note_body component', () => { }); it.each` - confidential | buttonText - ${false} | ${'Save comment'} - ${true} | ${'Save internal note'} - `('renders save button with text "$buttonText"', ({ confidential, buttonText }) => { - wrapper = createComponent({ props: { note: { ...note, confidential }, isEditing: true } }); + internal | buttonText + ${false} | ${'Save comment'} + ${true} | ${'Save internal note'} + `('renders save button with text "$buttonText"', ({ internal, buttonText }) => { + wrapper = createComponent({ props: { note: { ...note, internal }, isEditing: true } }); expect(wrapper.findComponent(NoteForm).props('saveButtonTitle')).toBe(buttonText); }); diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js index fad04e9063d..90473e7ccba 100644 --- a/spec/frontend/notes/components/note_form_spec.js +++ b/spec/frontend/notes/components/note_form_spec.js @@ -116,15 +116,15 @@ describe('issue_note_form component', () => { }); it.each` - confidential | placeholder - ${false} | ${'Write a comment or drag your files here…'} - ${true} | ${'Write an internal note or drag your files here…'} + internal | placeholder + ${false} | ${'Write a comment or drag your files here…'} + ${true} | ${'Write an internal note or drag your files here…'} `( - 'should set correct textarea placeholder text when discussion confidentiality is $confidential', - ({ confidential, placeholder }) => { + 'should set correct textarea placeholder text when discussion confidentiality is $internal', + ({ internal, placeholder }) => { props.note = { ...note, - confidential, + internal, }; wrapper = createComponentWrapper(); diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js index 43fbc5e26dc..76177229cff 100644 --- a/spec/frontend/notes/components/note_header_spec.js +++ b/spec/frontend/notes/components/note_header_spec.js @@ -3,7 +3,7 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import NoteHeader from '~/notes/components/note_header.vue'; -import { AVAILABILITY_STATUS } from '~/set_status_modal/utils'; +import { AVAILABILITY_STATUS } from '~/set_status_modal/constants'; import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue'; Vue.use(Vuex); @@ -40,13 +40,19 @@ describe('NoteHeader component', () => { availability: '', }; - const createComponent = (props) => { + const createComponent = (props, userAttributes = false) => { wrapper = shallowMountExtended(NoteHeader, { store: new Vuex.Store({ actions, }), propsData: { ...props }, stubs: { GlSprintf, UserNameWithStatus }, + provide: { + glFeatures: { + removeUserAttributesProjects: userAttributes, + removeUserAttributesGroups: userAttributes, + }, + }, }); }; @@ -55,6 +61,26 @@ describe('NoteHeader component', () => { wrapper = null; }); + describe('when removeUserAttributesProjects feature flag is enabled', () => { + it('does not render busy status', () => { + createComponent({ author: { ...author, availability: AVAILABILITY_STATUS.BUSY } }, true); + + expect(wrapper.find('.note-header-info').text()).not.toContain('(Busy)'); + }); + + it('does not render author status', () => { + createComponent({ author }, true); + + expect(findAuthorStatus().exists()).toBe(false); + }); + + it('does not render username', () => { + createComponent({ author }, true); + + expect(wrapper.find('.note-header-info').text()).not.toContain('@'); + }); + }); + it('does not render discussion actions when includeToggle is false', () => { createComponent({ includeToggle: false, diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js index b34305688d9..2175849aeb9 100644 --- a/spec/frontend/notes/components/noteable_discussion_spec.js +++ b/spec/frontend/notes/components/noteable_discussion_spec.js @@ -97,7 +97,7 @@ describe('noteable_discussion component', () => { `( 'reply button on form should have title "$saveButtonTitle" when note is $noteType', async ({ isNoteInternal, saveButtonTitle }) => { - wrapper.setProps({ discussion: { ...discussionMock, confidential: isNoteInternal } }); + wrapper.setProps({ discussion: { ...discussionMock, internal: isNoteInternal } }); await nextTick(); const replyPlaceholder = wrapper.findComponent(ReplyPlaceholder); diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js index e049c5bc0c8..b044d40cbe4 100644 --- a/spec/frontend/notes/components/noteable_note_spec.js +++ b/spec/frontend/notes/components/noteable_note_spec.js @@ -292,7 +292,7 @@ describe('issue_note', () => { describe('internal note', () => { it('has internal note class for internal notes', () => { - createWrapper({ note: { ...note, confidential: true } }); + createWrapper({ note: { ...note, internal: true } }); expect(wrapper.classes()).toContain('internal-note'); }); diff --git a/spec/frontend/notes/components/sort_discussion_spec.js b/spec/frontend/notes/components/sort_discussion_spec.js deleted file mode 100644 index 8b6e05da3c0..00000000000 --- a/spec/frontend/notes/components/sort_discussion_spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import Vuex from 'vuex'; -import SortDiscussion from '~/notes/components/sort_discussion.vue'; -import { ASC, DESC } from '~/notes/constants'; -import createStore from '~/notes/stores'; -import Tracking from '~/tracking'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; - -Vue.use(Vuex); - -describe('Sort Discussion component', () => { - let wrapper; - let store; - - const createComponent = () => { - jest.spyOn(store, 'dispatch').mockImplementation(); - - wrapper = shallowMount(SortDiscussion, { - store, - }); - }; - - const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - - beforeEach(() => { - store = createStore(); - jest.spyOn(Tracking, 'event'); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('has local storage sync with the correct props', () => { - expect(findLocalStorageSync().props('asString')).toBe(true); - }); - - it('calls setDiscussionSortDirection when update is emitted', () => { - findLocalStorageSync().vm.$emit('input', ASC); - - expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { direction: ASC }); - }); - }); - - describe('when asc', () => { - describe('when the dropdown is clicked', () => { - it('calls the right actions', () => { - createComponent(); - - wrapper.find('.js-newest-first').vm.$emit('click'); - - expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { - direction: DESC, - }); - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', { - property: DESC, - }); - }); - }); - - it('shows the "Oldest First" as the dropdown', () => { - createComponent(); - - expect(wrapper.find('.js-dropdown-text').props('text')).toBe('Oldest first'); - }); - }); - - describe('when desc', () => { - beforeEach(() => { - store.state.discussionSortOrder = DESC; - createComponent(); - }); - - describe('when the dropdown item is clicked', () => { - it('calls the right actions', () => { - wrapper.find('.js-oldest-first').vm.$emit('click'); - - expect(store.dispatch).toHaveBeenCalledWith('setDiscussionSortDirection', { - direction: ASC, - }); - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'change_discussion_sort_direction', { - property: ASC, - }); - }); - - it('sets is-checked to true on the active button in the dropdown', () => { - expect(wrapper.find('.js-newest-first').props('isChecked')).toBe(true); - }); - }); - - it('shows the "Newest First" as the dropdown', () => { - expect(wrapper.find('.js-dropdown-text').props('text')).toBe('Newest first'); - }); - }); -}); diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js index 35b3dec6298..1b4e8026d84 100644 --- a/spec/frontend/notes/mixins/discussion_navigation_spec.js +++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js @@ -110,16 +110,13 @@ describe('Discussion navigation mixin', () => { }); describe.each` - fn | args | currentId | expected - ${'jumpToNextDiscussion'} | ${[]} | ${null} | ${'a'} - ${'jumpToNextDiscussion'} | ${[]} | ${'a'} | ${'c'} - ${'jumpToNextDiscussion'} | ${[]} | ${'e'} | ${'a'} - ${'jumpToPreviousDiscussion'} | ${[]} | ${null} | ${'e'} - ${'jumpToPreviousDiscussion'} | ${[]} | ${'e'} | ${'c'} - ${'jumpToPreviousDiscussion'} | ${[]} | ${'c'} | ${'a'} - ${'jumpToNextRelativeDiscussion'} | ${[null]} | ${null} | ${'a'} - ${'jumpToNextRelativeDiscussion'} | ${['a']} | ${null} | ${'c'} - ${'jumpToNextRelativeDiscussion'} | ${['e']} | ${'c'} | ${'a'} + fn | args | currentId | expected + ${'jumpToNextDiscussion'} | ${[]} | ${null} | ${'a'} + ${'jumpToNextDiscussion'} | ${[]} | ${'a'} | ${'c'} + ${'jumpToNextDiscussion'} | ${[]} | ${'e'} | ${'a'} + ${'jumpToPreviousDiscussion'} | ${[]} | ${null} | ${'e'} + ${'jumpToPreviousDiscussion'} | ${[]} | ${'e'} | ${'c'} + ${'jumpToPreviousDiscussion'} | ${[]} | ${'c'} | ${'a'} `('$fn (args = $args, currentId = $currentId)', ({ fn, args, currentId, expected }) => { beforeEach(() => { store.state.notes.currentDiscussionId = currentId; @@ -133,19 +130,12 @@ describe('Discussion navigation mixin', () => { await nextTick(); }); - it('sets current discussion', () => { - expect(store.state.notes.currentDiscussionId).toEqual(expected); - }); - it('expands discussion', () => { expect(expandDiscussion).toHaveBeenCalled(); }); it('scrolls to element', () => { - expect(utils.scrollToElement).toHaveBeenCalledWith( - findDiscussion('div.discussion', expected), - { behavior: 'auto' }, - ); + expect(utils.scrollToElement).toHaveBeenCalled(); }); }); @@ -172,7 +162,7 @@ describe('Discussion navigation mixin', () => { expect(utils.scrollToElementWithContext).toHaveBeenCalledWith( findDiscussion('ul.notes', expected), - { behavior: 'auto' }, + { behavior: 'auto', offset: 0 }, ); }); }); @@ -213,7 +203,7 @@ describe('Discussion navigation mixin', () => { it('scrolls to discussion', () => { expect(utils.scrollToElement).toHaveBeenCalledWith( findDiscussion('div.discussion', expected), - { behavior: 'auto' }, + { behavior: 'auto', offset: 0 }, ); }); }); @@ -244,7 +234,6 @@ describe('Discussion navigation mixin', () => { it.each` tabValue ${'diffs'} - ${'show'} ${'other'} `( 'calls scrollToFile with setHash as $hashValue when the tab is $tabValue', diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index 02b27eca196..989dd74b6d0 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper'; import { TEST_HOST } from 'spec/test_constants'; import Api from '~/api'; import createFlash from '~/flash'; +import toast from '~/vue_shared/plugins/global_toast'; import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants'; import axios from '~/lib/utils/axios_utils'; import * as notesConstants from '~/notes/constants'; @@ -14,7 +15,9 @@ import mutations from '~/notes/stores/mutations'; import * as utils from '~/notes/stores/utils'; import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql'; import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql'; +import promoteTimelineEvent from '~/notes/graphql/promote_timeline_event.mutation.graphql'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; +import notesEventHub from '~/notes/event_hub'; import waitForPromises from 'helpers/wait_for_promises'; import { resetStore } from '../helpers'; import { @@ -38,6 +41,8 @@ jest.mock('~/flash', () => { return flash; }); +jest.mock('~/vue_shared/plugins/global_toast'); + describe('Actions Notes Store', () => { let commit; let dispatch; @@ -1324,6 +1329,102 @@ describe('Actions Notes Store', () => { }); }); + describe('promoteCommentToTimelineEvent', () => { + const actionArgs = { + noteId: '1', + addError: 'addError: Create error', + addGenericError: 'addGenericError', + }; + const commitSpy = jest.fn(); + + describe('for successful request', () => { + const timelineEventSuccessResponse = { + data: { + timelineEventPromoteFromNote: { + timelineEvent: { + id: 'gid://gitlab/IncidentManagement::TimelineEvent/19', + }, + errors: [], + }, + }, + }; + + beforeEach(() => { + jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(timelineEventSuccessResponse); + }); + + it('calls gqClient mutation with the correct values', () => { + actions.promoteCommentToTimelineEvent({ commit: () => {} }, actionArgs); + + expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1); + expect(utils.gqClient.mutate).toHaveBeenCalledWith({ + mutation: promoteTimelineEvent, + variables: { + input: { + noteId: 'gid://gitlab/Note/1', + }, + }, + }); + }); + + it('returns success response', () => { + jest.spyOn(notesEventHub, '$emit').mockImplementation(() => {}); + + return actions.promoteCommentToTimelineEvent({ commit: commitSpy }, actionArgs).then(() => { + expect(notesEventHub.$emit).toHaveBeenLastCalledWith( + 'comment-promoted-to-timeline-event', + ); + expect(toast).toHaveBeenCalledWith('Comment added to the timeline.'); + expect(commitSpy).toHaveBeenCalledWith( + mutationTypes.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS, + false, + ); + }); + }); + }); + + describe('for failing request', () => { + const errorResponse = { + data: { + timelineEventPromoteFromNote: { + timelineEvent: null, + errors: ['Create error'], + }, + }, + }; + + it.each` + mockReject | message | captureError | error + ${true} | ${'addGenericError'} | ${true} | ${new Error()} + ${false} | ${'addError: Create error'} | ${false} | ${null} + `( + 'should show an error when submission fails', + ({ mockReject, message, captureError, error }) => { + const expectedAlertArgs = { + captureError, + error, + message, + }; + if (mockReject) { + jest.spyOn(utils.gqClient, 'mutate').mockRejectedValueOnce(new Error()); + } else { + jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(errorResponse); + } + + return actions + .promoteCommentToTimelineEvent({ commit: commitSpy }, actionArgs) + .then(() => { + expect(createFlash).toHaveBeenCalledWith(expectedAlertArgs); + expect(commitSpy).toHaveBeenCalledWith( + mutationTypes.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS, + false, + ); + }); + }, + ); + }); + }); + describe('setFetchingState', () => { it('commits SET_NOTES_FETCHING_STATE', () => { return testAction( diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js index 6d078dcefcf..e03fa854e54 100644 --- a/spec/frontend/notes/stores/getters_spec.js +++ b/spec/frontend/notes/stores/getters_spec.js @@ -211,7 +211,7 @@ describe('Getters Notes Store', () => { describe('isNotesFetched', () => { it('should return the state for the fetching notes', () => { - expect(getters.isNotesFetched(state)).toBeFalsy(); + expect(getters.isNotesFetched(state)).toBe(false); }); }); @@ -512,8 +512,8 @@ describe('Getters Notes Store', () => { unresolvedDiscussionsIdsByDate: [], }; - expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(true)).toBeFalsy(); - expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeFalsy(); + expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(true)).toBeUndefined(); + expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeUndefined(); }); }); diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js index e0a0fc43ffe..8809a496c52 100644 --- a/spec/frontend/notes/stores/mutation_spec.js +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -74,7 +74,7 @@ describe('Notes Store mutations', () => { }); describe('DELETE_NOTE', () => { - it('should delete a note ', () => { + it('should delete a note', () => { const state = { discussions: [discussionMock] }; const toDelete = discussionMock.notes[0]; const lengthBefore = discussionMock.notes.length; diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js index c5d201c3aec..cd04adac72d 100644 --- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js +++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js @@ -56,8 +56,8 @@ describe('CustomNotificationsModal', () => { ); } - const findModalBodyDescription = () => wrapper.find(GlSprintf); - const findAllCheckboxes = () => wrapper.findAll(GlFormCheckbox); + const findModalBodyDescription = () => wrapper.findComponent(GlSprintf); + const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox); const findCheckboxAt = (index) => findAllCheckboxes().at(index); beforeEach(() => { @@ -111,7 +111,7 @@ describe('CustomNotificationsModal', () => { const checkbox = findCheckboxAt(index); expect(checkbox.text()).toContain(eventName); expect(checkbox.vm.$attrs.checked).toBe(enabled); - expect(checkbox.find(GlLoadingIcon).exists()).toBe(loading); + expect(checkbox.findComponent(GlLoadingIcon).exists()).toBe(loading); }, ); }); @@ -142,7 +142,7 @@ describe('CustomNotificationsModal', () => { wrapper = createComponent({ injectedProperties }); - wrapper.find(GlModal).vm.$emit('show'); + wrapper.findComponent(GlModal).vm.$emit('show'); await waitForPromises(); @@ -159,7 +159,7 @@ describe('CustomNotificationsModal', () => { wrapper = createComponent(); - wrapper.find(GlModal).vm.$emit('show'); + wrapper.findComponent(GlModal).vm.$emit('show'); expect(wrapper.vm.isLoading).toBe(true); await waitForPromises(); @@ -176,7 +176,7 @@ describe('CustomNotificationsModal', () => { mockAxios.onGet('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {}); wrapper = createComponent(); - wrapper.find(GlModal).vm.$emit('show'); + wrapper.findComponent(GlModal).vm.$emit('show'); await waitForPromises(); @@ -197,7 +197,7 @@ describe('CustomNotificationsModal', () => { ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group'} | ${'a groupId is given'} ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global'} | ${'neither projectId nor groupId are given'} `( - 'updates the $notificationType notification settings when $condition and the user clicks the checkbox ', + 'updates the $notificationType notification settings when $condition and the user clicks the checkbox', async ({ projectId, groupId, endpointUrl }) => { mockAxios .onGet(endpointUrl) diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js index 7ca6c2052ae..7a98b374095 100644 --- a/spec/frontend/notifications/components/notifications_dropdown_spec.js +++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js @@ -40,12 +40,13 @@ describe('NotificationsDropdown', () => { }); } - const findDropdown = () => wrapper.find(GlDropdown); + const findDropdown = () => wrapper.findComponent(GlDropdown); const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); - const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem); + const findAllNotificationsDropdownItems = () => + wrapper.findAllComponents(NotificationsDropdownItem); const findDropdownItemAt = (index) => - findAllNotificationsDropdownItems().at(index).find(GlDropdownItem); - const findNotificationsModal = () => wrapper.find(CustomNotificationsModal); + findAllNotificationsDropdownItems().at(index).findComponent(GlDropdownItem); + const findNotificationsModal = () => wrapper.findComponent(CustomNotificationsModal); const clickDropdownItemAt = async (index) => { const dropdownItem = findDropdownItemAt(index); @@ -243,7 +244,7 @@ describe('NotificationsDropdown', () => { expect(dropdownItem.props('isChecked')).toBe(true); }); - it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => { + it("won't update the selectedNotificationLevel and shows a toast message when the request fails and", async () => { mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {}); wrapper = createComponent(); diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js index 21145466016..810049220ae 100644 --- a/spec/frontend/operation_settings/components/metrics_settings_spec.js +++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js @@ -63,7 +63,7 @@ describe('operation settings external dashboard component', () => { describe('expand/collapse button', () => { it('renders as an expand button by default', () => { mountComponent(); - const button = wrapper.find(GlButton); + const button = wrapper.findComponent(GlButton); expect(button.text()).toBe('Expand'); }); @@ -82,7 +82,7 @@ describe('operation settings external dashboard component', () => { }); it('renders help page link', () => { - const link = subHeader.find(GlLink); + const link = subHeader.findComponent(GlLink); expect(link.text()).toBe('Learn more.'); expect(link.attributes().href).toBe(helpPage); @@ -96,7 +96,7 @@ describe('operation settings external dashboard component', () => { beforeEach(() => { mountComponent(false); - formGroup = wrapper.find(DashboardTimezone).find(GlFormGroup); + formGroup = wrapper.findComponent(DashboardTimezone).findComponent(GlFormGroup); }); it('uses label text', () => { @@ -117,7 +117,7 @@ describe('operation settings external dashboard component', () => { beforeEach(() => { mountComponent(); - select = wrapper.find(DashboardTimezone).find(GlFormSelect); + select = wrapper.findComponent(DashboardTimezone).findComponent(GlFormSelect); }); it('defaults to externalDashboardUrl', () => { @@ -132,7 +132,7 @@ describe('operation settings external dashboard component', () => { beforeEach(() => { mountComponent(false); - formGroup = wrapper.find(ExternalDashboard).find(GlFormGroup); + formGroup = wrapper.findComponent(ExternalDashboard).findComponent(GlFormGroup); }); it('uses label text', () => { @@ -153,7 +153,7 @@ describe('operation settings external dashboard component', () => { beforeEach(() => { mountComponent(); - input = wrapper.find(ExternalDashboard).find(GlFormInput); + input = wrapper.findComponent(ExternalDashboard).findComponent(GlFormInput); }); it('defaults to externalDashboardUrl', () => { @@ -167,7 +167,7 @@ describe('operation settings external dashboard component', () => { }); describe('submit button', () => { - const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton); + const findSubmitButton = () => wrapper.find('.settings-content form').findComponent(GlButton); const endpointRequest = [ operationsSettingsEndpoint, 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 ad67128502a..ff11c8843bb 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 @@ -11,8 +11,8 @@ describe('delete_button', () => { tooltipTitle: 'Bar tooltipTitle', }; - const findButton = () => wrapper.find(GlButton); - const findTooltip = () => wrapper.find(GlTooltip); + const findButton = () => wrapper.findComponent(GlButton); + const findTooltip = () => wrapper.findComponent(GlTooltip); const mountComponent = (props) => { wrapper = shallowMount(component, { 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 9680e273add..4a026f35822 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 @@ -13,8 +13,8 @@ import { describe('Delete alert', () => { let wrapper; - const findAlert = () => wrapper.find(GlAlert); - const findLink = () => wrapper.find(GlLink); + const findAlert = () => wrapper.findComponent(GlAlert); + const findLink = () => wrapper.findComponent(GlLink); const mountComponent = (propsData) => { wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData }); 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 9982286c625..b37edac83f7 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 @@ -120,7 +120,7 @@ describe('Details Header', () => { return waitForPromises(); }); - it('shows image.name ', () => { + it('shows image.name', () => { expect(findTitle().text()).toContain('foo'); }); @@ -289,7 +289,7 @@ describe('Details Header', () => { ); }); - describe('visibility and updated at ', () => { + describe('visibility and updated at', () => { it('has last updated text', async () => { mountComponent(); await waitForMetadataItems(); 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 1a27481a828..ce5ecfe4608 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 @@ -9,7 +9,7 @@ import { describe('Partial Cleanup alert', () => { let wrapper; - const findAlert = () => wrapper.find(GlAlert); + const findAlert = () => wrapper.findComponent(GlAlert); const findRunLink = () => wrapper.find('[data-testid="run-link"'); const findHelpLink = () => wrapper.find('[data-testid="help-link"'); 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 a11b102d9a6..d83a5099bcd 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 @@ -14,8 +14,8 @@ import { describe('Status Alert', () => { let wrapper; - const findLink = () => wrapper.find(GlLink); - const findAlert = () => wrapper.find(GlAlert); + const findLink = () => wrapper.findComponent(GlLink); + const findAlert = () => wrapper.findComponent(GlAlert); const findMessage = () => wrapper.find('[data-testid="message"]'); const mountComponent = (propsData) => { 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 84f01f10f21..96c670eaad2 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 @@ -32,7 +32,7 @@ describe('tags list row', () => { const findShortRevision = () => wrapper.find('[data-testid="digest"]'); const findClipboardButton = () => wrapper.findComponent(ClipboardButton); const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); - const findDetailsRows = () => wrapper.findAll(DetailsRow); + const findDetailsRows = () => wrapper.findAllComponents(DetailsRow); const findPublishedDateDetail = () => wrapper.find('[data-testid="published-date-detail"]'); const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]'); const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]'); @@ -359,7 +359,7 @@ describe('tags list row', () => { mountComponent(); await nextTick(); - expect(finderFunction().find(ClipboardButton).exists()).toBe(clipboard); + expect(finderFunction().findComponent(ClipboardButton).exists()).toBe(clipboard); }); it('is disabled when the component is disabled', async () => { 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 e5df260a260..88e79c513bc 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 @@ -5,7 +5,7 @@ import { GlSkeletonLoader } from '../../stubs'; describe('TagsLoader component', () => { let wrapper; - const findGlSkeletonLoaders = () => wrapper.findAll(GlSkeletonLoader); + const findGlSkeletonLoaders = () => wrapper.findAllComponents(GlSkeletonLoader); const mountComponent = () => { wrapper = shallowMount(component, { @@ -25,7 +25,7 @@ describe('TagsLoader component', () => { wrapper = null; }); - it('produces the correct amount of loaders ', () => { + 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 61503d0f3bf..535faebdd4e 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 @@ -16,7 +16,7 @@ describe('cleanup_status', () => { let wrapper; const findMainIcon = () => wrapper.findByTestId('main-icon'); - const findMainIconName = () => wrapper.findByTestId('main-icon').find(GlIcon); + const findMainIconName = () => wrapper.findByTestId('main-icon').findComponent(GlIcon); const findExtraInfoIcon = () => wrapper.findByTestId('extra-info'); const findPopover = () => wrapper.findComponent(GlPopover); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js deleted file mode 100644 index 7727bf167fe..00000000000 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/cli_commands_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import { GlDropdown } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue from 'vue'; -import Vuex from 'vuex'; -import QuickstartDropdown from '~/packages_and_registries/shared/components/cli_commands.vue'; -import { - QUICK_START, - LOGIN_COMMAND_LABEL, - COPY_LOGIN_TITLE, - BUILD_COMMAND_LABEL, - COPY_BUILD_TITLE, - PUSH_COMMAND_LABEL, - COPY_PUSH_TITLE, -} from '~/packages_and_registries/container_registry/explorer/constants'; -import Tracking from '~/tracking'; -import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; - -import { dockerCommands } from '../../mock_data'; - -Vue.use(Vuex); - -describe('cli_commands', () => { - let wrapper; - - const findDropdownButton = () => wrapper.find(GlDropdown); - const findCodeInstruction = () => wrapper.findAll(CodeInstruction); - - const mountComponent = () => { - wrapper = mount(QuickstartDropdown, { - propsData: { - ...dockerCommands, - }, - }); - }; - - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - mountComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('shows the correct text on the button', () => { - expect(findDropdownButton().text()).toContain(QUICK_START); - }); - - it('clicking on the dropdown emit a tracking event', () => { - findDropdownButton().vm.$emit('shown'); - expect(Tracking.event).toHaveBeenCalledWith( - undefined, - 'click_dropdown', - expect.objectContaining({ label: 'quickstart_dropdown' }), - ); - }); - - describe.each` - index | labelText | titleText | command | trackedEvent - ${0} | ${LOGIN_COMMAND_LABEL} | ${COPY_LOGIN_TITLE} | ${dockerCommands.dockerLoginCommand} | ${'click_copy_login'} - ${1} | ${BUILD_COMMAND_LABEL} | ${COPY_BUILD_TITLE} | ${dockerCommands.dockerBuildCommand} | ${'click_copy_build'} - ${2} | ${PUSH_COMMAND_LABEL} | ${COPY_PUSH_TITLE} | ${dockerCommands.dockerPushCommand} | ${'click_copy_push'} - `('code instructions at $index', ({ index, labelText, titleText, command, trackedEvent }) => { - let codeInstruction; - - beforeEach(() => { - codeInstruction = findCodeInstruction().at(index); - }); - - it('exists', () => { - expect(codeInstruction.exists()).toBe(true); - }); - - it(`has the correct props`, () => { - expect(codeInstruction.props()).toMatchObject({ - label: labelText, - instruction: command, - copyText: titleText, - trackingAction: trackedEvent, - trackingLabel: 'quickstart_dropdown', - }); - }); - }); -}); 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 d12933526bc..0b59fe2d8ce 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 @@ -233,7 +233,7 @@ describe('Image List Row', () => { it('contains a tag icon', () => { mountComponent(); - const icon = findTagsCount().find(GlIcon); + const icon = findTagsCount().findComponent(GlIcon); expect(icon.exists()).toBe(true); expect(icon.props('name')).toBe('tag'); }); 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 e0119954ed4..042b8383571 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 @@ -8,8 +8,8 @@ import { imagesListResponse, pageInfo as defaultPageInfo } from '../../mock_data describe('Image List', () => { let wrapper; - const findRow = () => wrapper.findAll(ImageListRow); - const findPagination = () => wrapper.find(GlKeysetPagination); + const findRow = () => wrapper.findAllComponents(ImageListRow); + const findPagination = () => wrapper.findComponent(GlKeysetPagination); const mountComponent = (props) => { wrapper = shallowMount(Component, { 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 a006de9f00c..e6d81d4a28f 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 @@ -17,7 +17,7 @@ jest.mock('~/lib/utils/datetime_utility', () => ({ describe('registry_header', () => { let wrapper; - const findTitleArea = () => wrapper.find(TitleArea); + const findTitleArea = () => wrapper.findComponent(TitleArea); const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]'); const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]'); const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]'); 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 1d161888a4d..ee6470a9df8 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 @@ -45,16 +45,16 @@ describe('Details Page', () => { let wrapper; let apolloProvider; - const findDeleteModal = () => wrapper.find(DeleteModal); - const findPagination = () => wrapper.find(GlKeysetPagination); - const findTagsLoader = () => wrapper.find(TagsLoader); - const findTagsList = () => wrapper.find(TagsList); - const findDeleteAlert = () => wrapper.find(DeleteAlert); - const findDetailsHeader = () => wrapper.find(DetailsHeader); - const findEmptyState = () => wrapper.find(GlEmptyState); - const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert); - const findStatusAlert = () => wrapper.find(StatusAlert); - const findDeleteImage = () => wrapper.find(DeleteImage); + const findDeleteModal = () => wrapper.findComponent(DeleteModal); + const findPagination = () => wrapper.findComponent(GlKeysetPagination); + const findTagsLoader = () => wrapper.findComponent(TagsLoader); + const findTagsList = () => wrapper.findComponent(TagsList); + const findDeleteAlert = () => wrapper.findComponent(DeleteAlert); + const findDetailsHeader = () => wrapper.findComponent(DetailsHeader); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findPartialCleanupAlert = () => wrapper.findComponent(PartialCleanupAlert); + const findStatusAlert = () => wrapper.findComponent(StatusAlert); + const findDeleteImage = () => wrapper.findComponent(DeleteImage); const routeId = 1; diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js index 5f4cb8969bc..add772d27ef 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/pages/index_spec.js @@ -4,7 +4,7 @@ import component from '~/packages_and_registries/container_registry/explorer/pag describe('List Page', () => { let wrapper; - const findRouterView = () => wrapper.find({ ref: 'router-view' }); + const findRouterView = () => wrapper.findComponent({ ref: 'router-view' }); const mountComponent = () => { wrapper = shallowMount(component, { 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 new file mode 100644 index 00000000000..a2e5cbdce8b --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_row_spec.js @@ -0,0 +1,143 @@ +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { n__ } from '~/locale'; +import ArtifactsListRow from '~/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue'; +import RealListItem from '~/vue_shared/components/registry/list_item.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { harborArtifactsList, defaultConfig } from '../../mock_data'; + +describe('Harbor artifact list row', () => { + let wrapper; + + const ListItem = { + ...RealListItem, + data() { + return { + detailsSlots: [], + isDetailsShown: true, + }; + }, + }; + + const RouterLinkStub = { + props: { + to: { + type: Object, + }, + }, + render(createElement) { + return createElement('a', {}, this.$slots.default); + }, + }; + + const findListItem = () => wrapper.findComponent(ListItem); + const findClipboardButton = () => wrapper.findAllComponents(ClipboardButton); + const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip); + const findByTestId = (testId) => wrapper.findByTestId(testId); + + const $route = { + params: { + project: defaultConfig.harborIntegrationProjectName, + image: 'test-repository', + }, + }; + + const mountComponent = ({ propsData, config = defaultConfig }) => { + wrapper = shallowMountExtended(ArtifactsListRow, { + stubs: { + GlSprintf, + ListItem, + 'router-link': RouterLinkStub, + }, + mocks: { + $route, + }, + propsData, + provide() { + return { + ...config, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('list item', () => { + beforeEach(() => { + mountComponent({ + propsData: { + artifact: harborArtifactsList[0], + }, + }); + }); + + it('exists', () => { + expect(findListItem().exists()).toBe(true); + }); + + it('has the correct artifact name', () => { + expect(findByTestId('name').text()).toBe(harborArtifactsList[0].digest); + }); + + it('has the correct tags count', () => { + const tagsCount = harborArtifactsList[0].tags.length; + expect(findByTestId('tags-count').text()).toBe(n__('%d tag', '%d tags', tagsCount)); + }); + + it('has correct digest', () => { + expect(findByTestId('digest').text()).toBe('Digest: mock_sh'); + }); + describe('time', () => { + it('has the correct push time', () => { + expect(findByTestId('time').text()).toBe('Published'); + expect(findTimeAgoTooltip().attributes()).toMatchObject({ + time: harborArtifactsList[0].pushTime, + }); + }); + }); + + describe('clipboard button', () => { + it('exists', () => { + expect(findClipboardButton()).toHaveLength(2); + }); + + it('has the correct props', () => { + expect(findClipboardButton().at(0).attributes()).toMatchObject({ + text: `docker pull demo.harbor.com/test-project/test-repository@${harborArtifactsList[0].digest}`, + title: `docker pull demo.harbor.com/test-project/test-repository@${harborArtifactsList[0].digest}`, + }); + + expect(findClipboardButton().at(1).attributes()).toMatchObject({ + text: harborArtifactsList[0].digest, + title: harborArtifactsList[0].digest, + }); + }); + }); + + describe('size', () => { + it('calculated correctly', () => { + expect(findByTestId('size').text()).toBe( + numberToHumanSize(Number(harborArtifactsList[0].size)), + ); + }); + + it('when size is missing', () => { + const artifactInfo = harborArtifactsList[0]; + artifactInfo.size = null; + + mountComponent({ + propsData: { + artifact: artifactInfo, + }, + }); + + expect(findByTestId('size').text()).toBe('0 bytes'); + }); + }); + }); +}); 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 new file mode 100644 index 00000000000..b9d6dc2679e --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/artifacts_list_spec.js @@ -0,0 +1,75 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlEmptyState } from '@gitlab/ui'; +import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue'; +import ArtifactsListRow from '~/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue'; +import { defaultConfig, harborArtifactsList } from '../../mock_data'; + +describe('Harbor artifacts list', () => { + let wrapper; + + const findTagsLoader = () => wrapper.findComponent(TagsLoader); + const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); + const findRegistryList = () => wrapper.findComponent(RegistryList); + const findArtifactsListRow = () => wrapper.findAllComponents(ArtifactsListRow); + + const mountComponent = ({ propsData, config = defaultConfig }) => { + wrapper = shallowMount(ArtifactsList, { + propsData, + stubs: { RegistryList }, + provide() { + return { + ...config, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when isLoading is true', () => { + beforeEach(() => { + mountComponent({ + propsData: { + isLoading: true, + pageInfo: {}, + filter: '', + artifacts: [], + }, + }); + }); + + it('show the loader', () => { + expect(findTagsLoader().exists()).toBe(true); + }); + + it('does not show the list', () => { + expect(findGlEmptyState().exists()).toBe(false); + expect(findRegistryList().exists()).toBe(false); + }); + }); + + describe('registry list', () => { + beforeEach(() => { + mountComponent({ + propsData: { + isLoading: false, + pageInfo: {}, + filter: '', + artifacts: harborArtifactsList, + }, + }); + }); + + it('exists', () => { + expect(findRegistryList().exists()).toBe(true); + }); + + it('one artifact row exist', () => { + expect(findArtifactsListRow()).toHaveLength(harborArtifactsList.length); + }); + }); +}); 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 new file mode 100644 index 00000000000..e8cc2b2e22d --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/details/details_header_spec.js @@ -0,0 +1,85 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { ROOT_IMAGE_TEXT } from '~/packages_and_registries/harbor_registry/constants/index'; + +describe('Harbor Details Header', () => { + let wrapper; + + const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); + const findTitle = () => findByTestId('title'); + const findArtifactsCount = () => findByTestId('artifacts-count'); + + const mountComponent = ({ propsData }) => { + wrapper = shallowMount(DetailsHeader, { + propsData, + stubs: { + TitleArea, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('artifact name', () => { + describe('missing image name', () => { + beforeEach(() => { + mountComponent({ propsData: { imagesDetail: { name: '', artifactCount: 1 } } }); + }); + + it('root image', () => { + expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT); + }); + }); + + describe('with artifact name present', () => { + beforeEach(() => { + mountComponent({ propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } } }); + }); + + it('shows artifact.name', () => { + expect(findTitle().text()).toContain('shao/flinkx'); + }); + }); + }); + + describe('metadata items', () => { + describe('artifacts count', () => { + it('displays "-- artifacts" while loading', async () => { + mountComponent({ propsData: { imagesDetail: {} } }); + await nextTick(); + + expect(findArtifactsCount().props('text')).toBe('-- artifacts'); + }); + + it('when there is more than one artifact has the correct text', async () => { + mountComponent({ propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 10 } } }); + + await nextTick(); + + expect(findArtifactsCount().props('text')).toBe('10 artifacts'); + }); + + it('when there is one artifact has the correct text', async () => { + mountComponent({ + propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } }, + }); + await nextTick(); + + expect(findArtifactsCount().props('text')).toBe('1 artifact'); + }); + + it('has the correct icon', async () => { + mountComponent({ + propsData: { imagesDetail: { name: 'shao/flinkx', artifactCount: 1 } }, + }); + await nextTick(); + + expect(findArtifactsCount().props('icon')).toBe('package'); + }); + }); + }); +}); 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 636f3eeb04a..7a6169d300c 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 @@ -7,14 +7,15 @@ import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import { HARBOR_REGISTRY_TITLE, LIST_INTRO_TEXT, + HARBOR_REGISTRY_HELP_PAGE_PATH, } from '~/packages_and_registries/harbor_registry/constants/index'; describe('harbor_list_header', () => { let wrapper; - const findTitleArea = () => wrapper.find(TitleArea); + const findTitleArea = () => wrapper.findComponent(TitleArea); const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]'); - const findImagesMetaDataItem = () => wrapper.find(MetadataItem); + const findImagesMetaDataItem = () => wrapper.findComponent(MetadataItem); const mountComponent = async (propsData, slots) => { wrapper = shallowMount(HarborListHeader, { @@ -77,10 +78,10 @@ describe('harbor_list_header', () => { describe('info messages', () => { describe('default message', () => { it('is correctly bound to title_area props', () => { - mountComponent({ helpPagePath: 'foo' }); + mountComponent(); expect(findTitleArea().props('infoMessages')).toEqual([ - { text: LIST_INTRO_TEXT, link: 'foo' }, + { text: LIST_INTRO_TEXT, link: HARBOR_REGISTRY_HELP_PAGE_PATH }, ]); }); }); 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 8560c4f78f7..b62d4e8836b 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 @@ -1,25 +1,24 @@ import { shallowMount, RouterLinkStub as RouterLink } from '@vue/test-utils'; -import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; +import { GlIcon, GlSkeletonLoader } from '@gitlab/ui'; import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import { harborListResponse } from '../../mock_data'; +import { harborImagesList } from '../../mock_data'; describe('Harbor List Row', () => { let wrapper; - const [item] = harborListResponse.repositories; + const item = harborImagesList[0]; - const findDetailsLink = () => wrapper.find(RouterLink); + const findDetailsLink = () => wrapper.findComponent(RouterLink); const findClipboardButton = () => wrapper.findComponent(ClipboardButton); - const findTagsCount = () => wrapper.find('[data-testid="tags-count"]'); + const findArtifactsCount = () => wrapper.find('[data-testid="artifacts-count"]'); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const mountComponent = (props) => { wrapper = shallowMount(HarborListRow, { stubs: { RouterLink, - GlSprintf, ListItem, }, propsData: { @@ -42,7 +41,8 @@ describe('Harbor List Row', () => { expect(findDetailsLink().props('to')).toMatchObject({ name: 'details', params: { - id: item.id, + image: 'nginx', + project: 'nginx', }, }); }); @@ -56,17 +56,17 @@ describe('Harbor List Row', () => { }); }); - describe('tags count', () => { + describe('artifacts count', () => { it('exists', () => { mountComponent(); - expect(findTagsCount().exists()).toBe(true); + expect(findArtifactsCount().exists()).toBe(true); }); - it('contains a tag icon', () => { + it('contains a package icon', () => { mountComponent(); - const icon = findTagsCount().find(GlIcon); + const icon = findArtifactsCount().findComponent(GlIcon); expect(icon.exists()).toBe(true); - expect(icon.props('name')).toBe('tag'); + expect(icon.props('name')).toBe('package'); }); describe('loading state', () => { @@ -76,23 +76,23 @@ describe('Harbor List Row', () => { expect(findSkeletonLoader().exists()).toBe(true); }); - it('hides the tags count while loading', () => { + it('hides the artifacts count while loading', () => { mountComponent({ metadataLoading: true }); - expect(findTagsCount().exists()).toBe(false); + expect(findArtifactsCount().exists()).toBe(false); }); }); - describe('tags count text', () => { - it('with one tag in the image', () => { + describe('artifacts count text', () => { + it('with one artifact in the image', () => { mountComponent({ item: { ...item, artifactCount: 1 } }); - expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag'); + expect(findArtifactsCount().text()).toMatchInterpolatedText('1 artifact'); }); - it('with more than one tag in the image', () => { + it('with more than one artifact in the image', () => { mountComponent({ item: { ...item, artifactCount: 3 } }); - expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags'); + expect(findArtifactsCount().text()).toMatchInterpolatedText('3 artifacts'); }); }); }); 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 f018eff58c9..e7e74a0da58 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 @@ -2,19 +2,19 @@ import { shallowMount } from '@vue/test-utils'; import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue'; import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; -import { harborListResponse } from '../../mock_data'; +import { harborImagesList } from '../../mock_data'; describe('Harbor List', () => { let wrapper; - const findHarborListRow = () => wrapper.findAll(HarborListRow); + const findHarborListRow = () => wrapper.findAllComponents(HarborListRow); const mountComponent = (props) => { wrapper = shallowMount(HarborList, { stubs: { RegistryList }, propsData: { - images: harborListResponse.repositories, - pageInfo: harborListResponse.pageInfo, + images: harborImagesList, + pageInfo: {}, ...props, }, }); @@ -28,7 +28,7 @@ describe('Harbor List', () => { it('contains one list element for each image', () => { mountComponent(); - expect(findHarborListRow().length).toBe(harborListResponse.repositories.length); + expect(findHarborListRow().length).toBe(harborImagesList.length); }); it('passes down the metadataLoading prop', () => { 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 new file mode 100644 index 00000000000..5e299a269e3 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_header_spec.js @@ -0,0 +1,52 @@ +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { mockArtifactDetail, MOCK_SHA_DIGEST } from '../../mock_data'; + +describe('Harbor Tags Header', () => { + let wrapper; + + const findTitle = () => wrapper.findByTestId('title'); + const findTagsCount = () => wrapper.findByTestId('tags-count'); + + const mountComponent = ({ propsData }) => { + wrapper = shallowMountExtended(TagsHeader, { + propsData, + stubs: { + TitleArea, + }, + }); + }; + + const mockPageInfo = { + page: 1, + perPage: 20, + total: 1, + totalPages: 1, + }; + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + mountComponent({ + propsData: { artifactDetail: mockArtifactDetail, pageInfo: mockPageInfo, tagsLoading: false }, + }); + }); + + describe('tags title', () => { + it('should be artifact digest', () => { + expect(findTitle().text()).toBe(`sha256:${MOCK_SHA_DIGEST}`); + }); + }); + + describe('tags count', () => { + it('would has the correct text', async () => { + await nextTick(); + + expect(findTagsCount().props('text')).toBe('1 tag'); + }); + }); +}); 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 new file mode 100644 index 00000000000..6fe3dabc603 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_row_spec.js @@ -0,0 +1,75 @@ +import { GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import TagsListRow from '~/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue'; +import { defaultConfig, harborTagsList } from '../../mock_data'; + +describe('Harbor tag list row', () => { + let wrapper; + + const findListItem = () => wrapper.find(ListItem); + const findClipboardButton = () => wrapper.find(ClipboardButton); + const findByTestId = (testId) => wrapper.findByTestId(testId); + + const $route = { + params: { + project: defaultConfig.harborIntegrationProjectName, + image: 'test-repository', + }, + }; + + const mountComponent = ({ propsData, config = defaultConfig }) => { + wrapper = shallowMountExtended(TagsListRow, { + stubs: { + ListItem, + GlSprintf, + }, + propsData, + mocks: { + $route, + }, + provide() { + return { + ...config, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('list item', () => { + beforeEach(() => { + mountComponent({ + propsData: { + tag: harborTagsList[0], + }, + }); + }); + + it('exists', () => { + expect(findListItem().exists()).toBe(true); + }); + + it('has the correct tag name', () => { + expect(findByTestId('name').text()).toBe(harborTagsList[0].name); + }); + + describe(' clipboard button', () => { + it('exists', () => { + expect(findClipboardButton().exists()).toBe(true); + }); + + it('has the correct props', () => { + const pullCommand = `docker pull demo.harbor.com/test-project/test-repository:${harborTagsList[0].name}`; + expect(findClipboardButton().attributes()).toMatchObject({ + text: pullCommand, + title: pullCommand, + }); + }); + }); + }); +}); 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 new file mode 100644 index 00000000000..6bcf6611d07 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/components/tags/tags_list_spec.js @@ -0,0 +1,66 @@ +import { shallowMount } from '@vue/test-utils'; +import TagsList from '~/packages_and_registries/harbor_registry/components/tags/tags_list.vue'; +import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; +import TagsListRow from '~/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue'; +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import { defaultConfig, harborTagsResponse } from '../../mock_data'; + +describe('Harbor Tags List', () => { + let wrapper; + + const findTagsLoader = () => wrapper.find(TagsLoader); + const findTagsListRows = () => wrapper.findAllComponents(TagsListRow); + const findRegistryList = () => wrapper.find(RegistryList); + + const mountComponent = ({ propsData, config = defaultConfig }) => { + wrapper = shallowMount(TagsList, { + propsData, + stubs: { RegistryList }, + provide() { + return { + ...config, + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when isLoading is true', () => { + beforeEach(() => { + mountComponent({ + propsData: { + isLoading: true, + pageInfo: {}, + tags: [], + }, + }); + }); + + it('show the loader', () => { + expect(findTagsLoader().exists()).toBe(true); + }); + }); + + describe('tags list', () => { + beforeEach(() => { + mountComponent({ + propsData: { + isLoading: false, + pageInfo: {}, + tags: harborTagsResponse, + }, + }); + }); + + it('should render correctly', () => { + expect(findRegistryList().exists()).toBe(true); + }); + + it('one tag row exists', () => { + expect(findTagsListRows()).toHaveLength(harborTagsResponse.length); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/mock_data.js b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js index 85399c22e79..b8989b6092e 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/harbor_registry/mock_data.js @@ -1,175 +1,114 @@ -export const harborListResponse = { - repositories: [ - { - artifactCount: 1, - creationTime: '2022-03-02T06:35:53.205Z', - id: 25, - name: 'shao/flinkx', - projectId: 21, - pullCount: 0, - updateTime: '2022-03-02T06:35:53.205Z', - location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', - }, - { - artifactCount: 1, - creationTime: '2022-03-02T06:35:53.205Z', - id: 26, - name: 'shao/flinkx1', - projectId: 21, - pullCount: 0, - updateTime: '2022-03-02T06:35:53.205Z', - location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', - }, - { - artifactCount: 1, - creationTime: '2022-03-02T06:35:53.205Z', - id: 27, - name: 'shao/flinkx2', - projectId: 21, - pullCount: 0, - updateTime: '2022-03-02T06:35:53.205Z', - location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', - }, - ], - totalCount: 3, - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, - }, +export const harborImageDetailEmptyResponse = { + data: null, }; -export const harborTagsResponse = { - tags: [ - { - digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', - name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', - revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255', - shortRevision: 'f53bde3d4', - createdAt: '2022-03-02T23:59:05+00:00', - totalSize: '6623124', - }, - { - digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', - name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', - revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e', - shortRevision: 'e1fe52d8b', - createdAt: '2022-02-10T01:09:56+00:00', - totalSize: '920760', - }, - { - digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', - name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', - revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f', - shortRevision: 'c72770c6e', - createdAt: '2021-12-22T04:48:48+00:00', - totalSize: '48609053', - }, - { - digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', - name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', - revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a', - shortRevision: '1ac2a4319', - createdAt: '2022-03-09T11:02:27+00:00', - totalSize: '35141894', - }, - { - digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', - name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', - revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c', - shortRevision: 'cf8fee086', - createdAt: '2022-01-21T11:31:43+00:00', - totalSize: '48716070', - }, - { - digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', - name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', - revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15', - shortRevision: '1a4b48198', - createdAt: '2022-01-21T11:31:51+00:00', - totalSize: '6623127', - }, - { - digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', - name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', - revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61', - shortRevision: '03e2e2777', - createdAt: '2022-03-02T23:58:20+00:00', - totalSize: '911377', - }, - { - digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', - name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', - revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012', - shortRevision: '350e78d60', - createdAt: '2022-01-19T13:49:14+00:00', - totalSize: '48710241', - }, - { - digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', - name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', - revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18', - shortRevision: '76038370b', - createdAt: '2022-01-24T12:56:22+00:00', - totalSize: '280065', - }, - { - digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07', - location: - 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', - path: - 'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', - name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', - revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f', - shortRevision: '3d4b49a7b', - createdAt: '2022-02-17T17:37:52+00:00', - totalSize: '48655767', - }, - ], - totalCount: 100, - pageInfo: { - hasNextPage: false, - hasPreviousPage: false, +export const MOCK_SHA_DIGEST = 'mock_sha_digest_value'; + +export const harborImageDetailResponse = { + artifactCount: 10, + creationTime: '2022-03-02T06:35:53.205Z', + id: 25, + name: 'shao/flinkx', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', +}; + +export const harborArtifactsResponse = [ + { + id: 1, + digest: `sha256:${MOCK_SHA_DIGEST}`, + size: 773928, + push_time: '2022-05-19T15:54:47.821Z', + tags: ['latest'], + }, +]; + +export const harborArtifactsList = [ + { + id: 1, + digest: `sha256:${MOCK_SHA_DIGEST}`, + size: 773928, + pushTime: '2022-05-19T15:54:47.821Z', + tags: ['latest'], + }, +]; + +export const harborTagsResponse = [ + { + repository_id: 4, + artifact_id: 5, + id: 4, + name: 'latest', + pull_time: '0001-01-01T00:00:00.000Z', + push_time: '2022-05-27T18:21:27.903Z', + signed: false, + immutable: false, + }, +]; + +export const harborTagsList = [ + { + repositoryId: 4, + artifactId: 5, + id: 4, + name: 'latest', + pullTime: '0001-01-01T00:00:00.000Z', + pushTime: '2022-05-27T18:21:27.903Z', + signed: false, + immutable: false, }, +]; + +export const defaultConfig = { + noContainersImage: 'noContainersImage', + repositoryUrl: 'demo.harbor.com', + harborIntegrationProjectName: 'test-project', + projectName: 'Flight', + endpoint: '/flightjs/Flight/-/harbor/repositories', + connectionError: false, + invalidPathError: false, + isGroupPage: false, + containersErrorImage: 'containersErrorImage', }; +export const defaultFullPath = 'flightjs/Flight'; + +export const harborImagesResponse = [ + { + id: 1, + name: 'nginx/nginx', + artifact_count: 1, + creation_time: '2022-05-29T10:07:16.812Z', + update_time: '2022-05-29T10:07:16.812Z', + project_id: 4, + pull_count: 0, + location: 'https://demo.goharbor.io/harbor/projects/4/repositories/nginx', + }, +]; + +export const harborImagesList = [ + { + id: 1, + name: 'nginx/nginx', + artifactCount: 1, + creationTime: '2022-05-29T10:07:16.812Z', + updateTime: '2022-05-29T10:07:16.812Z', + projectId: 4, + pullCount: 0, + location: 'https://demo.goharbor.io/harbor/projects/4/repositories/nginx', + }, +]; + export const dockerCommands = { dockerBuildCommand: 'foofoo', dockerPushCommand: 'barbar', dockerLoginCommand: 'bazbaz', }; + +export const mockArtifactDetail = { + project: 'test-project', + image: 'test-repository', + digest: `sha256:${MOCK_SHA_DIGEST}`, +}; 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 new file mode 100644 index 00000000000..8fd50bea280 --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/details_spec.js @@ -0,0 +1,162 @@ +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { GlFilteredSearchToken } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import HarborDetailsPage from '~/packages_and_registries/harbor_registry/pages/details.vue'; +import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; +import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue'; +import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; +import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + NAME_SORT_FIELD, + TOKEN_TYPE_TAG_NAME, +} from '~/packages_and_registries/harbor_registry/constants/index'; +import { harborArtifactsResponse, harborArtifactsList, defaultConfig } from '../mock_data'; + +let mockHarborArtifactsResponse; + +jest.mock('~/rest_api', () => ({ + getHarborArtifacts: () => mockHarborArtifactsResponse, +})); + +describe('Harbor Details Page', () => { + let wrapper; + + const findTagsLoader = () => wrapper.findComponent(TagsLoader); + const findArtifactsList = () => wrapper.findComponent(ArtifactsList); + const findDetailsHeader = () => wrapper.findComponent(DetailsHeader); + const findPersistedSearch = () => wrapper.findComponent(PersistedSearch); + + const waitForHarborDetailRequest = async () => { + await waitForPromises(); + await nextTick(); + }; + + const $route = { + params: { + project: 'test-project', + image: 'test-repository', + }, + }; + + const breadCrumbState = { + updateName: jest.fn(), + updateHref: jest.fn(), + }; + + const defaultHeaders = { + 'x-page': '1', + 'X-Per-Page': '20', + 'X-TOTAL': '1', + 'X-Total-Pages': '1', + }; + + const mountComponent = ({ config = defaultConfig } = {}) => { + wrapper = shallowMount(HarborDetailsPage, { + mocks: { + $route, + }, + provide() { + return { + breadCrumbState, + ...config, + }; + }, + }); + }; + + beforeEach(() => { + mockHarborArtifactsResponse = Promise.resolve({ + data: harborArtifactsResponse, + headers: defaultHeaders, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when isLoading is true', () => { + it('shows the loader', () => { + mountComponent(); + + expect(findTagsLoader().exists()).toBe(true); + }); + + it('does not show the list', () => { + mountComponent(); + + expect(findArtifactsList().exists()).toBe(false); + }); + }); + + describe('artifacts list', () => { + it('exists', async () => { + mountComponent(); + + findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] }); + await waitForHarborDetailRequest(); + + expect(findArtifactsList().exists()).toBe(true); + }); + + it('has the correct props bound', async () => { + mountComponent(); + + findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] }); + await waitForHarborDetailRequest(); + + expect(findArtifactsList().props()).toMatchObject({ + isLoading: false, + filter: '', + artifacts: harborArtifactsList, + pageInfo: { + page: 1, + perPage: 20, + total: 1, + totalPages: 1, + }, + }); + }); + }); + + describe('persisted search', () => { + it('has the correct props', () => { + mountComponent(); + + expect(findPersistedSearch().props()).toMatchObject({ + sortableFields: [NAME_SORT_FIELD], + defaultOrder: NAME_SORT_FIELD.orderBy, + defaultSort: 'asc', + tokens: [ + { + type: TOKEN_TYPE_TAG_NAME, + icon: 'tag', + title: s__('HarborRegistry|Tag'), + unique: true, + token: GlFilteredSearchToken, + operators: OPERATOR_IS_ONLY, + }, + ], + }); + }); + }); + + describe('header', () => { + it('has the correct props', async () => { + mountComponent(); + + findPersistedSearch().vm.$emit('update', { sort: 'NAME_ASC', filters: [] }); + await waitForHarborDetailRequest(); + + expect(findDetailsHeader().props()).toMatchObject({ + imagesDetail: { + name: 'test-project/test-repository', + artifactCount: 1, + }, + }); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js index 55fc8066f65..942cf9bad2c 100644 --- a/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/index_spec.js @@ -4,7 +4,7 @@ import component from '~/packages_and_registries/harbor_registry/pages/index.vue describe('List Page', () => { let wrapper; - const findRouterView = () => wrapper.find({ ref: 'router-view' }); + const findRouterView = () => wrapper.findComponent({ ref: 'router-view' }); const mountComponent = () => { wrapper = shallowMount(component, { 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 61ee36a2794..97d30e6fe99 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 @@ -5,15 +5,14 @@ import HarborListHeader from '~/packages_and_registries/harbor_registry/componen import HarborRegistryList from '~/packages_and_registries/harbor_registry/pages/list.vue'; import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; import waitForPromises from 'helpers/wait_for_promises'; -// import { harborListResponse } from '~/packages_and_registries/harbor_registry/mock_api.js'; import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue'; import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue'; import { SORT_FIELDS } from '~/packages_and_registries/harbor_registry/constants/index'; -import { harborListResponse, dockerCommands } from '../mock_data'; +import { harborImagesResponse, defaultConfig, harborImagesList } from '../mock_data'; let mockHarborListResponse; -jest.mock('~/packages_and_registries/harbor_registry/mock_api.js', () => ({ - harborListResponse: () => mockHarborListResponse, +jest.mock('~/rest_api', () => ({ + getHarborRepositoriesList: () => mockHarborListResponse, })); describe('Harbor List Page', () => { @@ -24,34 +23,43 @@ describe('Harbor List Page', () => { await nextTick(); }; - beforeEach(() => { - mockHarborListResponse = Promise.resolve(harborListResponse); - }); - const findHarborListHeader = () => wrapper.findComponent(HarborListHeader); const findPersistedSearch = () => wrapper.findComponent(PersistedSearch); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findHarborList = () => wrapper.findComponent(HarborList); const findCliCommands = () => wrapper.findComponent(CliCommands); + const defaultHeaders = { + 'x-page': '1', + 'X-Per-Page': '20', + 'X-TOTAL': '1', + 'X-Total-Pages': '1', + }; + const fireFirstSortUpdate = () => { findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] }); }; - const mountComponent = ({ config = { isGroupPage: false } } = {}) => { + const mountComponent = ({ config = defaultConfig } = {}) => { wrapper = shallowMount(HarborRegistryList, { stubs: { HarborListHeader, }, provide() { return { - config, - ...dockerCommands, + ...config, }; }, }); }; + beforeEach(() => { + mockHarborListResponse = Promise.resolve({ + data: harborImagesResponse, + headers: defaultHeaders, + }); + }); + afterEach(() => { wrapper.destroy(); }); @@ -64,7 +72,7 @@ describe('Harbor List Page', () => { expect(findHarborListHeader().exists()).toBe(true); expect(findHarborListHeader().props()).toMatchObject({ - imagesCount: 3, + imagesCount: 1, metadataLoading: false, }); }); @@ -117,6 +125,16 @@ describe('Harbor List Page', () => { await nextTick(); expect(findHarborList().exists()).toBe(true); + expect(findHarborList().props()).toMatchObject({ + images: harborImagesList, + metadataLoading: false, + pageInfo: { + page: 1, + perPage: 20, + total: 1, + totalPages: 1, + }, + }); }); }); 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 new file mode 100644 index 00000000000..7e0f05e736b --- /dev/null +++ b/spec/frontend/packages_and_registries/harbor_registry/pages/tags_spec.js @@ -0,0 +1,125 @@ +import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import HarborTagsPage from '~/packages_and_registries/harbor_registry/pages/harbor_tags.vue'; +import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue'; +import TagsList from '~/packages_and_registries/harbor_registry/components/tags/tags_list.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import { defaultConfig, harborTagsResponse, mockArtifactDetail } from '../mock_data'; + +let mockHarborTagsResponse; + +jest.mock('~/rest_api', () => ({ + getHarborTags: () => mockHarborTagsResponse, +})); + +describe('Harbor Tags page', () => { + let wrapper; + + const findTagsHeader = () => wrapper.find(TagsHeader); + const findTagsList = () => wrapper.find(TagsList); + + const waitForHarborTagsRequest = async () => { + await waitForPromises(); + await nextTick(); + }; + + const breadCrumbState = { + updateName: jest.fn(), + updateHref: jest.fn(), + }; + + const $route = { + params: mockArtifactDetail, + }; + + const defaultHeaders = { + 'x-page': '1', + 'X-Per-Page': '20', + 'X-TOTAL': '1', + 'X-Total-Pages': '1', + }; + + const mountComponent = ({ endpoint = defaultConfig.endpoint } = {}) => { + wrapper = shallowMount(HarborTagsPage, { + mocks: { + $route, + }, + provide() { + return { + breadCrumbState, + endpoint, + }; + }, + }); + }; + + beforeEach(() => { + mockHarborTagsResponse = Promise.resolve({ + data: harborTagsResponse, + headers: defaultHeaders, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('contains tags header', () => { + mountComponent(); + + expect(findTagsHeader().exists()).toBe(true); + }); + + it('contains tags list', () => { + mountComponent(); + + expect(findTagsList().exists()).toBe(true); + }); + + describe('header', () => { + it('has the correct props', async () => { + mountComponent(); + + await waitForHarborTagsRequest(); + expect(findTagsHeader().props()).toMatchObject({ + artifactDetail: mockArtifactDetail, + pageInfo: { + page: 1, + perPage: 20, + total: 1, + totalPages: 1, + }, + tagsLoading: false, + }); + }); + }); + + describe('list', () => { + it('has the correct props', async () => { + mountComponent(); + + await waitForHarborTagsRequest(); + expect(findTagsList().props()).toMatchObject({ + tags: [ + { + repositoryId: 4, + artifactId: 5, + id: 4, + name: 'latest', + pullTime: '0001-01-01T00:00:00.000Z', + pushTime: '2022-05-27T18:21:27.903Z', + signed: false, + immutable: false, + }, + ], + isLoading: false, + pageInfo: { + page: 1, + perPage: 20, + total: 1, + totalPages: 1, + }, + }); + }); + }); +}); 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 69c78e64e22..e74375b7705 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 @@ -76,8 +76,8 @@ describe('PackagesApp', () => { const packageTitle = () => wrapper.findComponent(TerraformTitle); const emptyState = () => wrapper.findComponent(GlEmptyState); const deleteButton = () => wrapper.find('.js-delete-button'); - const findDeleteModal = () => wrapper.find({ ref: 'deleteModal' }); - const findDeleteFileModal = () => wrapper.find({ ref: 'deleteFileModal' }); + const findDeleteModal = () => wrapper.findComponent({ ref: 'deleteModal' }); + const findDeleteFileModal = () => wrapper.findComponent({ ref: 'deleteFileModal' }); const versionsTab = () => wrapper.find('.js-versions-tab > a'); const packagesLoader = () => wrapper.findComponent(PackagesListLoader); const packagesVersionRows = () => wrapper.findAllComponents(PackageListRow); 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 95de2f0bb0b..b76d7c2b57b 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 @@ -17,8 +17,8 @@ describe('Package Files', () => { const findFirstRowDownloadLink = () => findFirstRow().find('[data-testid="download-link"]'); const findFirstRowCommitLink = () => findFirstRow().find('[data-testid="commit-link"]'); const findSecondRowCommitLink = () => findSecondRow().find('[data-testid="commit-link"]'); - const findFirstRowFileIcon = () => findFirstRow().find(FileIcon); - const findFirstRowCreatedAt = () => findFirstRow().find(TimeAgoTooltip); + const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon); + const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip); const findFirstActionMenu = () => findFirstRow().findComponent(GlDropdown); const findActionMenuDelete = () => findFirstActionMenu().find('[data-testid="delete-file"]'); const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton); 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 f10f05f4a0d..c6b5138639e 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 @@ -36,8 +36,8 @@ describe('Package History', () => { }); const findHistoryElement = (testId) => wrapper.find(`[data-testid="${testId}"]`); - const findElementLink = (container) => container.find(GlLink); - const findElementTimeAgo = (container) => container.find(TimeAgoTooltip); + const findElementLink = (container) => container.findComponent(GlLink); + const findElementTimeAgo = (container) => container.findComponent(TimeAgoTooltip); const findTitle = () => wrapper.find('[data-testid="title"]'); const findTimeline = () => wrapper.find('[data-testid="timeline"]'); 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 72d08d5683b..93d013bb458 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 @@ -7,8 +7,8 @@ describe('Infrastructure Title', () => { let wrapper; let store; - const findTitleArea = () => wrapper.find(TitleArea); - const findMetadataItem = () => wrapper.find(MetadataItem); + const findTitleArea = () => wrapper.findComponent(TitleArea); + const findMetadataItem = () => wrapper.findComponent(MetadataItem); const exampleProps = { helpUrl: 'http://example.gitlab.com/help' }; 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 31616e0b2f5..db1d3f3f633 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 @@ -31,9 +31,9 @@ describe('packages_list_app', () => { const GlLoadingIcon = { name: 'gl-loading-icon', template: '
loading
' }; const emptyListHelpUrl = 'helpUrl'; - const findEmptyState = () => wrapper.find(GlEmptyState); - const findListComponent = () => wrapper.find(PackageList); - const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findListComponent = () => wrapper.findComponent(PackageList); + const findInfrastructureSearch = () => wrapper.findComponent(InfrastructureSearch); const createStore = ({ filter = [], packageCount = 0 } = {}) => { store = new Vuex.Store({ @@ -151,7 +151,7 @@ describe('packages_list_app', () => { describe('empty state', () => { it('generate the correct empty list link', () => { - const link = findListComponent().find(GlLink); + const link = findListComponent().findComponent(GlLink); expect(link.attributes('href')).toBe(emptyListHelpUrl); expect(link.text()).toBe('publish and share your packages'); 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 fed82653016..fb5ee4e6884 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 @@ -20,11 +20,11 @@ describe('packages_list', () => { const EmptySlotStub = { name: 'empty-slot-stub', template: '
bar
' }; - const findPackagesListLoader = () => wrapper.find(PackagesListLoader); - const findPackageListPagination = () => wrapper.find(GlPagination); - const findPackageListDeleteModal = () => wrapper.find(GlModal); - const findEmptySlot = () => wrapper.find(EmptySlotStub); - const findPackagesListRow = () => wrapper.find(PackagesListRow); + const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader); + const findPackageListPagination = () => wrapper.findComponent(GlPagination); + const findPackageListDeleteModal = () => wrapper.findComponent(GlModal); + const findEmptySlot = () => wrapper.findComponent(EmptySlotStub); + const findPackagesListRow = () => wrapper.findComponent(PackagesListRow); const createStore = (isGroupPage, packages, isLoading) => { const state = { diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap index 67c3b8b795a..91824dee5b0 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap @@ -3,7 +3,7 @@ exports[`packages_list_row renders 1`] = `
{ let wrapper; - const findIcon = () => wrapper.find(GlIcon); + const findIcon = () => wrapper.findComponent(GlIcon); const mountComponent = () => { wrapper = shallowMount(InfrastructureIconAndName, {}); 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 d324d43258c..9449c40c7c6 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 @@ -71,7 +71,7 @@ describe('NugetInstallation', () => { }); }); - it('it has docs link', () => { + it('has docs link', () => { expect(findSetupDocsLink().attributes()).toMatchObject({ href: NUGET_HELP_PATH, target: '_blank', 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 031afa62890..5be05ddf629 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 @@ -3,7 +3,7 @@ exports[`packages_list_row renders 1`] = `
{ const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } }; const packageCannotDestroy = { ...packageData(), canDestroy: false }; - const findPackageTags = () => wrapper.find(PackageTags); - const findPackagePath = () => wrapper.find(PackagePath); + const findPackageTags = () => wrapper.findComponent(PackageTags); + const findPackagePath = () => wrapper.findComponent(PackagePath); const findDeleteDropdown = () => wrapper.findByTestId('action-delete'); - const findPackageIconAndName = () => wrapper.find(PackageIconAndName); + const findPackageIconAndName = () => wrapper.findComponent(PackageIconAndName); const findPackageLink = () => wrapper.findByTestId('details-link'); const findWarningIcon = () => wrapper.findByTestId('warning-icon'); const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos'); 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 660f00a2b31..3e3607a361c 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 @@ -190,7 +190,7 @@ describe('packages_list', () => { }); }); - describe('pagination ', () => { + describe('pagination', () => { beforeEach(() => { mountComponent({ pageInfo: { hasPreviousPage: true } }); }); 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 23e5c7330d5..b47515e15c3 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 @@ -7,8 +7,8 @@ describe('PackageTitle', () => { let wrapper; let store; - const findTitleArea = () => wrapper.find(TitleArea); - const findMetadataItem = () => wrapper.find(MetadataItem); + const findTitleArea = () => wrapper.findComponent(TitleArea); + const findMetadataItem = () => wrapper.findComponent(MetadataItem); const mountComponent = (propsData = { helpUrl: 'foo' }) => { wrapper = shallowMount(PackageTitle, { 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 d0c111bae2d..8f3c8667c47 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 @@ -6,8 +6,8 @@ import { PACKAGE_TYPES } from '~/packages_and_registries/package_registry/consta describe('packages_filter', () => { let wrapper; - const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken); - const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion); + const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); + const findFilteredSearchSuggestions = () => wrapper.findAllComponents(GlFilteredSearchSuggestion); const mountComponent = ({ attrs, listeners } = {}) => { wrapper = shallowMount(component, { @@ -24,13 +24,13 @@ describe('packages_filter', () => { wrapper = null; }); - it('it binds all of his attrs to filtered search token', () => { + it('binds all of his attrs to filtered search token', () => { mountComponent({ attrs: { foo: 'bar' } }); expect(findFilteredSearchToken().attributes('foo')).toBe('bar'); }); - it('it binds all of his events to filtered search token', () => { + it('binds all of his events to filtered search token', () => { const clickListener = jest.fn(); mountComponent({ listeners: { click: clickListener } }); 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 de78e6bb87b..83158d1cc5e 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 @@ -178,7 +178,7 @@ describe('PackagesApp', () => { ${PACKAGE_TYPE_PYPI} | ${true} ${PACKAGE_TYPE_NPM} | ${false} `( - `It is $visible that the component is visible when the package is $packageType`, + `is $visible that the component is visible when the package is $packageType`, async ({ packageType, visible }) => { createComponent({ resolver: jest.fn().mockResolvedValue( @@ -328,8 +328,8 @@ describe('PackagesApp', () => { findPackageFiles().vm.$emit('delete-files', [fileToDelete]); - expect(showDeletePackageSpy).not.toBeCalled(); - expect(showDeleteFileSpy).toBeCalled(); + expect(showDeletePackageSpy).not.toHaveBeenCalled(); + expect(showDeleteFileSpy).toHaveBeenCalled(); }); it('when its the only file opens delete package confirmation modal', async () => { @@ -357,8 +357,8 @@ describe('PackagesApp', () => { findPackageFiles().vm.$emit('delete-files', [fileToDelete]); - expect(showDeletePackageSpy).toBeCalled(); - expect(showDeleteFileSpy).not.toBeCalled(); + expect(showDeletePackageSpy).toHaveBeenCalled(); + expect(showDeleteFileSpy).not.toHaveBeenCalled(); }); it('confirming on the modal sets the loading state', async () => { @@ -443,7 +443,7 @@ describe('PackagesApp', () => { findPackageFiles().vm.$emit('delete-files', packageFiles()); - expect(showDeleteFilesSpy).toBeCalled(); + expect(showDeleteFilesSpy).toHaveBeenCalled(); }); it('confirming on the modal sets the loading state', async () => { @@ -532,7 +532,7 @@ describe('PackagesApp', () => { findPackageFiles().vm.$emit('delete-files', packageFiles()); - expect(showDeletePackageSpy).toBeCalled(); + expect(showDeletePackageSpy).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap b/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap deleted file mode 100644 index 5b56cb7f74e..00000000000 --- a/spec/frontend/packages_and_registries/settings/group/components/__snapshots__/settings_titles_spec.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`settings_titles renders properly 1`] = ` -
-
- - foo - -
- -

- bar -

- -
-`; 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 9d4c7f4737b..796d89231f4 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 @@ -169,7 +169,7 @@ describe('DependencyProxySettings', () => { toggleName | toggleFinder | localErrorMock | optimisticResponse ${'enable proxy'} | ${findEnableProxyToggle} | ${dependencyProxySettingMutationMock} | ${updateGroupDependencyProxySettingsOptimisticResponse} ${'enable ttl policies'} | ${findEnableTtlPoliciesToggle} | ${dependencyProxyUpdateTllPolicyMutationMock} | ${updateDependencyProxyImageTtlGroupPolicyOptimisticResponse} - `('$toggleName settings update ', ({ optimisticResponse, toggleFinder, localErrorMock }) => { + `('$toggleName settings update', ({ optimisticResponse, toggleFinder, localErrorMock }) => { describe('success state', () => { it('emits a success event', async () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js deleted file mode 100644 index 3eecdeb5b1f..00000000000 --- a/spec/frontend/packages_and_registries/settings/group/components/duplicates_settings_spec.js +++ /dev/null @@ -1,143 +0,0 @@ -import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import component from '~/packages_and_registries/settings/group/components/duplicates_settings.vue'; - -import { - DUPLICATES_TOGGLE_LABEL, - DUPLICATES_SETTING_EXCEPTION_TITLE, - DUPLICATES_SETTINGS_EXCEPTION_LEGEND, -} from '~/packages_and_registries/settings/group/constants'; - -describe('Duplicates Settings', () => { - let wrapper; - - const defaultProps = { - duplicatesAllowed: false, - duplicateExceptionRegex: 'foo', - modelNames: { - allowed: 'allowedModel', - exception: 'exceptionModel', - }, - }; - - const mountComponent = (propsData = defaultProps) => { - wrapper = shallowMount(component, { - propsData, - stubs: { - GlSprintf, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const findToggle = () => wrapper.findComponent(GlToggle); - - const findInputGroup = () => wrapper.findComponent(GlFormGroup); - const findInput = () => wrapper.findComponent(GlFormInput); - - it('has a toggle', () => { - mountComponent(); - - expect(findToggle().exists()).toBe(true); - expect(findToggle().props()).toMatchObject({ - label: DUPLICATES_TOGGLE_LABEL, - value: !defaultProps.duplicatesAllowed, - }); - }); - - it('toggle emits an update event', () => { - mountComponent(); - - findToggle().vm.$emit('change', false); - - expect(wrapper.emitted('update')).toStrictEqual([ - [{ [defaultProps.modelNames.allowed]: true }], - ]); - }); - - describe('when the duplicates are disabled', () => { - it('shows a form group with an input field', () => { - mountComponent(); - - expect(findInputGroup().exists()).toBe(true); - - expect(findInputGroup().attributes()).toMatchObject({ - 'label-for': 'maven-duplicated-settings-regex-input', - label: DUPLICATES_SETTING_EXCEPTION_TITLE, - description: DUPLICATES_SETTINGS_EXCEPTION_LEGEND, - }); - }); - - it('shows an input field', () => { - mountComponent(); - - expect(findInput().exists()).toBe(true); - - expect(findInput().attributes()).toMatchObject({ - id: 'maven-duplicated-settings-regex-input', - value: defaultProps.duplicateExceptionRegex, - }); - }); - - it('input change event emits an update event', () => { - mountComponent(); - - findInput().vm.$emit('change', 'bar'); - - expect(wrapper.emitted('update')).toStrictEqual([ - [{ [defaultProps.modelNames.exception]: 'bar' }], - ]); - }); - - describe('valid state', () => { - it('form group has correct props', () => { - mountComponent(); - - expect(findInputGroup().attributes()).toMatchObject({ - state: 'true', - 'invalid-feedback': '', - }); - }); - }); - - describe('invalid state', () => { - it('form group has correct props', () => { - const propsWithError = { - ...defaultProps, - duplicateExceptionRegexError: 'some error string', - }; - - mountComponent(propsWithError); - - expect(findInputGroup().attributes()).toMatchObject({ - 'invalid-feedback': propsWithError.duplicateExceptionRegexError, - }); - }); - }); - }); - - describe('when the duplicates are enabled', () => { - it('hides the form input group', () => { - mountComponent({ ...defaultProps, duplicatesAllowed: true }); - - expect(findInputGroup().exists()).toBe(false); - }); - }); - - describe('loading', () => { - beforeEach(() => { - mountComponent({ ...defaultProps, loading: true }); - }); - - it('disables the enable toggle', () => { - expect(findToggle().props('disabled')).toBe(true); - }); - - it('disables the form input', () => { - expect(findInput().attributes('disabled')).toBe('true'); - }); - }); -}); 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 new file mode 100644 index 00000000000..86f14961690 --- /dev/null +++ b/spec/frontend/packages_and_registries/settings/group/components/exceptions_input_spec.js @@ -0,0 +1,108 @@ +import { GlSprintf, GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import component from '~/packages_and_registries/settings/group/components/exceptions_input.vue'; + +import { DUPLICATES_SETTING_EXCEPTION_TITLE } from '~/packages_and_registries/settings/group/constants'; + +describe('Exceptions Input', () => { + let wrapper; + + const defaultProps = { + duplicatesAllowed: false, + duplicateExceptionRegex: 'foo', + id: 'maven-duplicated-settings-regex-input', + name: 'exceptionModel', + }; + + const mountComponent = (propsData = defaultProps) => { + wrapper = shallowMount(component, { + propsData, + stubs: { + GlSprintf, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findInputGroup = () => wrapper.findComponent(GlFormGroup); + const findInput = () => wrapper.findComponent(GlFormInput); + + it('shows a form group with an input field', () => { + mountComponent(); + + expect(findInputGroup().exists()).toBe(true); + + expect(findInputGroup().attributes()).toMatchObject({ + 'label-for': defaultProps.id, + label: DUPLICATES_SETTING_EXCEPTION_TITLE, + 'label-sr-only': '', + }); + }); + + it('shows an input field', () => { + mountComponent(); + + expect(findInput().exists()).toBe(true); + + expect(findInput().attributes()).toMatchObject({ + id: 'maven-duplicated-settings-regex-input', + value: defaultProps.duplicateExceptionRegex, + }); + }); + + it('input change event emits an update event', () => { + mountComponent(); + + findInput().vm.$emit('change', 'bar'); + + expect(wrapper.emitted('update')).toStrictEqual([[{ [defaultProps.name]: 'bar' }]]); + }); + + describe('valid state', () => { + beforeEach(() => { + mountComponent(); + }); + + it('form group has correct props', () => { + expect(findInputGroup().attributes('input-feedback')).toBeUndefined(); + }); + + it('form input has correct props', () => { + expect(findInput().attributes('state')).toBe('true'); + }); + }); + + describe('invalid state', () => { + const propsWithError = { + ...defaultProps, + duplicateExceptionRegexError: 'some error string', + }; + + beforeEach(() => { + mountComponent(propsWithError); + }); + + it('form group has correct props', () => { + expect(findInputGroup().attributes('invalid-feedback')).toBe( + propsWithError.duplicateExceptionRegexError, + ); + }); + + it('form input has correct props', () => { + expect(findInput().attributes('state')).toBeUndefined(); + }); + }); + + describe('loading', () => { + beforeEach(() => { + mountComponent({ ...defaultProps, loading: true }); + }); + + it('disables the form input', () => { + expect(findInput().attributes('disabled')).toBe('true'); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js deleted file mode 100644 index 4eafeedd55e..00000000000 --- a/spec/frontend/packages_and_registries/settings/group/components/generic_settings_spec.js +++ /dev/null @@ -1,54 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue'; -import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; - -describe('generic_settings', () => { - let wrapper; - - const mountComponent = () => { - wrapper = shallowMount(GenericSettings, { - scopedSlots: { - default: '
{{props.modelNames}}
', - }, - }); - }; - - const findSettingsTitle = () => wrapper.findComponent(SettingsTitles); - const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('title component', () => { - it('has a title component', () => { - mountComponent(); - - expect(findSettingsTitle().exists()).toBe(true); - }); - - it('passes the correct props', () => { - mountComponent(); - - expect(findSettingsTitle().props()).toMatchObject({ - title: 'Generic', - subTitle: 'Settings for Generic packages', - }); - }); - }); - - describe('default slot', () => { - it('accept a default slots', () => { - mountComponent(); - - expect(findDefaultSlot().exists()).toBe(true); - }); - - it('binds model names', () => { - mountComponent(); - - expect(findDefaultSlot().text()).toContain('genericDuplicatesAllowed'); - expect(findDefaultSlot().text()).toContain('genericDuplicateExceptionRegex'); - }); - }); -}); diff --git a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js deleted file mode 100644 index 22644b97b43..00000000000 --- a/spec/frontend/packages_and_registries/settings/group/components/maven_settings_spec.js +++ /dev/null @@ -1,54 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue'; -import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; - -describe('maven_settings', () => { - let wrapper; - - const mountComponent = () => { - wrapper = shallowMount(MavenSettings, { - scopedSlots: { - default: '
{{props.modelNames}}
', - }, - }); - }; - - const findSettingsTitle = () => wrapper.findComponent(SettingsTitles); - const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]'); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('title component', () => { - it('has a title component', () => { - mountComponent(); - - expect(findSettingsTitle().exists()).toBe(true); - }); - - it('passes the correct props', () => { - mountComponent(); - - expect(findSettingsTitle().props()).toMatchObject({ - title: 'Maven', - subTitle: 'Settings for Maven packages', - }); - }); - }); - - describe('default slot', () => { - it('accept a default slots', () => { - mountComponent(); - - expect(findDefaultSlot().exists()).toBe(true); - }); - - it('binds model names', () => { - mountComponent(); - - expect(findDefaultSlot().text()).toContain('mavenDuplicatesAllowed'); - expect(findDefaultSlot().text()).toContain('mavenDuplicateExceptionRegex'); - }); - }); -}); 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 274930ce668..13eba39ec8c 100644 --- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js +++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js @@ -1,13 +1,13 @@ import Vue, { nextTick } from 'vue'; +import { GlToggle } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue'; -import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue'; +import ExceptionsInput from '~/packages_and_registries/settings/group/components/exceptions_input.vue'; import component from '~/packages_and_registries/settings/group/components/packages_settings.vue'; -import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue'; import { + DUPLICATES_TOGGLE_LABEL, PACKAGE_SETTINGS_HEADER, PACKAGE_SETTINGS_DESCRIPTION, } from '~/packages_and_registries/settings/group/constants'; @@ -35,6 +35,7 @@ describe('Packages Settings', () => { }; const mountComponent = ({ + mountFn = shallowMountExtended, mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()), } = {}) => { Vue.use(VueApollo); @@ -43,7 +44,7 @@ describe('Packages Settings', () => { apolloProvider = createMockApollo(requestHandlers); - wrapper = shallowMountExtended(component, { + wrapper = mountFn(component, { apolloProvider, provide: defaultProvide, propsData: { @@ -51,8 +52,6 @@ describe('Packages Settings', () => { }, stubs: { SettingsBlock, - MavenSettings, - GenericSettings, }, }); }; @@ -63,11 +62,15 @@ describe('Packages Settings', () => { const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); const findDescription = () => wrapper.findByTestId('description'); - const findMavenSettings = () => wrapper.findComponent(MavenSettings); - const findMavenDuplicatedSettings = () => findMavenSettings().findComponent(DuplicatesSettings); - const findGenericSettings = () => wrapper.findComponent(GenericSettings); - const findGenericDuplicatedSettings = () => - findGenericSettings().findComponent(DuplicatesSettings); + const findMavenSettings = () => wrapper.findByTestId('maven-settings'); + const findGenericSettings = () => wrapper.findByTestId('generic-settings'); + + const findMavenDuplicatedSettingsToggle = () => findMavenSettings().findComponent(GlToggle); + const findGenericDuplicatedSettingsToggle = () => findGenericSettings().findComponent(GlToggle); + const findMavenDuplicatedSettingsExceptionsInput = () => + findMavenSettings().findComponent(ExceptionsInput); + const findGenericDuplicatedSettingsExceptionsInput = () => + findGenericSettings().findComponent(ExceptionsInput); const fillApolloCache = () => { apolloProvider.defaultClient.cache.writeQuery({ @@ -80,7 +83,7 @@ describe('Packages Settings', () => { }; const emitMavenSettingsUpdate = (override) => { - findMavenDuplicatedSettings().vm.$emit('update', { + findGenericDuplicatedSettingsExceptionsInput().vm.$emit('update', { mavenDuplicateExceptionRegex: ')', ...override, }); @@ -106,27 +109,46 @@ describe('Packages Settings', () => { describe('maven settings', () => { it('exists', () => { - mountComponent(); + mountComponent({ mountFn: mountExtended }); + + expect(findMavenSettings().find('td').text()).toBe('Maven'); + }); + + it('renders toggle', () => { + mountComponent({ mountFn: mountExtended }); - expect(findMavenSettings().exists()).toBe(true); + const { mavenDuplicatesAllowed } = packageSettings(); + + expect(findMavenDuplicatedSettingsToggle().exists()).toBe(true); + + expect(findMavenDuplicatedSettingsToggle().props()).toMatchObject({ + label: DUPLICATES_TOGGLE_LABEL, + value: mavenDuplicatesAllowed, + disabled: false, + labelPosition: 'hidden', + }); }); - it('assigns duplication allowness and exception props', async () => { - mountComponent(); + it('renders ExceptionsInput and assigns duplication allowness and exception props', () => { + mountComponent({ mountFn: mountExtended }); const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings(); - expect(findMavenDuplicatedSettings().props()).toMatchObject({ + expect(findMavenDuplicatedSettingsExceptionsInput().exists()).toBe(true); + + expect(findMavenDuplicatedSettingsExceptionsInput().props()).toMatchObject({ duplicatesAllowed: mavenDuplicatesAllowed, duplicateExceptionRegex: mavenDuplicateExceptionRegex, duplicateExceptionRegexError: '', loading: false, + name: 'mavenDuplicateExceptionRegex', + id: 'maven-duplicated-settings-regex-input', }); }); it('on update event calls the mutation', () => { const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()); - mountComponent({ mutationResolver }); + mountComponent({ mountFn: mountExtended, mutationResolver }); fillApolloCache(); @@ -140,31 +162,47 @@ describe('Packages Settings', () => { describe('generic settings', () => { it('exists', () => { - mountComponent(); + mountComponent({ mountFn: mountExtended }); - expect(findGenericSettings().exists()).toBe(true); + expect(findGenericSettings().find('td').text()).toBe('Generic'); }); - it('assigns duplication allowness and exception props', async () => { - mountComponent(); + it('renders toggle', () => { + mountComponent({ mountFn: mountExtended }); + + const { genericDuplicatesAllowed } = packageSettings(); + + expect(findGenericDuplicatedSettingsToggle().exists()).toBe(true); + expect(findGenericDuplicatedSettingsToggle().props()).toMatchObject({ + label: DUPLICATES_TOGGLE_LABEL, + value: genericDuplicatesAllowed, + disabled: false, + labelPosition: 'hidden', + }); + }); + + it('renders ExceptionsInput and assigns duplication allowness and exception props', async () => { + mountComponent({ mountFn: mountExtended }); const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings(); - expect(findGenericDuplicatedSettings().props()).toMatchObject({ + expect(findGenericDuplicatedSettingsExceptionsInput().props()).toMatchObject({ duplicatesAllowed: genericDuplicatesAllowed, duplicateExceptionRegex: genericDuplicateExceptionRegex, duplicateExceptionRegexError: '', loading: false, + name: 'genericDuplicateExceptionRegex', + id: 'generic-duplicated-settings-regex-input', }); }); it('on update event calls the mutation', async () => { const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationMock()); - mountComponent({ mutationResolver }); + mountComponent({ mountFn: mountExtended, mutationResolver }); fillApolloCache(); - findMavenDuplicatedSettings().vm.$emit('update', { + findGenericDuplicatedSettingsExceptionsInput().vm.$emit('update', { genericDuplicateExceptionRegex: ')', }); @@ -176,9 +214,11 @@ describe('Packages Settings', () => { describe('settings update', () => { describe('success state', () => { - it('emits a success event', async () => { - mountComponent(); + beforeEach(() => { + mountComponent({ mountFn: mountExtended }); + }); + it('emits a success event', async () => { fillApolloCache(); emitMavenSettingsUpdate(); @@ -189,11 +229,12 @@ describe('Packages Settings', () => { it('has an optimistic response', () => { const mavenDuplicateExceptionRegex = 'latest[main]something'; - mountComponent(); fillApolloCache(); - expect(findMavenDuplicatedSettings().props('duplicateExceptionRegex')).toBe(''); + expect( + findGenericDuplicatedSettingsExceptionsInput().props('duplicateExceptionRegex'), + ).toBe(''); emitMavenSettingsUpdate({ mavenDuplicateExceptionRegex }); @@ -209,7 +250,7 @@ describe('Packages Settings', () => { // note this is a complex test that covers all the path around errors that are shown in the form // it's one single it case, due to the expensive preparation and execution const mutationResolver = jest.fn().mockResolvedValue(groupPackageSettingsMutationErrorMock); - mountComponent({ mutationResolver }); + mountComponent({ mountFn: mountExtended, mutationResolver }); fillApolloCache(); @@ -218,9 +259,9 @@ describe('Packages Settings', () => { await waitForPromises(); // errors are bound to the component - expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe( - groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message, - ); + expect( + findMavenDuplicatedSettingsExceptionsInput().props('duplicateExceptionRegexError'), + ).toBe(groupPackageSettingsMutationErrorMock.errors[0].extensions.problems[0].message); // general error message is shown @@ -231,7 +272,9 @@ describe('Packages Settings', () => { await nextTick(); // errors are reset on mutation call - expect(findMavenDuplicatedSettings().props('duplicateExceptionRegexError')).toBe(''); + expect( + findMavenDuplicatedSettingsExceptionsInput().props('duplicateExceptionRegexError'), + ).toBe(''); }); it.each` @@ -239,7 +282,7 @@ describe('Packages Settings', () => { ${'local'} | ${jest.fn().mockResolvedValue(groupPackageSettingsMutationMock({ errors: ['foo'] }))} ${'network'} | ${jest.fn().mockRejectedValue()} `('mutation payload with $type error', async ({ mutationResolver }) => { - mountComponent({ mutationResolver }); + mountComponent({ mountFn: mountExtended, mutationResolver }); fillApolloCache(); emitMavenSettingsUpdate(); diff --git a/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js b/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js deleted file mode 100644 index fcfad4b42b8..00000000000 --- a/spec/frontend/packages_and_registries/settings/group/components/settings_titles_spec.js +++ /dev/null @@ -1,35 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; - -describe('settings_titles', () => { - let wrapper; - - const defaultProps = { - title: 'foo', - subTitle: 'bar', - }; - - const mountComponent = (propsData = defaultProps) => { - wrapper = shallowMount(SettingsTitles, { - propsData, - }); - }; - - const findSubTitle = () => wrapper.find('p'); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders properly', () => { - mountComponent(); - - expect(wrapper.element).toMatchSnapshot(); - }); - - it('does not render the subtitle paragraph when no subtitle is passed', () => { - mountComponent({ title: defaultProps.title }); - - expect(findSubTitle().exists()).toBe(false); - }); -}); 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 new file mode 100644 index 00000000000..8b60f31512b --- /dev/null +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/cleanup_image_tags_spec.js @@ -0,0 +1,164 @@ +import { GlAlert, GlSprintf, GlLink } 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 component from '~/packages_and_registries/settings/project/components/cleanup_image_tags.vue'; +import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue'; +import { + CONTAINER_CLEANUP_POLICY_TITLE, + CONTAINER_CLEANUP_POLICY_DESCRIPTION, + FETCH_SETTINGS_ERROR_MESSAGE, + UNAVAILABLE_FEATURE_INTRO_TEXT, + UNAVAILABLE_USER_FEATURE_TEXT, +} from '~/packages_and_registries/settings/project/constants'; +import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; + +import { + expirationPolicyPayload, + emptyExpirationPolicyPayload, + containerExpirationPolicyData, +} from '../mock_data'; + +describe('Cleanup image tags project settings', () => { + let wrapper; + let fakeApollo; + + const defaultProvidedValues = { + projectPath: 'path', + isAdmin: false, + adminSettingsPath: 'settingsPath', + enableHistoricEntries: false, + helpPagePath: 'helpPagePath', + showCleanupPolicyLink: false, + }; + + const findFormComponent = () => wrapper.findComponent(ContainerExpirationPolicyForm); + const findAlert = () => wrapper.findComponent(GlAlert); + const findTitle = () => wrapper.findByTestId('title'); + const findDescription = () => wrapper.findByTestId('description'); + + const mountComponent = (provide = defaultProvidedValues, config) => { + wrapper = shallowMountExtended(component, { + stubs: { + GlSprintf, + }, + provide, + ...config, + }); + }; + + const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => { + Vue.use(VueApollo); + + const requestHandlers = [[expirationPolicyQuery, resolver]]; + + fakeApollo = createMockApollo(requestHandlers); + mountComponent(provide, { + apolloProvider: fakeApollo, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('isEdited status', () => { + it.each` + description | apiResponse | workingCopy | result + ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false} + ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true} + ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false} + ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true} + ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true} + `('$description', async ({ apiResponse, workingCopy, result }) => { + mountComponentWithApollo({ + provide: { ...defaultProvidedValues, enableHistoricEntries: true }, + resolver: jest.fn().mockResolvedValue(apiResponse), + }); + await waitForPromises(); + + findFormComponent().vm.$emit('input', workingCopy); + + await waitForPromises(); + + expect(findFormComponent().props('isEdited')).toBe(result); + }); + }); + + it('renders the setting form', async () => { + mountComponentWithApollo({ + resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()), + }); + await waitForPromises(); + + expect(findFormComponent().exists()).toBe(true); + expect(findTitle().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_TITLE); + expect(findDescription().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_DESCRIPTION); + }); + + describe('the form is disabled', () => { + it('hides the form', () => { + mountComponent(); + + expect(findFormComponent().exists()).toBe(false); + }); + + it('shows an alert', () => { + mountComponent(); + + const text = findAlert().text(); + expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT); + expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT); + }); + + describe('an admin is visiting the page', () => { + it('shows the admin part of the alert message', () => { + mountComponent({ ...defaultProvidedValues, isAdmin: true }); + + const sprintf = findAlert().findComponent(GlSprintf); + expect(sprintf.text()).toBe('administration settings'); + expect(sprintf.findComponent(GlLink).attributes('href')).toBe( + defaultProvidedValues.adminSettingsPath, + ); + }); + }); + }); + + describe('fetchSettingsError', () => { + beforeEach(async () => { + mountComponentWithApollo({ + resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')), + }); + await waitForPromises(); + }); + + it('hides the form', () => { + expect(findFormComponent().exists()).toBe(false); + }); + + it('shows an alert', () => { + expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE); + }); + }); + + describe('empty API response', () => { + it.each` + enableHistoricEntries | isShown + ${true} | ${true} + ${false} | ${false} + `('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => { + mountComponentWithApollo({ + provide: { + ...defaultProvidedValues, + enableHistoricEntries, + }, + resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()), + }); + await waitForPromises(); + + expect(findFormComponent().exists()).toBe(isShown); + }); + }); +}); 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 ca44e77e694..8e08864bdb8 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 @@ -2,13 +2,11 @@ import { shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; 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 { GlCard, GlLoadingIcon } from 'jest/packages_and_registries/shared/stubs'; import component from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue'; -import { - UPDATE_SETTINGS_ERROR_MESSAGE, - UPDATE_SETTINGS_SUCCESS_MESSAGE, -} from '~/packages_and_registries/settings/project/constants'; +import { UPDATE_SETTINGS_ERROR_MESSAGE } from '~/packages_and_registries/settings/project/constants'; import updateContainerExpirationPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql'; import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; import Tracking from '~/tracking'; @@ -20,6 +18,7 @@ describe('Container Expiration Policy Settings Form', () => { const defaultProvidedValues = { projectPath: 'path', + projectSettingsPath: 'settings-path', }; const { @@ -36,7 +35,7 @@ describe('Container Expiration Policy Settings Form', () => { label: 'docker_container_retention_and_expiration_policies', }; - const findForm = () => wrapper.find({ ref: 'form-element' }); + const findForm = () => wrapper.find('form'); const findCancelButton = () => wrapper.find('[data-testid="cancel-button"'); const findSaveButton = () => wrapper.find('[data-testid="save-button"'); @@ -208,7 +207,9 @@ describe('Container Expiration Policy Settings Form', () => { }); it('validation event updates buttons disabled state', async () => { - mountComponent(); + mountComponent({ + props: { ...defaultProps, isEdited: true }, + }); expect(findSaveButton().props('disabled')).toBe(false); @@ -229,52 +230,22 @@ describe('Container Expiration Policy Settings Form', () => { }); describe('form', () => { - describe('form reset event', () => { - it('calls the appropriate function', () => { - mountComponent(); - - findForm().trigger('reset'); - - expect(wrapper.emitted('reset')).toEqual([[]]); - }); - - it('tracks the reset event', () => { - mountComponent(); - - findForm().trigger('reset'); - - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload); - }); - - it('resets the errors objects', async () => { - mountComponent({ - data: { apiErrors: { nameRegex: 'bar' }, localErrors: { nameRegexKeep: false } }, - }); - - findForm().trigger('reset'); - - await nextTick(); + describe('form submit event', () => { + useMockLocationHelper(); - expect(findKeepRegexInput().props('error')).toBe(''); - expect(findRemoveRegexInput().props('error')).toBe(''); - expect(findSaveButton().props('disabled')).toBe(false); - }); - }); - - describe('form submit event ', () => { it('save has type submit', () => { mountComponent(); expect(findSaveButton().attributes('type')).toBe('submit'); }); - it('dispatches the correct apollo mutation', () => { + it('dispatches the correct apollo mutation', async () => { const mutationResolver = jest.fn().mockResolvedValue(expirationPolicyMutationPayload()); mountComponentWithApollo({ mutationResolver, }); - findForm().trigger('submit'); + await submitForm(); expect(mutationResolver).toHaveBeenCalled(); }); @@ -286,9 +257,7 @@ describe('Container Expiration Policy Settings Form', () => { queryPayload: expirationPolicyPayload({ keepN: null, cadence: null, olderThan: null }), }); - await waitForPromises(); - - findForm().trigger('submit'); + await submitForm(); expect(mutationResolver).toHaveBeenCalledWith({ input: { @@ -303,24 +272,26 @@ describe('Container Expiration Policy Settings Form', () => { }); }); - it('tracks the submit event', () => { + it('tracks the submit event', async () => { mountComponentWithApollo({ mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), }); - findForm().trigger('submit'); + await submitForm(); expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload); }); - it('show a success toast when submit succeed', async () => { + it('redirects to package and registry project settings page when submitted successfully', async () => { mountComponentWithApollo({ mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), }); await submitForm(); - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE); + expect(window.location.href.endsWith('settings-path?showSetupSuccessAlert=true')).toBe( + true, + ); }); describe('when submit fails', () => { @@ -348,6 +319,7 @@ describe('Container Expiration Policy Settings Form', () => { await submitForm(); expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE); + expect(window.location.href).toBeUndefined(); }); it('parses the error messages', async () => { @@ -375,24 +347,24 @@ describe('Container Expiration Policy Settings Form', () => { describe('form actions', () => { describe('cancel button', () => { - it('has type reset', () => { + it('links to project package and registry settings path', () => { mountComponent(); - expect(findCancelButton().attributes('type')).toBe('reset'); + expect(findCancelButton().attributes('href')).toBe( + defaultProvidedValues.projectSettingsPath, + ); }); it.each` - isLoading | isEdited | mutationLoading - ${true} | ${true} | ${true} - ${false} | ${true} | ${true} - ${false} | ${false} | ${true} - ${true} | ${false} | ${false} - ${false} | ${false} | ${false} + isLoading | mutationLoading + ${true} | ${true} + ${false} | ${true} + ${true} | ${false} `( - 'when isLoading is $isLoading, isEdited is $isEdited and mutationLoading is $mutationLoading is disabled', - ({ isEdited, isLoading, mutationLoading }) => { + 'is disabled when isLoading is $isLoading and mutationLoading is $mutationLoading', + ({ isLoading, mutationLoading }) => { mountComponent({ - props: { ...defaultProps, isEdited, isLoading }, + props: { ...defaultProps, isLoading }, data: { mutationLoading }, }); @@ -409,18 +381,19 @@ describe('Container Expiration Policy Settings Form', () => { }); it.each` - isLoading | localErrors | mutationLoading - ${true} | ${{}} | ${true} - ${true} | ${{}} | ${false} - ${false} | ${{}} | ${true} - ${false} | ${{ foo: false }} | ${true} - ${true} | ${{ foo: false }} | ${false} - ${false} | ${{ foo: false }} | ${false} + isLoading | isEdited | localErrors | mutationLoading + ${true} | ${false} | ${{}} | ${true} + ${true} | ${false} | ${{}} | ${false} + ${false} | ${false} | ${{}} | ${true} + ${false} | ${false} | ${{}} | ${false} + ${false} | ${false} | ${{ foo: false }} | ${true} + ${true} | ${false} | ${{ foo: false }} | ${false} + ${false} | ${false} | ${{ foo: false }} | ${false} `( - 'when isLoading is $isLoading, localErrors is $localErrors and mutationLoading is $mutationLoading is disabled', - ({ localErrors, isLoading, mutationLoading }) => { + 'is disabled when isLoading is $isLoading, isEdited is $isEdited, localErrors is $localErrors and mutationLoading is $mutationLoading', + ({ localErrors, isEdited, isLoading, mutationLoading }) => { mountComponent({ - props: { ...defaultProps, isLoading }, + props: { ...defaultProps, isEdited, isLoading }, data: { mutationLoading, localErrors }, }); 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 d83c717da6a..35baeaeac61 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 @@ -1,12 +1,14 @@ -import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlSprintf, GlLink, GlCard } from '@gitlab/ui'; import Vue 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 component from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue'; -import ContainerExpirationPolicyForm from '~/packages_and_registries/settings/project/components/container_expiration_policy_form.vue'; import { + CONTAINER_CLEANUP_POLICY_EDIT_RULES, + CONTAINER_CLEANUP_POLICY_SET_RULES, + CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION, FETCH_SETTINGS_ERROR_MESSAGE, UNAVAILABLE_FEATURE_INTRO_TEXT, UNAVAILABLE_USER_FEATURE_TEXT, @@ -14,11 +16,7 @@ import { import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; -import { - expirationPolicyPayload, - emptyExpirationPolicyPayload, - containerExpirationPolicyData, -} from '../mock_data'; +import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data'; describe('Container expiration policy project settings', () => { let wrapper; @@ -28,17 +26,19 @@ describe('Container expiration policy project settings', () => { projectPath: 'path', isAdmin: false, adminSettingsPath: 'settingsPath', + cleanupSettingsPath: 'cleanupSettingsPath', enableHistoricEntries: false, helpPagePath: 'helpPagePath', - showCleanupPolicyLink: false, }; - const findFormComponent = () => wrapper.find(ContainerExpirationPolicyForm); - const findAlert = () => wrapper.find(GlAlert); - const findSettingsBlock = () => wrapper.find(SettingsBlock); + const findFormComponent = () => wrapper.findComponent(GlCard); + const findDescription = () => wrapper.findByTestId('description'); + const findButton = () => wrapper.findByTestId('rules-button'); + const findAlert = () => wrapper.findComponent(GlAlert); + const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); const mountComponent = (provide = defaultProvidedValues, config) => { - wrapper = shallowMount(component, { + wrapper = shallowMountExtended(component, { stubs: { GlSprintf, SettingsBlock, @@ -63,37 +63,19 @@ describe('Container expiration policy project settings', () => { wrapper.destroy(); }); - describe('isEdited status', () => { - it.each` - description | apiResponse | workingCopy | result - ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false} - ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true} - ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false} - ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true} - ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true} - `('$description', async ({ apiResponse, workingCopy, result }) => { - mountComponentWithApollo({ - provide: { ...defaultProvidedValues, enableHistoricEntries: true }, - resolver: jest.fn().mockResolvedValue(apiResponse), - }); - await waitForPromises(); - - findFormComponent().vm.$emit('input', workingCopy); - - await waitForPromises(); - - expect(findFormComponent().props('isEdited')).toBe(result); - }); - }); - it('renders the setting form', async () => { mountComponentWithApollo({ resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()), }); await waitForPromises(); - expect(findFormComponent().exists()).toBe(true); expect(findSettingsBlock().exists()).toBe(true); + expect(findFormComponent().exists()).toBe(true); + expect(findDescription().text()).toMatchInterpolatedText( + CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION, + ); + expect(findButton().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_EDIT_RULES); + expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath); }); describe('the form is disabled', () => { @@ -115,9 +97,9 @@ describe('Container expiration policy project settings', () => { it('shows the admin part of the alert message', () => { mountComponent({ ...defaultProvidedValues, isAdmin: true }); - const sprintf = findAlert().find(GlSprintf); + const sprintf = findAlert().findComponent(GlSprintf); expect(sprintf.text()).toBe('administration settings'); - expect(sprintf.find(GlLink).attributes('href')).toBe( + expect(sprintf.findComponent(GlLink).attributes('href')).toBe( defaultProvidedValues.adminSettingsPath, ); }); @@ -157,6 +139,10 @@ describe('Container expiration policy project settings', () => { await waitForPromises(); expect(findFormComponent().exists()).toBe(isShown); + if (isShown) { + expect(findButton().text()).toMatchInterpolatedText(CONTAINER_CLEANUP_POLICY_SET_RULES); + expect(findButton().attributes('href')).toBe(defaultProvidedValues.cleanupSettingsPath); + } }); }); }); 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 8b99ac6b06c..ae41fdf65e0 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 @@ -14,8 +14,8 @@ describe('ExpirationDropdown', () => { ], }; - const findFormSelect = () => wrapper.find(GlFormSelect); - const findFormGroup = () => wrapper.find(GlFormGroup); + const findFormSelect = () => wrapper.findComponent(GlFormSelect); + const findFormGroup = () => wrapper.findComponent(GlFormGroup); const findDescription = () => wrapper.find('[data-testid="description"]'); const findOptions = () => wrapper.findAll('[data-testid="option"]'); 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 6b681924fcf..1cea0704154 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 @@ -16,11 +16,11 @@ describe('ExpirationInput', () => { const tagsRegexHelpPagePath = 'fooPath'; - const findInput = () => wrapper.find(GlFormInput); - const findFormGroup = () => wrapper.find(GlFormGroup); + const findInput = () => wrapper.findComponent(GlFormInput); + const findFormGroup = () => wrapper.findComponent(GlFormGroup); const findLabel = () => wrapper.find('[data-testid="label"]'); const findDescription = () => wrapper.find('[data-testid="description"]'); - const findDescriptionLink = () => wrapper.find(GlLink); + const findDescriptionLink = () => wrapper.findComponent(GlLink); const mountComponent = (props) => { wrapper = shallowMount(component, { 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 94f7783afe7..653f2a8b40e 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 @@ -11,8 +11,8 @@ describe('ExpirationToggle', () => { let wrapper; const value = 'foo'; - const findInput = () => wrapper.find(GlFormInput); - const findFormGroup = () => wrapper.find(GlFormGroup); + const findInput = () => wrapper.findComponent(GlFormInput); + const findFormGroup = () => wrapper.findComponent(GlFormGroup); const mountComponent = (propsData) => { wrapper = shallowMount(component, { 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 45039614e49..55a66cebd83 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 @@ -10,7 +10,7 @@ import { describe('ExpirationToggle', () => { let wrapper; - const findToggle = () => wrapper.find(GlToggle); + const findToggle = () => wrapper.findComponent(GlToggle); const findDescription = () => wrapper.find('[data-testid="description"]'); const mountComponent = (propsData) => { 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 86f45d78bae..daf0ee85fdf 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 @@ -39,7 +39,7 @@ describe('Packages Cleanup Policy Settings Form', () => { label: 'packages_cleanup_policies', }; - const findForm = () => wrapper.find({ ref: 'form-element' }); + const findForm = () => wrapper.findComponent({ ref: 'form-element' }); const findSaveButton = () => wrapper.findByTestId('save-button'); const findKeepNDuplicatedPackageFilesDropdown = () => wrapper.findByTestId('keep-n-duplicated-package-files-dropdown'); 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 f576bc79eae..07d13839c61 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js @@ -1,41 +1,99 @@ +import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import * as commonUtils from '~/lib/utils/common_utils'; import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue'; import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue'; import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue'; +import { + SHOW_SETUP_SUCCESS_ALERT, + UPDATE_SETTINGS_SUCCESS_MESSAGE, +} from '~/packages_and_registries/settings/project/constants'; + +jest.mock('~/lib/utils/common_utils'); describe('Registry Settings app', () => { let wrapper; - const findContainerExpirationPolicy = () => wrapper.find(ContainerExpirationPolicy); - const findPackagesCleanupPolicy = () => wrapper.find(PackagesCleanupPolicy); + const findContainerExpirationPolicy = () => wrapper.findComponent(ContainerExpirationPolicy); + const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy); + const findAlert = () => wrapper.findComponent(GlAlert); afterEach(() => { wrapper.destroy(); wrapper = null; }); - const mountComponent = (provide) => { + const defaultProvide = { + showContainerRegistrySettings: true, + showPackageRegistrySettings: true, + }; + + const mountComponent = (provide = defaultProvide) => { wrapper = shallowMount(component, { provide, }); }; - it.each` - showContainerRegistrySettings | showPackageRegistrySettings - ${true} | ${false} - ${true} | ${true} - ${false} | ${true} - ${false} | ${false} - `( - 'container expiration policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings', - ({ showContainerRegistrySettings, showPackageRegistrySettings }) => { - mountComponent({ - showContainerRegistrySettings, - showPackageRegistrySettings, + describe('container policy success alert handling', () => { + const originalLocation = window.location.href; + const search = `?${SHOW_SETUP_SUCCESS_ALERT}=true`; + + beforeEach(() => { + setWindowLocation(search); + }); + + afterEach(() => { + setWindowLocation(originalLocation); + }); + + it(`renders alert if the query string contains ${SHOW_SETUP_SUCCESS_ALERT}`, async () => { + mountComponent(); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + expect(findAlert().props()).toMatchObject({ + dismissible: true, + variant: 'success', }); + expect(findAlert().text()).toMatchInterpolatedText(UPDATE_SETTINGS_SUCCESS_MESSAGE); + }); + + it('calls historyReplaceState with a clean url', () => { + mountComponent(); + + expect(commonUtils.historyReplaceState).toHaveBeenCalledWith(originalLocation); + }); + + it(`does nothing if the query string does not contain ${SHOW_SETUP_SUCCESS_ALERT}`, () => { + setWindowLocation('?'); + mountComponent(); - expect(findContainerExpirationPolicy().exists()).toBe(showContainerRegistrySettings); - expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings); - }, - ); + expect(findAlert().exists()).toBe(false); + expect(commonUtils.historyReplaceState).not.toHaveBeenCalled(); + }); + }); + + describe('settings', () => { + it.each` + showContainerRegistrySettings | showPackageRegistrySettings + ${true} | ${false} + ${true} | ${true} + ${false} | ${true} + ${false} | ${false} + `( + 'container expiration policy $showContainerRegistrySettings and package cleanup policy is $showPackageRegistrySettings', + ({ showContainerRegistrySettings, showPackageRegistrySettings }) => { + mountComponent({ + showContainerRegistrySettings, + showPackageRegistrySettings, + }); + + expect(findContainerExpirationPolicy().exists()).toBe(showContainerRegistrySettings); + expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings); + }, + ); + }); }); 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 new file mode 100644 index 00000000000..18084766db9 --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/cli_commands_spec.js @@ -0,0 +1,85 @@ +import { GlDropdown } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import QuickstartDropdown from '~/packages_and_registries/shared/components/cli_commands.vue'; +import { + QUICK_START, + LOGIN_COMMAND_LABEL, + COPY_LOGIN_TITLE, + BUILD_COMMAND_LABEL, + COPY_BUILD_TITLE, + PUSH_COMMAND_LABEL, + COPY_PUSH_TITLE, +} from '~/packages_and_registries/container_registry/explorer/constants'; +import Tracking from '~/tracking'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; + +import { dockerCommands } from 'jest/packages_and_registries/container_registry/explorer/mock_data'; + +Vue.use(Vuex); + +describe('cli_commands', () => { + let wrapper; + + const findDropdownButton = () => wrapper.findComponent(GlDropdown); + const findCodeInstruction = () => wrapper.findAllComponents(CodeInstruction); + + const mountComponent = () => { + wrapper = mount(QuickstartDropdown, { + propsData: { + ...dockerCommands, + }, + }); + }; + + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + mountComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('shows the correct text on the button', () => { + expect(findDropdownButton().text()).toContain(QUICK_START); + }); + + it('clicking on the dropdown emit a tracking event', () => { + findDropdownButton().vm.$emit('shown'); + expect(Tracking.event).toHaveBeenCalledWith( + undefined, + 'click_dropdown', + expect.objectContaining({ label: 'quickstart_dropdown' }), + ); + }); + + describe.each` + index | labelText | titleText | command | trackedEvent + ${0} | ${LOGIN_COMMAND_LABEL} | ${COPY_LOGIN_TITLE} | ${dockerCommands.dockerLoginCommand} | ${'click_copy_login'} + ${1} | ${BUILD_COMMAND_LABEL} | ${COPY_BUILD_TITLE} | ${dockerCommands.dockerBuildCommand} | ${'click_copy_build'} + ${2} | ${PUSH_COMMAND_LABEL} | ${COPY_PUSH_TITLE} | ${dockerCommands.dockerPushCommand} | ${'click_copy_push'} + `('code instructions at $index', ({ index, labelText, titleText, command, trackedEvent }) => { + let codeInstruction; + + beforeEach(() => { + codeInstruction = findCodeInstruction().at(index); + }); + + it('exists', () => { + expect(codeInstruction.exists()).toBe(true); + }); + + it(`has the correct props`, () => { + expect(codeInstruction.props()).toMatchObject({ + label: labelText, + instruction: command, + copyText: titleText, + trackingAction: trackedEvent, + trackingLabel: 'quickstart_dropdown', + }); + }); + }); +}); 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 index d6d1970cb12..a0ff6ca01b5 100644 --- 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 @@ -5,7 +5,7 @@ import PackageIconAndName from '~/packages_and_registries/shared/components/pack describe('PackageIconAndName', () => { let wrapper; - const findIcon = () => wrapper.find(GlIcon); + const findIcon = () => wrapper.findComponent(GlIcon); const mountComponent = () => { wrapper = shallowMount(PackageIconAndName, { 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 3a52c243867..3c512cfd6ae 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 @@ -48,7 +48,7 @@ describe('UsageStatistics', () => { expectEnabledservicePingFeaturesCheckBox(); }); - it('is switched to disabled when Service Ping checkbox is unchecked ', () => { + it('is switched to disabled when Service Ping checkbox is unchecked', () => { servicePingCheckBox.click(); servicePingFeaturesCheckBox.click(); expectEnabledservicePingFeaturesCheckBox(); 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 7a8a249cb2a..b020caa3010 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 @@ -14,7 +14,7 @@ describe('BitbucketServerStatusTable', () => { const findReconfigureButton = () => wrapper - .findAll(GlButton) + .findAllComponents(GlButton) .filter((w) => w.props().variant === 'info') .at(0); @@ -36,7 +36,7 @@ describe('BitbucketServerStatusTable', () => { it('renders bitbucket status table component', () => { createComponent(); - expect(wrapper.find(BitbucketStatusTable).exists()).toBe(true); + expect(wrapper.findComponent(BitbucketStatusTable).exists()).toBe(true); }); it('renders Reconfigure button', async () => { 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 a850b1655f7..1790a9c9bf5 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 @@ -84,7 +84,7 @@ describe('BulkImportsHistoryApp', () => { describe('general behavior', () => { it('renders loading state when loading', () => { createComponent(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders empty state when no data is available', async () => { @@ -92,8 +92,8 @@ describe('BulkImportsHistoryApp', () => { createComponent(); await axios.waitForAll(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(GlEmptyState).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true); }); it('renders table with data when history is available', async () => { @@ -101,7 +101,7 @@ describe('BulkImportsHistoryApp', () => { createComponent(); await axios.waitForAll(); - const table = wrapper.find(GlTable); + const table = wrapper.findComponent(GlTable); expect(table.exists()).toBe(true); // can't use .props() or .attributes() here expect(table.vm.$attrs.items).toHaveLength(DUMMY_RESPONSE.length); 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 4ff3f0361cf..82a3e11186e 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 @@ -41,7 +41,7 @@ describe('ImportErrorDetails', () => { describe('general behavior', () => { it('renders loading state when loading', () => { createComponent(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders import_error if it is available', async () => { @@ -50,7 +50,7 @@ describe('ImportErrorDetails', () => { createComponent(); await axios.waitForAll(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find('pre').text()).toBe(FAKE_IMPORT_ERROR); }); @@ -59,7 +59,7 @@ describe('ImportErrorDetails', () => { createComponent(); await axios.waitForAll(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.find('pre').text()).toBe('No additional information provided.'); }); }); 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 0d821b114cf..5030adae2fa 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 @@ -79,7 +79,7 @@ describe('ImportHistoryApp', () => { describe('general behavior', () => { it('renders loading state when loading', () => { createComponent(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('renders empty state when no data is available', async () => { @@ -87,8 +87,8 @@ describe('ImportHistoryApp', () => { createComponent(); await axios.waitForAll(); - expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find(GlEmptyState).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); + expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true); }); it('renders table with data when history is available', async () => { @@ -96,7 +96,7 @@ describe('ImportHistoryApp', () => { createComponent(); await axios.waitForAll(); - const table = wrapper.find(GlTable); + const table = wrapper.findComponent(GlTable); expect(table.exists()).toBe(true); expect(table.props().items).toStrictEqual(DUMMY_RESPONSE); }); @@ -127,7 +127,7 @@ describe('ImportHistoryApp', () => { expect(mock.history.get.length).toBe(1); expect(mock.history.get[0].params).toStrictEqual(expect.objectContaining({ page: NEW_PAGE })); - expect(wrapper.find(GlTable).props().items).toStrictEqual(FAKE_NEXT_PAGE_REPLY); + expect(wrapper.findComponent(GlTable).props().items).toStrictEqual(FAKE_NEXT_PAGE_REPLY); }); }); diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js deleted file mode 100644 index fa6e7e51a60..00000000000 --- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js +++ /dev/null @@ -1,115 +0,0 @@ -import $ from 'jquery'; -import { TEST_HOST } from 'helpers/test_constants'; -import axios from '~/lib/utils/axios_utils'; -import EmojiMenu from '~/pages/profiles/show/emoji_menu'; - -describe('EmojiMenu', () => { - const dummyEmojiTag = ''; - const dummyToggleButtonSelector = '.toggle-button-selector'; - const dummyMenuClass = 'dummy-menu-class'; - - let emojiMenu; - let dummySelectEmojiCallback; - let dummyEmojiList; - - beforeEach(() => { - dummySelectEmojiCallback = jest.fn().mockName('dummySelectEmojiCallback'); - dummyEmojiList = { - glEmojiTag() { - return dummyEmojiTag; - }, - normalizeEmojiName(emoji) { - return emoji; - }, - isEmojiNameValid() { - return true; - }, - getEmojiCategoryMap() { - return { dummyCategory: [] }; - }, - }; - - emojiMenu = new EmojiMenu( - dummyEmojiList, - dummyToggleButtonSelector, - dummyMenuClass, - dummySelectEmojiCallback, - ); - }); - - afterEach(() => { - emojiMenu.destroy(); - }); - - describe('addAward', () => { - const dummyAwardUrl = `${TEST_HOST}/award/url`; - const dummyEmoji = 'tropical_fish'; - const dummyVotesBlock = () => $('
'); - - it('calls selectEmojiCallback', async () => { - expect(dummySelectEmojiCallback).not.toHaveBeenCalled(); - - await emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false); - expect(dummySelectEmojiCallback).toHaveBeenCalledWith(dummyEmoji, dummyEmojiTag); - }); - - it('does not make an axios request', async () => { - jest.spyOn(axios, 'request').mockReturnValue(); - - await emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false); - expect(axios.request).not.toHaveBeenCalled(); - }); - }); - - describe('bindEvents', () => { - beforeEach(() => { - jest.spyOn(emojiMenu, 'registerEventListener').mockReturnValue(); - }); - - it('binds event listeners to custom toggle button', () => { - emojiMenu.bindEvents(); - - expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( - 'one', - expect.anything(), - 'mouseenter focus', - dummyToggleButtonSelector, - 'mouseenter focus', - expect.anything(), - ); - - expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( - 'on', - expect.anything(), - 'click', - dummyToggleButtonSelector, - expect.anything(), - ); - }); - - it('binds event listeners to custom menu class', () => { - emojiMenu.bindEvents(); - - expect(emojiMenu.registerEventListener).toHaveBeenCalledWith( - 'on', - expect.anything(), - 'click', - `.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`, - expect.anything(), - ); - }); - }); - - describe('createEmojiMenu', () => { - it('renders the menu with custom menu class', () => { - const menuElement = () => - document.body.querySelector(`.emoji-menu.${dummyMenuClass} .emoji-menu-content`); - - expect(menuElement()).toBe(null); - - emojiMenu.createEmojiMenu(); - - expect(menuElement()).not.toBe(null); - }); - }); -}); 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 2a0fde45384..f221a90da61 100644 --- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js +++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js @@ -4,11 +4,14 @@ import { mount, shallowMount } from '@vue/test-utils'; import axios from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; import { kebabCase } from 'lodash'; -import { nextTick } from 'vue'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import createFlash from '~/flash'; -import httpStatus from '~/lib/utils/http_status'; 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'; +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('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); @@ -16,6 +19,7 @@ jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); describe('ForkForm component', () => { let wrapper; let axiosMock; + let mockQueryResponse; const PROJECT_VISIBILITY_TYPE = { private: @@ -24,26 +28,11 @@ describe('ForkForm component', () => { public: 'Public The project can be accessed without any authentication.', }; - const GON_GITLAB_URL = 'https://gitlab.com'; const GON_API_VERSION = 'v7'; - const MOCK_NAMESPACES_RESPONSE = [ - { - name: 'one', - full_name: 'one-group/one', - id: 1, - }, - { - name: 'two', - full_name: 'two-group/two', - id: 2, - }, - ]; - const DEFAULT_PROVIDE = { newGroupPath: 'some/groups/path', visibilityHelpPath: 'some/visibility/help/path', - endpoint: '/some/project-full-path/-/forks/new.json', projectFullPath: '/some/project-full-path', projectId: '10', projectName: 'Project Name', @@ -53,12 +42,44 @@ describe('ForkForm component', () => { restrictedVisibilityLevels: [], }; - const mockGetRequest = (data = {}, statusCode = httpStatus.OK) => { - axiosMock.onGet(DEFAULT_PROVIDE.endpoint).replyOnce(statusCode, data); - }; + Vue.use(VueApollo); const createComponentFactory = (mountFn) => (provide = {}, data = {}) => { + const queryResponse = { + project: { + id: 'gid://gitlab/Project/1', + forkTargets: { + nodes: [ + { + id: 'gid://gitlab/Group/21', + fullPath: 'flightjs', + name: 'Flight JS', + visibility: 'public', + }, + { + id: 'gid://gitlab/Namespace/4', + fullPath: 'root', + name: 'Administrator', + visibility: 'public', + }, + ], + }, + }, + }; + + mockQueryResponse = jest.fn().mockResolvedValue({ data: queryResponse }); + const requestHandlers = [[searchQuery, mockQueryResponse]]; + const apolloProvider = createMockApollo(requestHandlers); + + apolloProvider.clients.defaultClient.cache.writeQuery({ + query: searchQuery, + data: { + ...queryResponse, + }, + }); + wrapper = mountFn(ForkForm, { + apolloProvider, provide: { ...DEFAULT_PROVIDE, ...provide, @@ -83,7 +104,6 @@ describe('ForkForm component', () => { beforeEach(() => { axiosMock = new AxiosMockAdapter(axios); window.gon = { - gitlab_url: GON_GITLAB_URL, api_version: GON_API_VERSION, }; }); @@ -93,12 +113,11 @@ describe('ForkForm component', () => { axiosMock.restore(); }); - const findFormSelectOptions = () => wrapper.find('select[name="namespace"]').findAll('option'); const findPrivateRadio = () => wrapper.find('[data-testid="radio-private"]'); const findInternalRadio = () => wrapper.find('[data-testid="radio-internal"]'); const findPublicRadio = () => wrapper.find('[data-testid="radio-public"]'); const findForkNameInput = () => wrapper.find('[data-testid="fork-name-input"]'); - const findForkUrlInput = () => wrapper.find('[data-testid="fork-url-input"]'); + const findForkUrlInput = () => wrapper.findComponent(ProjectNamespace); const findForkSlugInput = () => wrapper.find('[data-testid="fork-slug-input"]'); const findForkDescriptionTextarea = () => wrapper.find('[data-testid="fork-description-textarea"]'); @@ -106,7 +125,6 @@ describe('ForkForm component', () => { wrapper.find('[data-testid="fork-visibility-radio-group"]'); it('will go to projectFullPath when click cancel button', () => { - mockGetRequest(); createComponent(); const { projectFullPath } = DEFAULT_PROVIDE; @@ -115,8 +133,13 @@ describe('ForkForm component', () => { expect(cancelButton.attributes('href')).toBe(projectFullPath); }); + const selectedMockNamespace = { name: 'two', full_name: 'two-group/two', id: 2 }; + + const fillForm = () => { + findForkUrlInput().vm.$emit('select', selectedMockNamespace); + }; + it('has input with csrf token', () => { - mockGetRequest(); createComponent(); expect(wrapper.find('input[name="authenticity_token"]').attributes('value')).toBe( @@ -125,7 +148,6 @@ describe('ForkForm component', () => { }); it('pre-populate form from project props', () => { - mockGetRequest(); createComponent(); expect(findForkNameInput().attributes('value')).toBe(DEFAULT_PROVIDE.projectName); @@ -135,75 +157,19 @@ describe('ForkForm component', () => { ); }); - it('sets project URL prepend text with gon.gitlab_url', () => { - mockGetRequest(); - createComponent(); - - expect(wrapper.find(GlFormInputGroup).text()).toContain(`${GON_GITLAB_URL}/`); - }); - it('will have required attribute for required fields', () => { - mockGetRequest(); createComponent(); expect(findForkNameInput().attributes('required')).not.toBeUndefined(); - expect(findForkUrlInput().attributes('required')).not.toBeUndefined(); expect(findForkSlugInput().attributes('required')).not.toBeUndefined(); expect(findVisibilityRadioGroup().attributes('required')).not.toBeUndefined(); expect(findForkDescriptionTextarea().attributes('required')).toBeUndefined(); }); - describe('forks namespaces', () => { - beforeEach(() => { - mockGetRequest({ namespaces: MOCK_NAMESPACES_RESPONSE }); - createFullComponent(); - }); - - it('make GET request from endpoint', async () => { - await axios.waitForAll(); - - expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROVIDE.endpoint); - }); - - it('generate default option', async () => { - await axios.waitForAll(); - - const optionsArray = findForkUrlInput().findAll('option'); - - expect(optionsArray.at(0).text()).toBe('Select a namespace'); - }); - - it('populate project url namespace options', async () => { - await axios.waitForAll(); - - const optionsArray = findForkUrlInput().findAll('option'); - - expect(optionsArray).toHaveLength(MOCK_NAMESPACES_RESPONSE.length + 1); - expect(optionsArray.at(1).text()).toBe(MOCK_NAMESPACES_RESPONSE[0].full_name); - expect(optionsArray.at(2).text()).toBe(MOCK_NAMESPACES_RESPONSE[1].full_name); - }); - - it('set namespaces in alphabetical order', async () => { - const namespace = { - name: 'three', - full_name: 'aaa/three', - id: 3, - }; - mockGetRequest({ - namespaces: [...MOCK_NAMESPACES_RESPONSE, namespace], - }); - createComponent(); - await axios.waitForAll(); - - expect(wrapper.vm.namespaces).toEqual([namespace, ...MOCK_NAMESPACES_RESPONSE]); - }); - }); - describe('project slug', () => { const projectPath = 'some other project slug'; beforeEach(() => { - mockGetRequest(); createComponent({ projectPath, }); @@ -232,10 +198,9 @@ describe('ForkForm component', () => { describe('visibility level', () => { it('displays the correct description', () => { - mockGetRequest(); createComponent(); - const formRadios = wrapper.findAll(GlFormRadio); + const formRadios = wrapper.findAllComponents(GlFormRadio); Object.keys(PROJECT_VISIBILITY_TYPE).forEach((visibilityType, index) => { expect(formRadios.at(index).text()).toBe(PROJECT_VISIBILITY_TYPE[visibilityType]); @@ -243,10 +208,9 @@ describe('ForkForm component', () => { }); it('displays all 3 visibility levels', () => { - mockGetRequest(); createComponent(); - expect(wrapper.findAll(GlFormRadio)).toHaveLength(3); + expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(3); }); describe('when the namespace is changed', () => { @@ -262,16 +226,12 @@ describe('ForkForm component', () => { }, ]; - beforeEach(() => { - mockGetRequest(); - }); - it('resets the visibility to default "private"', async () => { createFullComponent({ projectVisibility: 'public' }, { namespaces }); expect(wrapper.vm.form.fields.visibility.value).toBe('public'); - await findFormSelectOptions().at(1).setSelected(); + fillForm(); await nextTick(); expect(getByRole(wrapper.element, 'radio', { name: /private/i }).checked).toBe(true); @@ -280,8 +240,7 @@ describe('ForkForm component', () => { it('sets the visibility to be null when restrictedVisibilityLevels is set', async () => { createFullComponent({ restrictedVisibilityLevels: [10] }, { namespaces }); - await findFormSelectOptions().at(1).setSelected(); - + fillForm(); await nextTick(); const container = getByRole(wrapper.element, 'radiogroup', { name: /visibility/i }); @@ -315,8 +274,7 @@ describe('ForkForm component', () => { ${'public'} | ${[0, 20]} ${'public'} | ${[10, 20]} ${'public'} | ${[0, 10, 20]} - `('checks the correct radio button', async ({ project, restrictedVisibilityLevels }) => { - mockGetRequest(); + `('checks the correct radio button', ({ project, restrictedVisibilityLevels }) => { createFullComponent({ projectVisibility: project, restrictedVisibilityLevels, @@ -357,7 +315,7 @@ describe('ForkForm component', () => { ${'public'} | ${'public'} | ${undefined} | ${'true'} | ${'true'} | ${[0, 10, 20]} `( 'sets appropriate radio button disabled state', - async ({ + ({ project, namespace, privateIsDisabled, @@ -365,7 +323,6 @@ describe('ForkForm component', () => { publicIsDisabled, restrictedVisibilityLevels, }) => { - mockGetRequest(); createComponent( { projectVisibility: project, @@ -387,11 +344,9 @@ describe('ForkForm component', () => { const setupComponent = (fields = {}) => { jest.spyOn(urlUtility, 'redirectTo').mockImplementation(); - mockGetRequest(); createFullComponent( {}, { - namespaces: MOCK_NAMESPACES_RESPONSE, form: { state: true, ...fields, @@ -400,25 +355,21 @@ describe('ForkForm component', () => { ); }; - const selectedMockNamespaceIndex = 1; - const namespaceId = MOCK_NAMESPACES_RESPONSE[selectedMockNamespaceIndex].id; - - const fillForm = async () => { - const namespaceOptions = findForkUrlInput().findAll('option'); - - await namespaceOptions.at(selectedMockNamespaceIndex + 1).setSelected(); - }; + beforeEach(() => { + setupComponent(); + }); const submitForm = async () => { - await fillForm(); - const form = wrapper.find(GlForm); + fillForm(); + await nextTick(); + const form = wrapper.findComponent(GlForm); await form.trigger('submit'); await nextTick(); }; describe('with invalid form', () => { - it('does not make POST request', async () => { + it('does not make POST request', () => { jest.spyOn(axios, 'post'); setupComponent(); @@ -471,7 +422,7 @@ describe('ForkForm component', () => { description: projectDescription, id: projectId, name: projectName, - namespace_id: namespaceId, + namespace_id: selectedMockNamespace.id, path: projectPath, visibility: projectVisibility, }; 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 new file mode 100644 index 00000000000..1a88aebae32 --- /dev/null +++ b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js @@ -0,0 +1,177 @@ +import { + GlButton, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + GlTruncate, +} 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 createFlash from '~/flash'; +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'); + +describe('ProjectNamespace component', () => { + let wrapper; + let originalGon; + + const data = { + project: { + __typename: 'Project', + id: 'gid://gitlab/Project/1', + forkTargets: { + nodes: [ + { + id: 'gid://gitlab/Group/21', + fullPath: 'flightjs', + name: 'Flight JS', + visibility: 'public', + }, + { + id: 'gid://gitlab/Namespace/4', + fullPath: 'root', + name: 'Administrator', + visibility: 'public', + }, + ], + }, + }, + }; + + const mockQueryResponse = jest.fn().mockResolvedValue({ data }); + + const emptyQueryResponse = { + project: { + __typename: 'Project', + id: 'gid://gitlab/Project/1', + forkTargets: { + nodes: [], + }, + }, + }; + + const mockQueryError = jest.fn().mockRejectedValue(new Error('Network error')); + + Vue.use(VueApollo); + + const gitlabUrl = 'https://gitlab.com'; + + const defaultProvide = { + projectFullPath: 'gitlab-org/project', + }; + + const mountComponent = ({ + provide = defaultProvide, + queryHandler = mockQueryResponse, + mountFn = shallowMount, + } = {}) => { + const requestHandlers = [[searchQuery, queryHandler]]; + const apolloProvider = createMockApollo(requestHandlers); + + wrapper = mountFn(ProjectNamespace, { + apolloProvider, + provide, + }); + }; + + const findButtonLabel = () => wrapper.findComponent(GlButton); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownText = () => wrapper.findComponent(GlTruncate); + const findInput = () => wrapper.findComponent(GlSearchBoxByType); + + const clickDropdownItem = async () => { + wrapper.findComponent(GlDropdownItem).vm.$emit('click'); + await nextTick(); + }; + + const showDropdown = () => { + findDropdown().vm.$emit('shown'); + }; + + beforeAll(() => { + originalGon = window.gon; + window.gon = { gitlab_url: gitlabUrl }; + }); + + afterAll(() => { + window.gon = originalGon; + wrapper.destroy(); + }); + + describe('Initial state', () => { + beforeEach(() => { + mountComponent({ mountFn: mount }); + jest.runOnlyPendingTimers(); + }); + + it('renders the root url as a label', () => { + expect(findButtonLabel().text()).toBe(`${gitlabUrl}/`); + expect(findButtonLabel().props('label')).toBe(true); + }); + + it('renders placeholder text', () => { + expect(findDropdownText().props('text')).toBe('Select a namespace'); + }); + }); + + describe('After user interactions', () => { + beforeEach(async () => { + mountComponent({ mountFn: mount }); + jest.runOnlyPendingTimers(); + await nextTick(); + showDropdown(); + }); + + it('focuses on the input when the dropdown is opened', () => { + const spy = jest.spyOn(findInput().vm, 'focusInput'); + showDropdown(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('displays fetched namespaces', () => { + const listItems = wrapper.findAll('li'); + expect(listItems).toHaveLength(3); + expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Namespaces'); + expect(listItems.at(1).text()).toBe(data.project.forkTargets.nodes[0].fullPath); + expect(listItems.at(2).text()).toBe(data.project.forkTargets.nodes[1].fullPath); + }); + + it('sets the selected namespace', async () => { + const { fullPath } = data.project.forkTargets.nodes[0]; + await clickDropdownItem(); + expect(findDropdownText().props('text')).toBe(fullPath); + }); + }); + + describe('With empty query response', () => { + beforeEach(() => { + mountComponent({ queryHandler: emptyQueryResponse, mountFn: mount }); + jest.runOnlyPendingTimers(); + }); + + it('renders `No matches found`', () => { + expect(wrapper.find('li').text()).toBe('No matches found'); + }); + }); + + describe('With error while fetching data', () => { + beforeEach(async () => { + mountComponent({ queryHandler: mockQueryError }); + jest.runOnlyPendingTimers(); + await nextTick(); + }); + + it('creates a flash message and captures the error', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: 'Something went wrong while loading data. Please refresh the page to try again.', + captureError: true, + error: expect.any(Error), + }); + }); + }); +}); diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js index f272891919d..2f2edd6b025 100644 --- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js +++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js @@ -20,9 +20,9 @@ describe('Code Coverage', () => { const graphRef = 'master'; const graphCsvPath = 'url/'; - const findAlert = () => wrapper.find(GlAlert); - const findAreaChart = () => wrapper.find(GlAreaChart); - const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findAlert = () => wrapper.findComponent(GlAlert); + const findAreaChart = () => wrapper.findComponent(GlAreaChart); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findFirstDropdownItem = () => findAllDropdownItems().at(0); const findSecondDropdownItem = () => findAllDropdownItems().at(1); const findDownloadButton = () => wrapper.find('[data-testid="download-button"]'); @@ -142,7 +142,7 @@ describe('Code Coverage', () => { }); it('renders the dropdown with all custom names as options', () => { - expect(wrapper.find(GlDropdown).exists()).toBeDefined(); + expect(wrapper.findComponent(GlDropdown).exists()).toBeDefined(); expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length); expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name); }); diff --git a/spec/frontend/pages/projects/merge_requests/edit/update_form_spec.js b/spec/frontend/pages/projects/merge_requests/edit/update_form_spec.js new file mode 100644 index 00000000000..72077038dff --- /dev/null +++ b/spec/frontend/pages/projects/merge_requests/edit/update_form_spec.js @@ -0,0 +1,59 @@ +import { setHTMLFixture, resetHTMLFixture } from 'jest/__helpers__/fixtures'; +import initFormUpdate from '~/pages/projects/merge_requests/edit/update_form'; + +describe('Update form state', () => { + const submitEvent = new Event('submit', { + bubbles: true, + cancelable: true, + }); + + const submitForm = () => document.querySelector('.merge-request-form').dispatchEvent(submitEvent); + const hiddenInputs = () => document.querySelectorAll('input[type="hidden"]'); + const checkboxes = () => document.querySelectorAll('.js-form-update'); + + beforeEach(() => { + setHTMLFixture(` +
+
+ + +
+
+ + +
+
`); + initFormUpdate(); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + it('at initial state', () => { + submitForm(); + expect(hiddenInputs()).toHaveLength(2); + }); + + it('when one element is checked', () => { + checkboxes()[0].setAttribute('checked', true); + submitForm(); + expect(hiddenInputs()).toHaveLength(1); + }); + + it('when all elements are checked', () => { + checkboxes()[0].setAttribute('checked', true); + checkboxes()[1].setAttribute('checked', true); + submitForm(); + expect(hiddenInputs()).toHaveLength(0); + }); + + it('when checked and then unchecked', () => { + checkboxes()[0].setAttribute('checked', true); + checkboxes()[0].removeAttribute('checked'); + checkboxes()[1].setAttribute('checked', true); + checkboxes()[1].removeAttribute('checked'); + submitForm(); + expect(hiddenInputs()).toHaveLength(2); + }); +}); 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 ca7f70f4434..a633332ab65 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 @@ -21,7 +21,7 @@ describe('Pipeline Schedule Callout', () => { }; const findInnerContentOfCallout = () => wrapper.find('[data-testid="innerContent"]'); - const findDismissCalloutBtn = () => wrapper.find(GlButton); + const findDismissCalloutBtn = () => wrapper.findComponent(GlButton); describe(`when ${cookieKey} cookie is set`, () => { beforeEach(async () => { 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 f908508c4b5..ed7d4ad269e 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 @@ -5,8 +5,12 @@ import settingsPanel from '~/pages/projects/shared/permissions/components/settin import { featureAccessLevel, visibilityLevelDescriptions, - visibilityOptions, } from '~/pages/projects/shared/permissions/constants'; +import { + VISIBILITY_LEVEL_PRIVATE_INTEGER, + VISIBILITY_LEVEL_INTERNAL_INTEGER, + VISIBILITY_LEVEL_PUBLIC_INTEGER, +} from '~/visibility_level/constants'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; const defaultProps = { @@ -81,15 +85,17 @@ describe('Settings Panel', () => { }); }; - const findLFSSettingsRow = () => wrapper.find({ ref: 'git-lfs-settings' }); + const findLFSSettingsRow = () => wrapper.findComponent({ ref: 'git-lfs-settings' }); const findLFSSettingsMessage = () => findLFSSettingsRow().find('p'); - const findLFSFeatureToggle = () => findLFSSettingsRow().find(GlToggle); - const findRepositoryFeatureProjectRow = () => wrapper.find({ ref: 'repository-settings' }); + const findLFSFeatureToggle = () => findLFSSettingsRow().findComponent(GlToggle); + const findRepositoryFeatureProjectRow = () => + wrapper.findComponent({ ref: 'repository-settings' }); const findRepositoryFeatureSetting = () => - findRepositoryFeatureProjectRow().find(ProjectFeatureSetting); - const findProjectVisibilitySettings = () => wrapper.find({ ref: 'project-visibility-settings' }); - const findIssuesSettingsRow = () => wrapper.find({ ref: 'issues-settings' }); - const findAnalyticsRow = () => wrapper.find({ ref: 'analytics-settings' }); + findRepositoryFeatureProjectRow().findComponent(ProjectFeatureSetting); + const findProjectVisibilitySettings = () => + wrapper.findComponent({ ref: 'project-visibility-settings' }); + const findIssuesSettingsRow = () => wrapper.findComponent({ ref: 'issues-settings' }); + const findAnalyticsRow = () => wrapper.findComponent({ ref: 'analytics-settings' }); const findProjectVisibilityLevelInput = () => wrapper.find('[name="project[visibility_level]"]'); const findRequestAccessEnabledInput = () => wrapper.find('[name="project[request_access_enabled]"]'); @@ -99,35 +105,40 @@ describe('Settings Panel', () => { wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]'); const findBuildsAccessLevelInput = () => wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]'); - const findContainerRegistrySettings = () => wrapper.find({ ref: 'container-registry-settings' }); + const findContainerRegistrySettings = () => + wrapper.findComponent({ ref: 'container-registry-settings' }); const findContainerRegistryPublicNoteGlSprintfComponent = () => findContainerRegistrySettings().findComponent(GlSprintf); const findContainerRegistryAccessLevelInput = () => wrapper.find('[name="project[project_feature_attributes][container_registry_access_level]"]'); - const findPackageSettings = () => wrapper.find({ ref: 'package-settings' }); + const findPackageSettings = () => wrapper.findComponent({ ref: 'package-settings' }); const findPackageAccessLevel = () => wrapper.find('[data-testid="package-registry-access-level"]'); const findPackageAccessLevels = () => wrapper.find('[name="project[project_feature_attributes][package_registry_access_level]"]'); const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]'); - const findPagesSettings = () => wrapper.find({ ref: 'pages-settings' }); + const findPagesSettings = () => wrapper.findComponent({ ref: 'pages-settings' }); const findPagesAccessLevels = () => wrapper.find('[name="project[project_feature_attributes][pages_access_level]"]'); - const findEmailSettings = () => wrapper.find({ ref: 'email-settings' }); + const findEmailSettings = () => wrapper.findComponent({ ref: 'email-settings' }); const findShowDefaultAwardEmojis = () => wrapper.find('input[name="project[project_setting_attributes][show_default_award_emojis]"]'); const findWarnAboutPuc = () => wrapper.find( 'input[name="project[project_setting_attributes][warn_about_potentially_unwanted_characters]"]', ); - const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' }); + const findMetricsVisibilitySettings = () => + wrapper.findComponent({ ref: 'metrics-visibility-settings' }); const findMetricsVisibilityInput = () => findMetricsVisibilitySettings().findComponent(ProjectFeatureSetting); - const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' }); + const findOperationsSettings = () => wrapper.findComponent({ ref: 'operations-settings' }); const findOperationsVisibilityInput = () => findOperationsSettings().findComponent(ProjectFeatureSetting); const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger); const findEnvironmentsSettings = () => wrapper.findComponent({ ref: 'environments-settings' }); + const findFeatureFlagsSettings = () => wrapper.findComponent({ ref: 'feature-flags-settings' }); + const findReleasesSettings = () => wrapper.findComponent({ ref: 'environments-settings' }); + const findMonitorSettings = () => wrapper.findComponent({ ref: 'monitor-settings' }); afterEach(() => { wrapper.destroy(); @@ -156,13 +167,13 @@ describe('Settings Panel', () => { }); it.each` - option | allowedOptions | disabled - ${visibilityOptions.PRIVATE} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false} - ${visibilityOptions.PRIVATE} | ${[visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${true} - ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false} - ${visibilityOptions.INTERNAL} | ${[visibilityOptions.PRIVATE, visibilityOptions.PUBLIC]} | ${true} - ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL, visibilityOptions.PUBLIC]} | ${false} - ${visibilityOptions.PUBLIC} | ${[visibilityOptions.PRIVATE, visibilityOptions.INTERNAL]} | ${true} + option | allowedOptions | disabled + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${true} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${true} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER, VISIBILITY_LEVEL_PUBLIC_INTEGER]} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[VISIBILITY_LEVEL_PRIVATE_INTEGER, VISIBILITY_LEVEL_INTERNAL_INTEGER]} | ${true} `( 'sets disabled to $disabled for the visibility option $option when given $allowedOptions', ({ option, allowedOptions, disabled }) => { @@ -181,35 +192,37 @@ describe('Settings Panel', () => { it('should set the visibility level description based upon the selected visibility level', () => { wrapper = mountComponent({ stubs: { GlSprintf } }); - findProjectVisibilityLevelInput().setValue(visibilityOptions.INTERNAL); + findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_INTERNAL_INTEGER); expect(findProjectVisibilitySettings().text()).toContain( - visibilityLevelDescriptions[visibilityOptions.INTERNAL], + visibilityLevelDescriptions[VISIBILITY_LEVEL_INTERNAL_INTEGER], ); }); it('should show the request access checkbox if the visibility level is not private', () => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.INTERNAL }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER }, }); expect(findRequestAccessEnabledInput().exists()).toBe(true); }); it('should not show the request access checkbox if the visibility level is private', () => { - wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } }); + wrapper = mountComponent({ + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER }, + }); expect(findRequestAccessEnabledInput().exists()).toBe(false); }); it('does not require confirmation if the visibility is reduced', async () => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.INTERNAL }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER }, }); expect(findConfirmDangerButton().exists()).toBe(false); - await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER); expect(findConfirmDangerButton().exists()).toBe(false); }); @@ -217,7 +230,7 @@ describe('Settings Panel', () => { describe('showVisibilityConfirmModal=true', () => { beforeEach(() => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.INTERNAL }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_INTERNAL_INTEGER }, showVisibilityConfirmModal: true, }); }); @@ -225,7 +238,7 @@ describe('Settings Panel', () => { it('will render the confirmation dialog if the visibility is reduced', async () => { expect(findConfirmDangerButton().exists()).toBe(false); - await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER); expect(findConfirmDangerButton().exists()).toBe(true); }); @@ -233,7 +246,7 @@ describe('Settings Panel', () => { it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => { expect(wrapper.emitted('confirm')).toBeUndefined(); - await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER); await findConfirmDangerButton().vm.$emit('confirm'); expect(wrapper.emitted('confirm')).toHaveLength(1); @@ -253,7 +266,9 @@ describe('Settings Panel', () => { describe('Repository', () => { it('should set the repository help text when the visibility level is set to private', () => { - wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PRIVATE } }); + wrapper = mountComponent({ + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER }, + }); expect(findRepositoryFeatureProjectRow().props('helpText')).toBe( 'View and edit files in this project.', @@ -261,7 +276,9 @@ describe('Settings Panel', () => { }); it('should set the repository help text with a read access warning when the visibility level is set to non-private', () => { - wrapper = mountComponent({ currentSettings: { visibilityLevel: visibilityOptions.PUBLIC } }); + wrapper = mountComponent({ + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER }, + }); expect(findRepositoryFeatureProjectRow().props('helpText')).toBe( 'View and edit files in this project. Non-project members have only read access.', @@ -345,7 +362,7 @@ describe('Settings Panel', () => { it('should show the container registry public note if the visibility level is public and the registry is available', () => { wrapper = mountComponent({ currentSettings: { - visibilityLevel: visibilityOptions.PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER, containerRegistryAccessLevel: featureAccessLevel.EVERYONE, }, registryAvailable: true, @@ -360,7 +377,7 @@ describe('Settings Panel', () => { it('should hide the container registry public note if the visibility level is public but the registry is private', () => { wrapper = mountComponent({ currentSettings: { - visibilityLevel: visibilityOptions.PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER, containerRegistryAccessLevel: featureAccessLevel.PROJECT_MEMBERS, }, registryAvailable: true, @@ -371,7 +388,7 @@ describe('Settings Panel', () => { it('should hide the container registry public note if the visibility level is private and the registry is available', () => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.PRIVATE }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PRIVATE_INTEGER }, registryAvailable: true, }); @@ -380,7 +397,7 @@ describe('Settings Panel', () => { it('has label for the toggle', () => { wrapper = mountComponent({ - currentSettings: { visibilityLevel: visibilityOptions.PUBLIC }, + currentSettings: { visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER }, registryAvailable: true, }); @@ -569,10 +586,10 @@ describe('Settings Panel', () => { }); it.each` - visibilityLevel | output - ${visibilityOptions.PRIVATE} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]} - ${visibilityOptions.INTERNAL} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]} - ${visibilityOptions.PUBLIC} | ${[[30, 'Everyone']]} + visibilityLevel | output + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${[[featureAccessLevel.PROJECT_MEMBERS, 'Only Project Members'], [30, 'Everyone']]} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${[[featureAccessLevel.EVERYONE, 'Everyone With Access'], [30, 'Everyone']]} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${[[30, 'Everyone']]} `( 'renders correct options when visibilityLevel is $visibilityLevel', async ({ visibilityLevel, output }) => { @@ -589,23 +606,23 @@ describe('Settings Panel', () => { ); it.each` - initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption - ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.PRIVATE} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.INTERNAL} | ${visibilityOptions.PUBLIC} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} - ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.PUBLIC} | ${visibilityOptions.PRIVATE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS} - ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} - ${visibilityOptions.PUBLIC} | ${visibilityOptions.INTERNAL} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE} + initialProjectVisibilityLevel | newProjectVisibilityLevel | initialPackageRegistryOption | expectedPackageRegistryOption + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE} `( 'changes option from $initialPackageRegistryOption to $expectedPackageRegistryOption when visibilityLevel changed from $initialProjectVisibilityLevel to $newProjectVisibilityLevel', async ({ @@ -635,13 +652,13 @@ describe('Settings Panel', () => { describe('Pages', () => { it.each` - visibilityLevel | pagesAccessControlForced | output - ${visibilityOptions.PRIVATE} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.PRIVATE} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} - ${visibilityOptions.INTERNAL} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.INTERNAL} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} - ${visibilityOptions.PUBLIC} | ${true} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access']]} - ${visibilityOptions.PUBLIC} | ${false} | ${[[visibilityOptions.INTERNAL, 'Only Project Members'], [visibilityOptions.PUBLIC, 'Everyone With Access'], [30, 'Everyone']]} + visibilityLevel | pagesAccessControlForced | output + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]} + ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]} + ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${true} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access']]} + ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${false} | ${[[VISIBILITY_LEVEL_INTERNAL_INTEGER, 'Only Project Members'], [VISIBILITY_LEVEL_PUBLIC_INTEGER, 'Everyone With Access'], [30, 'Everyone']]} `( 'renders correct options when pagesAccessControlForced is $pagesAccessControlForced and visibilityLevel is $visibilityLevel', async ({ visibilityLevel, pagesAccessControlForced, output }) => { @@ -760,13 +777,13 @@ describe('Settings Panel', () => { it('should reduce Metrics visibility level when visibility is set to private', async () => { wrapper = mountComponent({ currentSettings: { - visibilityLevel: visibilityOptions.PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_INTEGER, operationsAccessLevel: featureAccessLevel.EVERYONE, metricsDashboardAccessLevel: featureAccessLevel.EVERYONE, }, }); - await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findProjectVisibilityLevelInput().setValue(VISIBILITY_LEVEL_PRIVATE_INTEGER); expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.PROJECT_MEMBERS); }); @@ -806,4 +823,78 @@ describe('Settings Panel', () => { }); }); }); + describe('Feature Flags', () => { + describe('with feature flag', () => { + it('should show the feature flags toggle', () => { + wrapper = mountComponent({ + glFeatures: { splitOperationsVisibilityPermissions: true }, + }); + + expect(findFeatureFlagsSettings().exists()).toBe(true); + }); + }); + describe('without feature flag', () => { + it('should not show the feature flags toggle', () => { + wrapper = mountComponent({}); + + expect(findFeatureFlagsSettings().exists()).toBe(false); + }); + }); + }); + describe('Releases', () => { + describe('with feature flag', () => { + it('should show the releases toggle', () => { + wrapper = mountComponent({ + glFeatures: { splitOperationsVisibilityPermissions: true }, + }); + + expect(findReleasesSettings().exists()).toBe(true); + }); + }); + describe('without feature flag', () => { + it('should not show the releases toggle', () => { + wrapper = mountComponent({}); + + expect(findReleasesSettings().exists()).toBe(false); + }); + }); + }); + describe('Monitor', () => { + const expectedAccessLevel = [ + [10, 'Only Project Members'], + [20, 'Everyone With Access'], + ]; + describe('with feature flag', () => { + it('shows Monitor toggle instead of Operations toggle', () => { + wrapper = mountComponent({ + glFeatures: { splitOperationsVisibilityPermissions: true }, + }); + + expect(findMonitorSettings().exists()).toBe(true); + expect(findOperationsSettings().exists()).toBe(false); + expect(findMonitorSettings().findComponent(ProjectFeatureSetting).props('options')).toEqual( + expectedAccessLevel, + ); + }); + it('when monitorAccessLevel is for project members, it is also for everyone', () => { + wrapper = mountComponent({ + glFeatures: { splitOperationsVisibilityPermissions: true }, + currentSettings: { monitorAccessLevel: featureAccessLevel.PROJECT_MEMBERS }, + }); + + expect(findMetricsVisibilityInput().props('value')).toBe(featureAccessLevel.EVERYONE); + }); + }); + describe('without feature flag', () => { + it('shows Operations toggle instead of Monitor toggle', () => { + wrapper = mountComponent({}); + + expect(findMonitorSettings().exists()).toBe(false); + expect(findOperationsSettings().exists()).toBe(true); + expect( + findOperationsSettings().findComponent(ProjectFeatureSetting).props('options'), + ).toEqual(expectedAccessLevel); + }); + }); + }); }); 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 108f816fe01..982c81b9272 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js @@ -38,7 +38,7 @@ describe('pages/shared/wikis/components/wiki_content', () => { const findGlAlert = () => wrapper.findComponent(GlAlert); const findGlSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); - const findContent = () => wrapper.find('[data-testid="wiki_page_content"]'); + const findContent = () => wrapper.find('[data-testid="wiki-page-content"]'); describe('when loading content', () => { beforeEach(() => { 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 204c48f8de1..b37d2f06191 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -39,7 +39,7 @@ describe('WikiForm', () => { const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link'); const findContentEditor = () => wrapper.findComponent(ContentEditor); const findClassicEditor = () => wrapper.findComponent(MarkdownField); - const findLocalStorageSync = () => wrapper.find(LocalStorageSync); + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); const setFormat = (value) => { const format = findFormat(); @@ -302,19 +302,15 @@ describe('WikiForm', () => { }); it.each` - format | enabled | action + format | exists | action ${'markdown'} | ${true} | ${'displays'} ${'rdoc'} | ${false} | ${'hides'} ${'asciidoc'} | ${false} | ${'hides'} ${'org'} | ${false} | ${'hides'} - `('$action toggle editing mode button when format is $format', async ({ format, enabled }) => { + `('$action toggle editing mode button when format is $format', async ({ format, exists }) => { await setFormat(format); - expect(findToggleEditingModeButton().exists()).toBe(enabled); - }); - - it('displays toggle editing mode button', () => { - expect(findToggleEditingModeButton().exists()).toBe(true); + expect(findToggleEditingModeButton().exists()).toBe(exists); }); describe('when content editor is not active', () => { @@ -351,15 +347,8 @@ describe('WikiForm', () => { }); describe('when content editor is active', () => { - let mockContentEditor; - beforeEach(() => { createWrapper(); - mockContentEditor = { - getSerializedContent: jest.fn(), - setSerializedContent: jest.fn(), - }; - findToggleEditingModeButton().vm.$emit('input', 'richText'); }); @@ -368,14 +357,7 @@ describe('WikiForm', () => { }); describe('when clicking the toggle editing mode button', () => { - const contentEditorFakeSerializedContent = 'fake content'; - beforeEach(async () => { - mockContentEditor.getSerializedContent.mockReturnValueOnce( - contentEditorFakeSerializedContent, - ); - - findContentEditor().vm.$emit('initialized', mockContentEditor); await findToggleEditingModeButton().vm.$emit('input', 'source'); await nextTick(); }); @@ -387,10 +369,6 @@ describe('WikiForm', () => { it('displays the classic editor', () => { expect(findClassicEditor().exists()).toBe(true); }); - - it('updates the classic editor content field', () => { - expect(findContent().element.value).toBe(contentEditorFakeSerializedContent); - }); }); describe('when content editor is loading', () => { @@ -480,8 +458,14 @@ describe('WikiForm', () => { }); describe('when wiki content is updated', () => { + const updatedMarkdown = 'hello **world**'; + beforeEach(() => { - findContentEditor().vm.$emit('change', { empty: false }); + findContentEditor().vm.$emit('change', { + empty: false, + changed: true, + markdown: updatedMarkdown, + }); }); it('sets before unload warning', () => { @@ -512,16 +496,8 @@ describe('WikiForm', () => { }); }); - it('updates content from content editor on form submit', async () => { - // old value - expect(findContent().element.value).toBe(' My page content '); - - // wait for content editor to load - await waitForPromises(); - - await triggerFormSubmit(); - - expect(findContent().element.value).toBe('hello **world**'); + it('sets content field to the content editor updated markdown', async () => { + expect(findContent().element.value).toBe(updatedMarkdown); }); }); }); diff --git a/spec/frontend/performance_bar/components/add_request_spec.js b/spec/frontend/performance_bar/components/add_request_spec.js index 627e004ce3e..5460feb66fe 100644 --- a/spec/frontend/performance_bar/components/add_request_spec.js +++ b/spec/frontend/performance_bar/components/add_request_spec.js @@ -51,7 +51,7 @@ describe('add request form', () => { }); it('emits an event to add the request', () => { - expect(wrapper.emitted()['add-request']).toBeTruthy(); + expect(wrapper.emitted()['add-request']).toHaveLength(1); expect(wrapper.emitted()['add-request'][0]).toEqual([ 'http://gitlab.example.com/users/root/calendar.json', ]); diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js index 2ae36740dfb..437d51e02ba 100644 --- a/spec/frontend/performance_bar/components/detailed_metric_spec.js +++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js @@ -257,7 +257,7 @@ describe('detailedMetric', () => { }); it('displays request warnings', () => { - expect(wrapper.find(RequestWarning).exists()).toBe(true); + expect(wrapper.findComponent(RequestWarning).exists()).toBe(true); }); it('can open and close traces', async () => { diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js index bff8fcda9b9..9cd5bb9e9a1 100644 --- a/spec/frontend/persistent_user_callout_spec.js +++ b/spec/frontend/persistent_user_callout_spec.js @@ -201,7 +201,7 @@ describe('PersistentUserCallout', () => { await waitForPromises(); - expect(window.location.assign).toBeCalledWith(href); + expect(window.location.assign).toHaveBeenCalledWith(href); expect(persistentUserCallout.container.remove).not.toHaveBeenCalled(); expect(mockAxios.history.post[0].data).toBe(JSON.stringify({ feature_name: featureName })); }); diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js index bec6c2a8d0c..0ee6da9d329 100644 --- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js +++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js @@ -152,7 +152,7 @@ describe('Pipeline Editor | Commit Form', () => { }); it('emits "scrolled-to-commit-form"', () => { - expect(wrapper.emitted()['scrolled-to-commit-form']).toBeTruthy(); + expect(wrapper.emitted()['scrolled-to-commit-form']).toHaveLength(1); }); }); }); diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js index 33c76309951..744b0378a75 100644 --- a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js +++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js @@ -224,7 +224,7 @@ describe('Pipeline Editor | Commit section', () => { }); it('emits a commit event with the right type, sourceBranch and targetBranch', () => { - expect(wrapper.emitted('commit')).toBeTruthy(); + expect(wrapper.emitted('commit')).toHaveLength(1); expect(wrapper.emitted('commit')[0]).toMatchObject([ { type: COMMIT_SUCCESS_WITH_REDIRECT, diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js index 7dbacad34bf..8f6f4d8cff9 100644 --- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js +++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js @@ -119,7 +119,7 @@ describe('Pipeline editor branch switcher', () => { }; const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); const findInfiniteScroll = () => wrapper.findComponent(GlInfiniteScroll); diff --git a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/pipeline_editor/components/file-tree/container_spec.js index 04a93e8db25..f79074f1e0f 100644 --- a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js +++ b/spec/frontend/pipeline_editor/components/file-tree/container_spec.js @@ -31,7 +31,7 @@ describe('Pipeline editor file nav', () => { const findTip = () => wrapper.findComponent(GlAlert); const findCurrentConfigFilename = () => wrapper.findByTestId('current-config-filename'); - const fileTreeItems = () => wrapper.findAll(PipelineEditorFileTreeItem); + const fileTreeItems = () => wrapper.findAllComponents(PipelineEditorFileTreeItem); afterEach(() => { localStorage.clear(); diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js new file mode 100644 index 00000000000..d40a9cc8100 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js @@ -0,0 +1,109 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue'; +import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; +import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql'; +import { PIPELINE_FAILURE } from '~/pipeline_editor/constants'; +import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data'; + +Vue.use(VueApollo); + +describe('Pipeline Status', () => { + let wrapper; + let mockApollo; + let mockLinkedPipelinesQuery; + + const createComponent = ({ hasStages = true, options } = {}) => { + wrapper = shallowMount(PipelineEditorMiniGraph, { + provide: { + dataMethod: 'graphql', + projectFullPath: mockProjectFullPath, + }, + propsData: { + pipeline: mockProjectPipeline({ hasStages }).pipeline, + }, + ...options, + }); + }; + + const createComponentWithApollo = (hasStages = true) => { + const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]]; + mockApollo = createMockApollo(handlers); + + createComponent({ + hasStages, + options: { + apolloProvider: mockApollo, + }, + }); + }; + + const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); + + beforeEach(() => { + mockLinkedPipelinesQuery = jest.fn(); + }); + + afterEach(() => { + mockLinkedPipelinesQuery.mockReset(); + wrapper.destroy(); + }); + + describe('when there are stages', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders pipeline mini graph', () => { + expect(findPipelineMiniGraph().exists()).toBe(true); + }); + }); + + describe('when there are no stages', () => { + beforeEach(() => { + createComponent({ hasStages: false }); + }); + + it('does not render pipeline mini graph', () => { + expect(findPipelineMiniGraph().exists()).toBe(false); + }); + }); + + describe('when querying upstream and downstream pipelines', () => { + describe('when query succeeds', () => { + beforeEach(() => { + mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines()); + createComponentWithApollo(); + }); + + it('should call the query with the correct variables', () => { + expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1); + expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({ + fullPath: mockProjectFullPath, + iid: mockProjectPipeline().pipeline.iid, + }); + }); + }); + + describe('when query fails', () => { + beforeEach(async () => { + mockLinkedPipelinesQuery.mockRejectedValue(new Error()); + createComponentWithApollo(); + await waitForPromises(); + }); + + it('should emit an error event when query fails', async () => { + expect(wrapper.emitted('showError')).toHaveLength(1); + expect(wrapper.emitted('showError')[0]).toEqual([ + { + type: PIPELINE_FAILURE, + reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError], + }, + ]); + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js index 93eb18c90cf..d40a9cc8100 100644 --- a/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js +++ b/spec/frontend/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import PipelineEditorMiniGraph from '~/pipeline_editor/components/header/pipeline_editor_mini_graph.vue'; -import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; +import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql'; import { PIPELINE_FAILURE } from '~/pipeline_editor/constants'; import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data'; diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js index 82ac390971d..7f89eda4dff 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js @@ -24,11 +24,11 @@ describe('CI Lint Results', () => { }); }; - const findTable = () => wrapper.find(GlTableLite); + const findTable = () => wrapper.findComponent(GlTableLite); const findByTestId = (selector) => () => wrapper.find(`[data-testid="ci-lint-${selector}"]`); const findAllByTestId = (selector) => () => wrapper.findAll(`[data-testid="ci-lint-${selector}"]`); - const findLinkToDoc = () => wrapper.find(GlLink); + const findLinkToDoc = () => wrapper.findComponent(GlLink); const findErrors = findByTestId('errors'); const findWarnings = findByTestId('warnings'); const findStatus = findByTestId('status'); diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js index 4b576508ee9..36052a2e16a 100644 --- a/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js +++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_warnings_spec.js @@ -17,9 +17,9 @@ describe('CI lint warnings', () => { }); }; - const findWarningAlert = () => wrapper.find(GlAlert); + const findWarningAlert = () => wrapper.findComponent(GlAlert); const findWarnings = () => wrapper.findAll('[data-testid="ci-lint-warning"]'); - const findWarningMessage = () => trimText(wrapper.find(GlSprintf).text()); + const findWarningMessage = () => trimText(wrapper.findComponent(GlSprintf).text()); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index 2f3e1b49b37..3b79739630d 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -256,7 +256,7 @@ describe('Pipeline editor tabs component', () => { ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false} ${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true} `( - 'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged ', + 'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged', ({ appStatus, editor, viz, validate, merged }) => { createComponent({ appStatus }); diff --git a/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js b/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js index 8d172a8462a..b86c82850c5 100644 --- a/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js +++ b/spec/frontend/pipeline_editor/components/popovers/walkthrough_popover_spec.js @@ -23,7 +23,7 @@ describe('WalkthroughPopover component', () => { }); it('emits "walkthrough-popover-cta-clicked" event', async () => { - expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toBeTruthy(); + expect(wrapper.emitted()['walkthrough-popover-cta-clicked']).toHaveLength(1); }); }); }); diff --git a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js index 3a40ce32a24..24f27e8c5fb 100644 --- a/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js +++ b/spec/frontend/pipeline_editor/components/ui/editor_tab_spec.js @@ -58,7 +58,7 @@ describe('~/pipeline_editor/components/ui/editor_tab.vue', () => { const findSlotComponent = () => wrapper.findComponent(MockSourceEditor); const findAlert = () => wrapper.findComponent(GlAlert); - const findBadges = () => wrapper.findAll(GlBadge); + const findBadges = () => wrapper.findAllComponents(GlBadge); beforeEach(() => { mockChildMounted = jest.fn(); diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index 0ce6cc3f2d4..1989f23a415 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -149,8 +149,7 @@ describe('Pipeline editor app component', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findEditorHome = () => wrapper.findComponent(PipelineEditorHome); const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState); - const findEmptyStateButton = () => - wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton); + const findEmptyStateButton = () => findEmptyState().findComponent(GlButton); const findValidationSegment = () => wrapper.findComponent(ValidationSegment); beforeEach(() => { diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js index 0cb7155c8c0..e317d1ddcc2 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js @@ -254,7 +254,7 @@ describe('Pipeline editor home wrapper', () => { expect(findPipelineEditorDrawer().props('isVisible')).toBe(true); - findPipelineEditorDrawer().find(GlDrawer).vm.$emit('close'); + findPipelineEditorDrawer().findComponent(GlDrawer).vm.$emit('close'); await nextTick(); expect(findPipelineEditorDrawer().props('isVisible')).toBe(false); diff --git a/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js new file mode 100644 index 00000000000..512b152f106 --- /dev/null +++ b/spec/frontend/pipeline_new/components/legacy_pipeline_new_form_spec.js @@ -0,0 +1,456 @@ +import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import { nextTick } from 'vue'; +import CreditCardValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue'; +import { TEST_HOST } from 'helpers/test_constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { redirectTo } from '~/lib/utils/url_utility'; +import LegacyPipelineNewForm from '~/pipeline_new/components/legacy_pipeline_new_form.vue'; +import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue'; +import { + mockQueryParams, + mockPostParams, + mockProjectId, + mockError, + mockRefs, + mockCreditCardValidationRequiredError, +} from '../mock_data'; + +jest.mock('~/lib/utils/url_utility', () => ({ + redirectTo: jest.fn(), +})); + +const projectRefsEndpoint = '/root/project/refs'; +const pipelinesPath = '/root/project/-/pipelines'; +const configVariablesPath = '/root/project/-/pipelines/config_variables'; +const newPipelinePostResponse = { id: 1 }; +const defaultBranch = 'main'; + +describe('Pipeline New Form', () => { + let wrapper; + let mock; + let dummySubmitEvent; + + const findForm = () => wrapper.findComponent(GlForm); + const findRefsDropdown = () => wrapper.findComponent(RefsDropdown); + const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]'); + const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]'); + const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]'); + const findDropdowns = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-type"]'); + const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]'); + const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]'); + const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]'); + const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]'); + const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf); + const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]'); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert); + const getFormPostParams = () => JSON.parse(mock.history.post[0].data); + + const selectBranch = (branch) => { + // Select a branch in the dropdown + findRefsDropdown().vm.$emit('input', { + shortName: branch, + fullName: `refs/heads/${branch}`, + }); + }; + + const createComponent = (props = {}, method = shallowMount) => { + wrapper = method(LegacyPipelineNewForm, { + provide: { + projectRefsEndpoint, + }, + propsData: { + projectId: mockProjectId, + pipelinesPath, + configVariablesPath, + defaultBranch, + refParam: defaultBranch, + settingsLink: '', + maxWarnings: 25, + ...props, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {}); + mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs); + + dummySubmitEvent = { + preventDefault: jest.fn(), + }; + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + mock.restore(); + }); + + describe('Form', () => { + beforeEach(async () => { + createComponent(mockQueryParams, mount); + + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse); + + await waitForPromises(); + }); + + it('displays the correct values for the provided query params', async () => { + expect(findDropdowns().at(0).props('text')).toBe('Variable'); + expect(findDropdowns().at(1).props('text')).toBe('File'); + expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' }); + expect(findVariableRows()).toHaveLength(3); + }); + + 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'); + }); + + 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(''); + expect(findDropdowns().at(2).props('text')).toBe('Variable'); + }); + + it('does not display remove icon for last row', () => { + expect(findRemoveIcons()).toHaveLength(2); + }); + + it('removes ci variable row on remove icon button click', async () => { + findRemoveIcons().at(1).trigger('click'); + + await nextTick(); + + expect(findVariableRows()).toHaveLength(2); + }); + + it('creates blank variable on input change event', async () => { + const input = findKeyInputs().at(2); + input.element.value = 'test_var_2'; + input.trigger('change'); + + await nextTick(); + + expect(findVariableRows()).toHaveLength(4); + expect(findKeyInputs().at(3).element.value).toBe(''); + expect(findValueInputs().at(3).element.value).toBe(''); + }); + }); + + describe('Pipeline creation', () => { + beforeEach(async () => { + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse); + + await waitForPromises(); + }); + + it('does not submit the native HTML form', async () => { + createComponent(); + + findForm().vm.$emit('submit', dummySubmitEvent); + + expect(dummySubmitEvent.preventDefault).toHaveBeenCalled(); + }); + + it('disables the submit button immediately after submitting', async () => { + createComponent(); + + expect(findSubmitButton().props('disabled')).toBe(false); + + findForm().vm.$emit('submit', dummySubmitEvent); + await waitForPromises(); + + expect(findSubmitButton().props('disabled')).toBe(true); + }); + + it('creates pipeline with full ref and variables', async () => { + createComponent(); + + findForm().vm.$emit('submit', dummySubmitEvent); + await waitForPromises(); + + expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`); + expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); + }); + + it('creates a pipeline with short ref and variables from the query params', async () => { + createComponent(mockQueryParams); + + await waitForPromises(); + + findForm().vm.$emit('submit', dummySubmitEvent); + + await waitForPromises(); + + expect(getFormPostParams()).toEqual(mockPostParams); + expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); + }); + }); + + describe('When the ref has been changed', () => { + beforeEach(async () => { + createComponent({}, mount); + + await waitForPromises(); + }); + it('variables persist between ref changes', async () => { + selectBranch('main'); + + await waitForPromises(); + + const mainInput = findKeyInputs().at(0); + mainInput.element.value = 'build_var'; + mainInput.trigger('change'); + + await nextTick(); + + selectBranch('branch-1'); + + await waitForPromises(); + + const branchOneInput = findKeyInputs().at(0); + branchOneInput.element.value = 'deploy_var'; + branchOneInput.trigger('change'); + + await nextTick(); + + selectBranch('main'); + + await waitForPromises(); + + expect(findKeyInputs().at(0).element.value).toBe('build_var'); + expect(findVariableRows().length).toBe(2); + + selectBranch('branch-1'); + + await waitForPromises(); + + expect(findKeyInputs().at(0).element.value).toBe('deploy_var'); + expect(findVariableRows().length).toBe(2); + }); + }); + + describe('when yml defines a variable', () => { + const mockYmlKey = 'yml_var'; + const mockYmlValue = 'yml_var_val'; + const mockYmlMultiLineValue = `A value + with multiple + lines`; + const mockYmlDesc = 'A var from yml.'; + + it('loading icon is shown when content is requested and hidden when received', async () => { + createComponent(mockQueryParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlValue, + description: mockYmlDesc, + }, + }); + + expect(findLoadingIcon().exists()).toBe(true); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + + it('multi-line strings are added to the value field without removing line breaks', async () => { + createComponent(mockQueryParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlMultiLineValue, + description: mockYmlDesc, + }, + }); + + await waitForPromises(); + + expect(findValueInputs().at(0).element.value).toBe(mockYmlMultiLineValue); + }); + + describe('with description', () => { + beforeEach(async () => { + createComponent(mockQueryParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlValue, + description: mockYmlDesc, + }, + }); + + await waitForPromises(); + }); + + it('displays all the variables', async () => { + expect(findVariableRows()).toHaveLength(4); + }); + + it('displays a variable from yml', () => { + expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey); + expect(findValueInputs().at(0).element.value).toBe(mockYmlValue); + }); + + it('displays a variable from provided query params', () => { + expect(findKeyInputs().at(1).element.value).toBe('test_var'); + expect(findValueInputs().at(1).element.value).toBe('test_var_val'); + }); + + it('adds a description to the first variable from yml', () => { + expect(findVariableRows().at(0).text()).toContain(mockYmlDesc); + }); + + it('removes the description when a variable key changes', async () => { + findKeyInputs().at(0).element.value = 'yml_var_modified'; + findKeyInputs().at(0).trigger('change'); + + await nextTick(); + + expect(findVariableRows().at(0).text()).not.toContain(mockYmlDesc); + }); + }); + + describe('without description', () => { + beforeEach(async () => { + createComponent(mockQueryParams, mount); + + mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { + [mockYmlKey]: { + value: mockYmlValue, + description: null, + }, + yml_var2: { + value: 'yml_var2_val', + }, + yml_var3: { + description: '', + }, + }); + + await waitForPromises(); + }); + + it('displays all the variables', async () => { + expect(findVariableRows()).toHaveLength(3); + }); + }); + }); + + describe('Form errors and warnings', () => { + beforeEach(() => { + createComponent(); + }); + + describe('when the refs cannot be loaded', () => { + beforeEach(() => { + mock + .onGet(projectRefsEndpoint, { params: { search: '' } }) + .reply(httpStatusCodes.INTERNAL_SERVER_ERROR); + + findRefsDropdown().vm.$emit('loadingError'); + }); + + it('shows both an error alert', () => { + expect(findErrorAlert().exists()).toBe(true); + expect(findWarningAlert().exists()).toBe(false); + }); + }); + + describe('when the error response can be handled', () => { + beforeEach(async () => { + mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError); + + findForm().vm.$emit('submit', dummySubmitEvent); + + await waitForPromises(); + }); + + it('shows both error and warning', () => { + expect(findErrorAlert().exists()).toBe(true); + expect(findWarningAlert().exists()).toBe(true); + }); + + it('shows the correct error', () => { + expect(findErrorAlert().text()).toBe(mockError.errors[0]); + }); + + it('shows the correct warning title', () => { + const { length } = mockError.warnings; + + expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`); + }); + + it('shows the correct amount of warnings', () => { + expect(findWarnings()).toHaveLength(mockError.warnings.length); + }); + + it('re-enables the submit button', () => { + expect(findSubmitButton().props('disabled')).toBe(false); + }); + + it('does not show the credit card validation required alert', () => { + expect(findCCAlert().exists()).toBe(false); + }); + + describe('when the error response is credit card validation required', () => { + beforeEach(async () => { + mock + .onPost(pipelinesPath) + .reply(httpStatusCodes.BAD_REQUEST, mockCreditCardValidationRequiredError); + + window.gon = { + subscriptions_url: TEST_HOST, + payment_form_url: TEST_HOST, + }; + + findForm().vm.$emit('submit', dummySubmitEvent); + + await waitForPromises(); + }); + + it('shows credit card validation required alert', () => { + expect(findErrorAlert().exists()).toBe(false); + expect(findCCAlert().exists()).toBe(true); + }); + + it('clears error and hides the alert on dismiss', async () => { + expect(findCCAlert().exists()).toBe(true); + expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]); + + findCCAlert().vm.$emit('dismiss'); + + await nextTick(); + + expect(findCCAlert().exists()).toBe(false); + expect(wrapper.vm.$data.error).toBe(null); + }); + }); + }); + + describe('when the error response cannot be handled', () => { + beforeEach(async () => { + mock + .onPost(pipelinesPath) + .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong'); + + findForm().vm.$emit('submit', dummySubmitEvent); + + await waitForPromises(); + }); + + it('re-enables the submit button', () => { + expect(findSubmitButton().props('disabled')).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js index 18dbd1ce9d6..5ce29bd6c5d 100644 --- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js +++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js @@ -34,7 +34,7 @@ describe('Pipeline New Form', () => { let mock; let dummySubmitEvent; - const findForm = () => wrapper.find(GlForm); + const findForm = () => wrapper.findComponent(GlForm); const findRefsDropdown = () => wrapper.findComponent(RefsDropdown); const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]'); const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]'); @@ -44,9 +44,9 @@ describe('Pipeline New Form', () => { const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]'); const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]'); const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]'); - const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf); + const findWarningAlertSummary = () => findWarningAlert().findComponent(GlSprintf); const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]'); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert); const getFormPostParams = () => JSON.parse(mock.history.post[0].data); @@ -329,6 +329,12 @@ describe('Pipeline New Form', () => { value: mockYmlValue, description: null, }, + yml_var2: { + value: 'yml_var2_val', + }, + yml_var3: { + description: '', + }, }); await waitForPromises(); diff --git a/spec/frontend/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/pipeline_new/components/refs_dropdown_spec.js index 826f2826d3c..8cba876c688 100644 --- a/spec/frontend/pipeline_new/components/refs_dropdown_spec.js +++ b/spec/frontend/pipeline_new/components/refs_dropdown_spec.js @@ -19,8 +19,8 @@ describe('Pipeline New Form', () => { let wrapper; let mock; - const findDropdown = () => wrapper.find(GlDropdown); - const findRefsDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findRefsDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); const createComponent = (props = {}, mountFn = shallowMount) => { diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js index c987accbb0d..d7e019c642e 100644 --- a/spec/frontend/pipeline_wizard/components/commit_spec.js +++ b/spec/frontend/pipeline_wizard/components/commit_spec.js @@ -174,7 +174,7 @@ describe('Pipeline Wizard - Commit Page', () => { }); it('will not emit a done event', () => { - expect(wrapper.emitted().done?.length).toBeFalsy(); + expect(wrapper.emitted().done?.length).toBeUndefined(); }); afterEach(() => { diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js index 540a08d2c7f..26e4b8eb0ea 100644 --- a/spec/frontend/pipeline_wizard/components/editor_spec.js +++ b/spec/frontend/pipeline_wizard/components/editor_spec.js @@ -11,7 +11,7 @@ describe('Pages Yaml Editor wrapper', () => { const wrapper = mount(YamlEditor, defaultOptions); it('editor is mounted', () => { - expect(wrapper.vm.editor).not.toBeFalsy(); + expect(wrapper.vm.editor).not.toBeUndefined(); expect(wrapper.find('.gl-source-editor').exists()).toBe(true); }); }); @@ -57,13 +57,4 @@ describe('Pages Yaml Editor wrapper', () => { }); }); }); - - describe('events', () => { - const wrapper = mount(YamlEditor, defaultOptions); - - it('emits touch if content is changed in editor', async () => { - await wrapper.vm.editor.setValue('foo: boo'); - expect(wrapper.emitted('touch')).toEqual([expect.any(Array)]); - }); - }); }); diff --git a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js index ea2448b1362..f288264a11e 100644 --- a/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js +++ b/spec/frontend/pipeline_wizard/components/input_wrapper_spec.js @@ -30,7 +30,7 @@ describe('Pipeline Wizard -- Input Wrapper', () => { beforeEach(() => { createComponent({}); - inputChild = wrapper.find(TextWidget); + inputChild = wrapper.findComponent(TextWidget); }); afterEach(() => { diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js index 357a9d21723..f064bf01c86 100644 --- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js +++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js @@ -2,6 +2,7 @@ import { Document, parseDocument } from 'yaml'; import { GlProgressBar } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import { mockTracking } from 'helpers/tracking_helper'; import PipelineWizardWrapper, { i18n } from '~/pipeline_wizard/components/wrapper.vue'; import WizardStep from '~/pipeline_wizard/components/step.vue'; import CommitStep from '~/pipeline_wizard/components/commit.vue'; @@ -19,9 +20,11 @@ describe('Pipeline Wizard - wrapper.vue', () => { const steps = parseDocument(stepsYaml).toJS(); const getAsYamlNode = (value) => new Document(value).contents; + const templateId = 'my-namespace/my-template'; const createComponent = (props = {}, mountFn = shallowMountExtended) => { wrapper = mountFn(PipelineWizardWrapper, { propsData: { + templateId, projectPath: '/user/repo', defaultBranch: 'main', filename: '.gitlab-ci.yml', @@ -311,4 +314,126 @@ describe('Pipeline Wizard - wrapper.vue', () => { }); }); }); + + describe('when commit step done', () => { + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('emits done', () => { + expect(wrapper.emitted('done')).toBeUndefined(); + + wrapper.findComponent(CommitStep).vm.$emit('done'); + + expect(wrapper.emitted('done')).toHaveLength(1); + }); + }); + + describe('tracking', () => { + let trackingSpy; + const trackingCategory = `pipeline_wizard:${templateId}`; + + const setUpTrackingSpy = () => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }; + + it('tracks next button click event', () => { + createComponent(); + setUpTrackingSpy(); + findFirstVisibleStep().vm.$emit('next'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', { + category: trackingCategory, + property: 'next', + label: 'pipeline_wizard_navigation', + extra: { + fromStep: 0, + toStep: 1, + }, + }); + }); + + it('tracks back button click event', () => { + createComponent(); + + // Navigate to step 1 without the spy set up + findFirstVisibleStep().vm.$emit('next'); + + // Now enable the tracking spy + setUpTrackingSpy(); + + findFirstVisibleStep().vm.$emit('back'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', { + category: trackingCategory, + property: 'back', + label: 'pipeline_wizard_navigation', + extra: { + fromStep: 1, + toStep: 0, + }, + }); + }); + + it('tracks back button click event on the commit step', () => { + createComponent(); + + // Navigate to step 2 without the spy set up + findFirstVisibleStep().vm.$emit('next'); + findFirstVisibleStep().vm.$emit('next'); + + // Now enable the tracking spy + setUpTrackingSpy(); + + wrapper.findComponent(CommitStep).vm.$emit('back'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', { + category: trackingCategory, + property: 'back', + label: 'pipeline_wizard_navigation', + extra: { + fromStep: 2, + toStep: 1, + }, + }); + }); + + it('tracks done event on the commit step', () => { + createComponent(); + + // Navigate to step 2 without the spy set up + findFirstVisibleStep().vm.$emit('next'); + findFirstVisibleStep().vm.$emit('next'); + + // Now enable the tracking spy + setUpTrackingSpy(); + + wrapper.findComponent(CommitStep).vm.$emit('done'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', { + category: trackingCategory, + label: 'pipeline_wizard_commit', + property: 'commit', + }); + }); + + it('tracks when editor emits touch events', () => { + createComponent(); + setUpTrackingSpy(); + + wrapper.findComponent(YamlEditor).vm.$emit('touch'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'edit', { + category: trackingCategory, + label: 'pipeline_wizard_editor_interaction', + extra: { + currentStep: 0, + }, + }); + }); + }); }); diff --git a/spec/frontend/pipeline_wizard/mock/yaml.js b/spec/frontend/pipeline_wizard/mock/yaml.js index e7087b59ce7..12b6f1052b2 100644 --- a/spec/frontend/pipeline_wizard/mock/yaml.js +++ b/spec/frontend/pipeline_wizard/mock/yaml.js @@ -71,6 +71,7 @@ bar: barVal `; export const fullTemplate = ` +id: test/full-template title: some title description: some description filename: foo.yml @@ -84,6 +85,7 @@ steps: `; export const fullTemplateWithoutFilename = ` +id: test/full-template-no-filename title: some title description: some description steps: diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js index 3f689ffdbc8..13234525159 100644 --- a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js +++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js @@ -59,6 +59,7 @@ describe('PipelineWizard', () => { defaultBranch, projectPath, filename: parseDocument(template).get('filename'), + templateId: parseDocument(template).get('id'), }), ); }); diff --git a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js index 212f8e19a6d..28a08b6da0f 100644 --- a/spec/frontend/pipelines/components/dag/dag_annotations_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_annotations_spec.js @@ -11,7 +11,7 @@ describe('The DAG annotations', () => { const getAllColorBlocks = () => wrapper.findAll('[data-testid="dag-color-block"]'); const getTextBlock = () => wrapper.find('[data-testid="dag-note-text"]'); const getAllTextBlocks = () => wrapper.findAll('[data-testid="dag-note-text"]'); - const getToggleButton = () => wrapper.find(GlButton); + const getToggleButton = () => wrapper.findComponent(GlButton); const createComponent = (propsData = {}, method = shallowMount) => { if (wrapper?.destroy) { diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js index d78df3eb35e..b0c26976c85 100644 --- a/spec/frontend/pipelines/components/dag/dag_spec.js +++ b/spec/frontend/pipelines/components/dag/dag_spec.js @@ -18,12 +18,12 @@ import { describe('Pipeline DAG graph wrapper', () => { let wrapper; - const getAlert = () => wrapper.find(GlAlert); - const getAllAlerts = () => wrapper.findAll(GlAlert); - const getGraph = () => wrapper.find(DagGraph); - const getNotes = () => wrapper.find(DagAnnotations); + const getAlert = () => wrapper.findComponent(GlAlert); + const getAllAlerts = () => wrapper.findAllComponents(GlAlert); + const getGraph = () => wrapper.findComponent(DagGraph); + const getNotes = () => wrapper.findComponent(DagAnnotations); const getErrorText = (type) => wrapper.vm.$options.errorTexts[type]; - const getEmptyState = () => wrapper.find(GlEmptyState); + const getEmptyState = () => wrapper.findComponent(GlEmptyState); const createComponent = ({ graphData = mockParsedGraphQLNodes, 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 new file mode 100644 index 00000000000..5ea57c51e70 --- /dev/null +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list_spec.js @@ -0,0 +1,176 @@ +import { mount } from '@vue/test-utils'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import LinkedPipelinesMiniList from '~/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue'; +import mockData from './linked_pipelines_mock_data'; + +describe('Linked pipeline mini list', () => { + let wrapper; + + const findCiIcon = () => wrapper.findComponent(CiIcon); + const findCiIcons = () => wrapper.findAllComponents(CiIcon); + const findLinkedPipelineCounter = () => wrapper.find('[data-testid="linked-pipeline-counter"]'); + const findLinkedPipelineMiniItem = () => + wrapper.find('[data-testid="linked-pipeline-mini-item"]'); + const findLinkedPipelineMiniItems = () => + wrapper.findAll('[data-testid="linked-pipeline-mini-item"]'); + const findLinkedPipelineMiniList = () => wrapper.findComponent(LinkedPipelinesMiniList); + + const createComponent = (props = {}) => { + wrapper = mount(LinkedPipelinesMiniList, { + directives: { + GlTooltip: createMockDirective(), + }, + propsData: { + ...props, + }, + }); + }; + + describe('when passed an upstream pipeline as prop', () => { + beforeEach(() => { + createComponent({ + triggeredBy: [mockData.triggered_by], + }); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should render one linked pipeline item', () => { + expect(findLinkedPipelineMiniItem().exists()).toBe(true); + }); + + it('should render a linked pipeline with the correct href', () => { + expect(findLinkedPipelineMiniItem().exists()).toBe(true); + + expect(findLinkedPipelineMiniItem().attributes('href')).toBe( + '/gitlab-org/gitlab-foss/-/pipelines/129', + ); + }); + + it('should render one ci status icon', () => { + expect(findCiIcon().exists()).toBe(true); + }); + + it('should render a borderless ci-icon', () => { + expect(findCiIcon().exists()).toBe(true); + + expect(findCiIcon().props('isBorderless')).toBe(true); + expect(findCiIcon().classes('borderless')).toBe(true); + }); + + it('should render a ci-icon with a custom border class', () => { + expect(findCiIcon().exists()).toBe(true); + + expect(findCiIcon().classes('gl-border')).toBe(true); + }); + + it('should render the correct ci status icon', () => { + expect(findCiIcon().classes('ci-status-icon-running')).toBe(true); + }); + + it('should have an activated tooltip', () => { + expect(findLinkedPipelineMiniItem().exists()).toBe(true); + const tooltip = getBinding(findLinkedPipelineMiniItem().element, 'gl-tooltip'); + + expect(tooltip.value.title).toBe('GitLabCE - running'); + }); + + it('should correctly set is-upstream', () => { + expect(findLinkedPipelineMiniList().exists()).toBe(true); + + expect(findLinkedPipelineMiniList().classes('is-upstream')).toBe(true); + }); + + it('should correctly compute shouldRenderCounter', () => { + expect(findLinkedPipelineMiniList().vm.shouldRenderCounter).toBe(false); + }); + + it('should not render the pipeline counter', () => { + expect(findLinkedPipelineCounter().exists()).toBe(false); + }); + }); + + describe('when passed downstream pipelines as props', () => { + beforeEach(() => { + createComponent({ + triggered: mockData.triggered, + pipelinePath: 'my/pipeline/path', + }); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should render three linked pipeline items', () => { + expect(findLinkedPipelineMiniItems().exists()).toBe(true); + expect(findLinkedPipelineMiniItems().length).toBe(3); + }); + + it('should render three ci status icons', () => { + expect(findCiIcons().exists()).toBe(true); + expect(findCiIcons().length).toBe(3); + }); + + it('should render the correct ci status icon', () => { + expect(findCiIcon().classes('ci-status-icon-running')).toBe(true); + }); + + it('should have an activated tooltip', () => { + expect(findLinkedPipelineMiniItem().exists()).toBe(true); + const tooltip = getBinding(findLinkedPipelineMiniItem().element, 'gl-tooltip'); + + expect(tooltip.value.title).toBe('GitLabCE - running'); + }); + + it('should correctly set is-downstream', () => { + expect(findLinkedPipelineMiniList().exists()).toBe(true); + + expect(findLinkedPipelineMiniList().classes('is-downstream')).toBe(true); + }); + + it('should render a borderless ci-icon', () => { + expect(findCiIcon().exists()).toBe(true); + + expect(findCiIcon().props('isBorderless')).toBe(true); + expect(findCiIcon().classes('borderless')).toBe(true); + }); + + it('should render a ci-icon with a custom border class', () => { + expect(findCiIcon().exists()).toBe(true); + + expect(findCiIcon().classes('gl-border')).toBe(true); + }); + + it('should render the pipeline counter', () => { + expect(findLinkedPipelineCounter().exists()).toBe(true); + }); + + it('should correctly compute shouldRenderCounter', () => { + expect(findLinkedPipelineMiniList().vm.shouldRenderCounter).toBe(true); + }); + + it('should correctly trim linkedPipelines', () => { + expect(findLinkedPipelineMiniList().props('triggered').length).toBe(6); + expect(findLinkedPipelineMiniList().vm.linkedPipelinesTrimmed.length).toBe(3); + }); + + it('should set the correct pipeline path', () => { + expect(findLinkedPipelineCounter().exists()).toBe(true); + + expect(findLinkedPipelineCounter().attributes('href')).toBe('my/pipeline/path'); + }); + + it('should render the correct counterTooltipText', () => { + expect(findLinkedPipelineCounter().exists()).toBe(true); + const tooltip = getBinding(findLinkedPipelineCounter().element, 'gl-tooltip'); + + expect(tooltip.value.title).toBe(findLinkedPipelineMiniList().vm.counterTooltipText); + }); + }); +}); diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js new file mode 100644 index 00000000000..117c7f2ae52 --- /dev/null +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/linked_pipelines_mock_data.js @@ -0,0 +1,407 @@ +export default { + triggered_by: { + id: 129, + active: true, + path: '/gitlab-org/gitlab-foss/-/pipelines/129', + project: { + name: 'GitLabCE', + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-foss/-/pipelines/129', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + }, + flags: { + latest: false, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: true, + }, + ref: { + name: '7-5-stable', + path: '/gitlab-org/gitlab-foss/commits/7-5-stable', + tag: false, + branch: true, + }, + commit: { + id: '23433d4d8b20d7e45c103d0b6048faad38a130ab', + short_id: '23433d4d', + title: 'Version 7.5.0.rc1', + created_at: '2014-11-17T15:44:14.000+01:00', + parent_ids: ['30ac909f30f58d319b42ed1537664483894b18cd'], + message: 'Version 7.5.0.rc1\n', + author_name: 'Jacob Vosmaer', + author_email: 'contact@jacobvosmaer.nl', + authored_date: '2014-11-17T15:44:14.000+01:00', + committer_name: 'Jacob Vosmaer', + committer_email: 'contact@jacobvosmaer.nl', + committed_date: '2014-11-17T15:44:14.000+01:00', + author_gravatar_url: + 'http://www.gravatar.com/avatar/e66d11c0eedf8c07b3b18fca46599807?s=80&d=identicon', + commit_url: + 'http://localhost:3000/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab', + commit_path: '/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab', + }, + retry_path: '/gitlab-org/gitlab-foss/-/pipelines/129/retry', + cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/129/cancel', + created_at: '2017-05-24T14:46:20.090Z', + updated_at: '2017-05-24T14:46:29.906Z', + }, + triggered: [ + { + id: 132, + active: true, + path: '/gitlab-org/gitlab-foss/-/pipelines/132', + project: { + name: 'GitLabCE', + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-foss/-/pipelines/132', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + }, + flags: { + latest: false, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: true, + }, + ref: { + name: 'crowd', + path: '/gitlab-org/gitlab-foss/commits/crowd', + tag: false, + branch: true, + }, + commit: { + id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', + short_id: 'b9d58c4c', + title: 'getting user keys publically through http without any authentication, the github…', + created_at: '2013-10-03T12:50:33.000+05:30', + parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], + message: + 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n', + author_name: 'devaroop', + author_email: 'devaroop123@yahoo.co.in', + authored_date: '2013-10-02T20:39:29.000+05:30', + committer_name: 'devaroop', + committer_email: 'devaroop123@yahoo.co.in', + committed_date: '2013-10-03T12:50:33.000+05:30', + author_gravatar_url: + 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', + commit_url: + 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', + commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', + }, + retry_path: '/gitlab-org/gitlab-foss/-/pipelines/132/retry', + cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/132/cancel', + created_at: '2017-05-24T14:46:24.644Z', + updated_at: '2017-05-24T14:48:55.226Z', + }, + { + id: 133, + active: true, + path: '/gitlab-org/gitlab-foss/-/pipelines/133', + project: { + name: 'GitLabCE', + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-foss/-/pipelines/133', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + }, + flags: { + latest: false, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: true, + }, + ref: { + name: 'crowd', + path: '/gitlab-org/gitlab-foss/commits/crowd', + tag: false, + branch: true, + }, + commit: { + id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b', + short_id: 'b6bd4856', + title: 'getting user keys publically through http without any authentication, the github…', + created_at: '2013-10-02T20:39:29.000+05:30', + parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], + message: + 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n', + author_name: 'devaroop', + author_email: 'devaroop123@yahoo.co.in', + authored_date: '2013-10-02T20:39:29.000+05:30', + committer_name: 'devaroop', + committer_email: 'devaroop123@yahoo.co.in', + committed_date: '2013-10-02T20:39:29.000+05:30', + author_gravatar_url: + 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', + commit_url: + 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', + commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', + }, + retry_path: '/gitlab-org/gitlab-foss/-/pipelines/133/retry', + cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/133/cancel', + created_at: '2017-05-24T14:46:24.648Z', + updated_at: '2017-05-24T14:48:59.673Z', + }, + { + id: 130, + active: true, + path: '/gitlab-org/gitlab-foss/-/pipelines/130', + project: { + name: 'GitLabCE', + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-foss/-/pipelines/130', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + }, + flags: { + latest: false, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: true, + }, + ref: { + name: 'crowd', + path: '/gitlab-org/gitlab-foss/commits/crowd', + tag: false, + branch: true, + }, + commit: { + id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f', + short_id: '6d7ced4a', + title: 'Whitespace fixes to patch', + created_at: '2013-10-08T13:53:22.000-05:00', + parent_ids: ['1875141a963a4238bda29011d8f7105839485253'], + message: 'Whitespace fixes to patch\n', + author_name: 'Dale Hamel', + author_email: 'dale.hamel@srvthe.net', + authored_date: '2013-10-08T13:53:22.000-05:00', + committer_name: 'Dale Hamel', + committer_email: 'dale.hamel@invenia.ca', + committed_date: '2013-10-08T13:53:22.000-05:00', + author_gravatar_url: + 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon', + commit_url: + 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', + commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', + }, + retry_path: '/gitlab-org/gitlab-foss/-/pipelines/130/retry', + cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/130/cancel', + created_at: '2017-05-24T14:46:24.630Z', + updated_at: '2017-05-24T14:49:45.091Z', + }, + { + id: 131, + active: true, + path: '/gitlab-org/gitlab-foss/-/pipelines/132', + project: { + name: 'GitLabCE', + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-foss/-/pipelines/132', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + }, + flags: { + latest: false, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: true, + }, + ref: { + name: 'crowd', + path: '/gitlab-org/gitlab-foss/commits/crowd', + tag: false, + branch: true, + }, + commit: { + id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', + short_id: 'b9d58c4c', + title: 'getting user keys publically through http without any authentication, the github…', + created_at: '2013-10-03T12:50:33.000+05:30', + parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], + message: + 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n', + author_name: 'devaroop', + author_email: 'devaroop123@yahoo.co.in', + authored_date: '2013-10-02T20:39:29.000+05:30', + committer_name: 'devaroop', + committer_email: 'devaroop123@yahoo.co.in', + committed_date: '2013-10-03T12:50:33.000+05:30', + author_gravatar_url: + 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', + commit_url: + 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', + commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', + }, + retry_path: '/gitlab-org/gitlab-foss/-/pipelines/132/retry', + cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/132/cancel', + created_at: '2017-05-24T14:46:24.644Z', + updated_at: '2017-05-24T14:48:55.226Z', + }, + { + id: 134, + active: true, + path: '/gitlab-org/gitlab-foss/-/pipelines/133', + project: { + name: 'GitLabCE', + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-foss/-/pipelines/133', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + }, + flags: { + latest: false, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: true, + }, + ref: { + name: 'crowd', + path: '/gitlab-org/gitlab-foss/commits/crowd', + tag: false, + branch: true, + }, + commit: { + id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b', + short_id: 'b6bd4856', + title: 'getting user keys publically through http without any authentication, the github…', + created_at: '2013-10-02T20:39:29.000+05:30', + parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], + message: + 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n', + author_name: 'devaroop', + author_email: 'devaroop123@yahoo.co.in', + authored_date: '2013-10-02T20:39:29.000+05:30', + committer_name: 'devaroop', + committer_email: 'devaroop123@yahoo.co.in', + committed_date: '2013-10-02T20:39:29.000+05:30', + author_gravatar_url: + 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', + commit_url: + 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', + commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', + }, + retry_path: '/gitlab-org/gitlab-foss/-/pipelines/133/retry', + cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/133/cancel', + created_at: '2017-05-24T14:46:24.648Z', + updated_at: '2017-05-24T14:48:59.673Z', + }, + { + id: 135, + active: true, + path: '/gitlab-org/gitlab-foss/-/pipelines/130', + project: { + name: 'GitLabCE', + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab-foss/-/pipelines/130', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + }, + flags: { + latest: false, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: true, + }, + ref: { + name: 'crowd', + path: '/gitlab-org/gitlab-foss/commits/crowd', + tag: false, + branch: true, + }, + commit: { + id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f', + short_id: '6d7ced4a', + title: 'Whitespace fixes to patch', + created_at: '2013-10-08T13:53:22.000-05:00', + parent_ids: ['1875141a963a4238bda29011d8f7105839485253'], + message: 'Whitespace fixes to patch\n', + author_name: 'Dale Hamel', + author_email: 'dale.hamel@srvthe.net', + authored_date: '2013-10-08T13:53:22.000-05:00', + committer_name: 'Dale Hamel', + committer_email: 'dale.hamel@invenia.ca', + committed_date: '2013-10-08T13:53:22.000-05:00', + author_gravatar_url: + 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon', + commit_url: + 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', + commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', + }, + retry_path: '/gitlab-org/gitlab-foss/-/pipelines/130/retry', + cancel_path: '/gitlab-org/gitlab-foss/-/pipelines/130/cancel', + created_at: '2017-05-24T14:46:24.630Z', + updated_at: '2017-05-24T14:49:45.091Z', + }, + ], +}; 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 new file mode 100644 index 00000000000..7fa8a18ea1f --- /dev/null +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_mini_graph_spec.js @@ -0,0 +1,149 @@ +import { mount } from '@vue/test-utils'; +import { pipelines } from 'test_fixtures/pipelines/pipelines.json'; +import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; +import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue'; +import mockLinkedPipelines from './linked_pipelines_mock_data'; + +const mockStages = pipelines[0].details.stages; + +describe('Pipeline Mini Graph', () => { + let wrapper; + + const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); + const findPipelineStages = () => wrapper.findComponent(PipelineStages); + + const findLinkedPipelineUpstream = () => + wrapper.findComponent('[data-testid="pipeline-mini-graph-upstream"]'); + const findLinkedPipelineDownstream = () => + wrapper.findComponent('[data-testid="pipeline-mini-graph-downstream"]'); + const findDownstreamArrowIcon = () => wrapper.find('[data-testid="downstream-arrow-icon"]'); + const findUpstreamArrowIcon = () => wrapper.find('[data-testid="upstream-arrow-icon"]'); + + const createComponent = (props = {}) => { + wrapper = mount(PipelineMiniGraph, { + propsData: { + stages: mockStages, + ...props, + }, + }); + }; + + describe('rendered state without upstream or downstream pipelines', () => { + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should render the pipeline stages', () => { + expect(findPipelineStages().exists()).toBe(true); + }); + + it('should have the correct props', () => { + expect(findPipelineMiniGraph().props()).toMatchObject({ + downstreamPipelines: [], + isMergeTrain: false, + pipelinePath: '', + stages: expect.any(Array), + stagesClass: '', + updateDropdown: false, + upstreamPipeline: undefined, + }); + }); + + it('should have no linked pipelines', () => { + expect(findLinkedPipelineDownstream().exists()).toBe(false); + expect(findLinkedPipelineUpstream().exists()).toBe(false); + }); + + it('should not render arrow icons', () => { + expect(findUpstreamArrowIcon().exists()).toBe(false); + expect(findDownstreamArrowIcon().exists()).toBe(false); + }); + + it('triggers events in "action request complete"', () => { + createComponent(); + + findPipelineMiniGraph(0).vm.$emit('pipelineActionRequestComplete'); + findPipelineMiniGraph(1).vm.$emit('pipelineActionRequestComplete'); + + expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2); + }); + }); + + describe('rendered state with upstream pipeline', () => { + beforeEach(() => { + createComponent({ + upstreamPipeline: mockLinkedPipelines.triggered_by, + }); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('should have the correct props', () => { + expect(findPipelineMiniGraph().props()).toMatchObject({ + downstreamPipelines: [], + isMergeTrain: false, + pipelinePath: '', + stages: expect.any(Array), + stagesClass: '', + updateDropdown: false, + upstreamPipeline: expect.any(Object), + }); + }); + + it('should render the upstream linked pipelines mini list only', () => { + expect(findLinkedPipelineUpstream().exists()).toBe(true); + expect(findLinkedPipelineDownstream().exists()).toBe(false); + }); + + it('should render an upstream arrow icon only', () => { + expect(findDownstreamArrowIcon().exists()).toBe(false); + expect(findUpstreamArrowIcon().exists()).toBe(true); + expect(findUpstreamArrowIcon().props('name')).toBe('long-arrow'); + }); + }); + + describe('rendered state with downstream pipelines', () => { + beforeEach(() => { + createComponent({ + downstreamPipelines: mockLinkedPipelines.triggered, + pipelinePath: 'my/pipeline/path', + }); + }); + + it('should have the correct props', () => { + expect(findPipelineMiniGraph().props()).toMatchObject({ + downstreamPipelines: expect.any(Array), + isMergeTrain: false, + pipelinePath: 'my/pipeline/path', + stages: expect.any(Array), + stagesClass: '', + updateDropdown: false, + upstreamPipeline: undefined, + }); + }); + + 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); + }); + + it('should render a downstream arrow icon only', () => { + expect(findUpstreamArrowIcon().exists()).toBe(false); + expect(findDownstreamArrowIcon().exists()).toBe(true); + expect(findDownstreamArrowIcon().props('name')).toBe('long-arrow'); + }); + }); +}); 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 new file mode 100644 index 00000000000..52b440f18bb --- /dev/null +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js @@ -0,0 +1,260 @@ +import { GlDropdown } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import axios from '~/lib/utils/axios_utils'; +import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue'; +import eventHub from '~/pipelines/event_hub'; +import waitForPromises from 'helpers/wait_for_promises'; +import { stageReply } from '../../mock_data'; + +const dropdownPath = 'path.json'; + +describe('Pipelines stage component', () => { + let wrapper; + let mock; + let glTooltipDirectiveMock; + + const createComponent = (props = {}) => { + glTooltipDirectiveMock = jest.fn(); + wrapper = mount(PipelineStage, { + attachTo: document.body, + directives: { + GlTooltip: glTooltipDirectiveMock, + }, + propsData: { + stage: { + status: { + group: 'success', + icon: 'status_success', + title: 'success', + }, + dropdown_path: dropdownPath, + }, + updateDropdown: false, + ...props, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + jest.spyOn(eventHub, '$emit'); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + eventHub.$emit.mockRestore(); + mock.restore(); + }); + + const findCiActionBtn = () => wrapper.find('.js-ci-action'); + const findCiIcon = () => wrapper.findComponent(CiIcon); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownToggle = () => wrapper.find('button.dropdown-toggle'); + const findDropdownMenu = () => + wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]'); + const findDropdownMenuTitle = () => + wrapper.find('[data-testid="pipeline-stage-dropdown-menu-title"]'); + const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]'); + const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]'); + + const openStageDropdown = async () => { + await findDropdownToggle().trigger('click'); + await waitForPromises(); + await nextTick(); + }; + + describe('loading state', () => { + beforeEach(async () => { + createComponent({ updateDropdown: true }); + + mock.onGet(dropdownPath).reply(200, stageReply); + + await openStageDropdown(); + }); + + it('displays loading state while jobs are being fetched', async () => { + jest.runOnlyPendingTimers(); + await nextTick(); + + expect(findLoadingState().exists()).toBe(true); + expect(findLoadingState().text()).toBe(PipelineStage.i18n.loadingText); + }); + + it('does not display loading state after jobs have been fetched', async () => { + await waitForPromises(); + + expect(findLoadingState().exists()).toBe(false); + }); + }); + + describe('default appearance', () => { + beforeEach(() => { + createComponent(); + }); + + it('sets up the tooltip to not have a show delay animation', () => { + expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true); + }); + + it('renders a dropdown with the status icon', () => { + expect(findDropdown().exists()).toBe(true); + expect(findDropdownToggle().exists()).toBe(true); + expect(findCiIcon().exists()).toBe(true); + }); + + it('renders a borderless ci-icon', () => { + expect(findCiIcon().exists()).toBe(true); + expect(findCiIcon().props('isBorderless')).toBe(true); + expect(findCiIcon().classes('borderless')).toBe(true); + }); + + it('renders a ci-icon with a custom border class', () => { + expect(findCiIcon().exists()).toBe(true); + expect(findCiIcon().classes('gl-border')).toBe(true); + }); + }); + + describe('when user opens dropdown and stage request is successful', () => { + beforeEach(async () => { + mock.onGet(dropdownPath).reply(200, stageReply); + createComponent(); + + await openStageDropdown(); + await jest.runAllTimers(); + await axios.waitForAll(); + }); + + it('renders the received data and emits the correct events', async () => { + expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); + expect(findDropdownMenuTitle().text()).toContain(stageReply.name); + expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); + expect(wrapper.emitted('miniGraphStageClick')).toEqual([[]]); + }); + + it('refreshes when updateDropdown is set to true', async () => { + expect(mock.history.get).toHaveLength(1); + + wrapper.setProps({ updateDropdown: true }); + await axios.waitForAll(); + + expect(mock.history.get).toHaveLength(2); + }); + }); + + describe('when user opens dropdown and stage request fails', () => { + it('should close the dropdown', async () => { + mock.onGet(dropdownPath).reply(500); + createComponent(); + + await openStageDropdown(); + await axios.waitForAll(); + await waitForPromises(); + + expect(findDropdown().classes('show')).toBe(false); + }); + }); + + describe('update endpoint correctly', () => { + beforeEach(async () => { + const copyStage = { ...stageReply }; + copyStage.latest_statuses[0].name = 'this is the updated content'; + mock.onGet('bar.json').reply(200, copyStage); + createComponent({ + stage: { + status: { + group: 'running', + icon: 'status_running', + title: 'running', + }, + dropdown_path: 'bar.json', + }, + }); + await axios.waitForAll(); + }); + + it('should update the stage to request the new endpoint provided', async () => { + await openStageDropdown(); + jest.runOnlyPendingTimers(); + await waitForPromises(); + + expect(findDropdownMenu().text()).toContain('this is the updated content'); + }); + }); + + describe('pipelineActionRequestComplete', () => { + beforeEach(async () => { + mock.onGet(dropdownPath).reply(200, stageReply); + mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); + + createComponent(); + await waitForPromises(); + await nextTick(); + }); + + const clickCiAction = async () => { + await openStageDropdown(); + jest.runOnlyPendingTimers(); + await waitForPromises(); + + await findCiActionBtn().trigger('click'); + }; + + it('closes dropdown when job item action is clicked', async () => { + const hidden = jest.fn(); + + wrapper.vm.$root.$on('bv::dropdown::hide', hidden); + + expect(hidden).toHaveBeenCalledTimes(0); + + await clickCiAction(); + await waitForPromises(); + + expect(hidden).toHaveBeenCalledTimes(1); + }); + + it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => { + await clickCiAction(); + await waitForPromises(); + + expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1); + }); + }); + + describe('With merge trains enabled', () => { + it('shows a warning on the dropdown', async () => { + mock.onGet(dropdownPath).reply(200, stageReply); + createComponent({ + isMergeTrain: true, + }); + + await openStageDropdown(); + jest.runOnlyPendingTimers(); + await waitForPromises(); + + const warning = findMergeTrainWarning(); + + expect(warning.text()).toBe('Merge train pipeline jobs can not be retried'); + }); + }); + + describe('With merge trains disabled', () => { + beforeEach(async () => { + mock.onGet(dropdownPath).reply(200, stageReply); + createComponent(); + + await openStageDropdown(); + await axios.waitForAll(); + }); + + it('does not show a warning on the dropdown', () => { + const warning = findMergeTrainWarning(); + + expect(warning.exists()).toBe(false); + }); + }); +}); 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 new file mode 100644 index 00000000000..bfb780d5d39 --- /dev/null +++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stages_spec.js @@ -0,0 +1,83 @@ +import { shallowMount } from '@vue/test-utils'; +import { pipelines } from 'test_fixtures/pipelines/pipelines.json'; +import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue'; +import PipelineStages from '~/pipelines/components/pipeline_mini_graph/pipeline_stages.vue'; + +const mockStages = pipelines[0].details.stages; + +describe('Pipeline Stages', () => { + let wrapper; + + const findPipelineStages = () => wrapper.findAllComponents(PipelineStage); + const findPipelineStagesAt = (i) => findPipelineStages().at(i); + + const createComponent = (props = {}) => { + wrapper = shallowMount(PipelineStages, { + propsData: { + stages: mockStages, + ...props, + }, + }); + }; + + it('renders stages', () => { + createComponent(); + + expect(findPipelineStages()).toHaveLength(mockStages.length); + }); + + it('renders stages with a custom class', () => { + createComponent({ stagesClass: 'my-class' }); + + expect(wrapper.findAll('.my-class')).toHaveLength(mockStages.length); + }); + + it('does not fail when stages are empty', () => { + createComponent({ stages: [] }); + + expect(wrapper.exists()).toBe(true); + expect(findPipelineStages()).toHaveLength(0); + }); + + it('triggers events in "action request complete" in stages', () => { + createComponent(); + + findPipelineStagesAt(0).vm.$emit('pipelineActionRequestComplete'); + findPipelineStagesAt(1).vm.$emit('pipelineActionRequestComplete'); + + expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2); + }); + + it('update dropdown is false by default', () => { + createComponent(); + + expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(false); + expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(false); + }); + + it('update dropdown is set to true', () => { + createComponent({ updateDropdown: true }); + + expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(true); + expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(true); + }); + + it('is merge train is false by default', () => { + createComponent(); + + expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(false); + expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(false); + }); + + it('is merge train is set to true', () => { + createComponent({ isMergeTrain: true }); + + 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/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js index f958f12acd4..ee3eaaf5ef3 100644 --- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js +++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js @@ -2,17 +2,19 @@ import { GlFilteredSearch } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { nextTick } from 'vue'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import Api from '~/api'; import axios from '~/lib/utils/axios_utils'; import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { TRACKING_CATEGORIES } from '~/pipelines/constants'; import { users, mockSearch, branches, tags } from '../mock_data'; describe('Pipelines filtered search', () => { let wrapper; let mock; - const findFilteredSearch = () => wrapper.find(GlFilteredSearch); + const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); const getSearchToken = (type) => findFilteredSearch() .props('availableTokens') @@ -177,4 +179,20 @@ describe('Pipelines filtered search', () => { expect(findFilteredSearch().props('value')).toHaveLength(expectedValueProp.length); }); }); + + describe('tracking', () => { + afterEach(() => { + unmockTracking(); + }); + + it('tracks filtered search click', () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + + findFilteredSearch().vm.$emit('submit', mockSearch); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filtered_search', { + label: TRACKING_CATEGORIES.search, + }); + }); + }); }); diff --git a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js deleted file mode 100644 index 1cb43c199aa..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/pipeline_mini_graph_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { pipelines } from 'test_fixtures/pipelines/pipelines.json'; -import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; -import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; - -const mockStages = pipelines[0].details.stages; - -describe('Pipeline Mini Graph', () => { - let wrapper; - - const findPipelineStages = () => wrapper.findAll(PipelineStage); - const findPipelineStagesAt = (i) => findPipelineStages().at(i); - - const createComponent = (props = {}) => { - wrapper = shallowMount(PipelineMiniGraph, { - propsData: { - stages: mockStages, - ...props, - }, - }); - }; - - it('renders stages', () => { - createComponent(); - - expect(findPipelineStages()).toHaveLength(mockStages.length); - }); - - it('renders stages with a custom class', () => { - createComponent({ stagesClass: 'my-class' }); - - expect(wrapper.findAll('.my-class')).toHaveLength(mockStages.length); - }); - - it('does not fail when stages are empty', () => { - createComponent({ stages: [] }); - - expect(wrapper.exists()).toBe(true); - expect(findPipelineStages()).toHaveLength(0); - }); - - it('triggers events in "action request complete" in stages', () => { - createComponent(); - - findPipelineStagesAt(0).vm.$emit('pipelineActionRequestComplete'); - findPipelineStagesAt(1).vm.$emit('pipelineActionRequestComplete'); - - expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(2); - }); - - it('update dropdown is false by default', () => { - createComponent(); - - expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(false); - expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(false); - }); - - it('update dropdown is set to true', () => { - createComponent({ updateDropdown: true }); - - expect(findPipelineStagesAt(0).props('updateDropdown')).toBe(true); - expect(findPipelineStagesAt(1).props('updateDropdown')).toBe(true); - }); - - it('is merge train is false by default', () => { - createComponent(); - - expect(findPipelineStagesAt(0).props('isMergeTrain')).toBe(false); - expect(findPipelineStagesAt(1).props('isMergeTrain')).toBe(false); - }); - - it('is merge train is set to true', () => { - createComponent({ isMergeTrain: true }); - - 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/pipelines_list/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js deleted file mode 100644 index e712cdeaea2..00000000000 --- a/spec/frontend/pipelines/components/pipelines_list/pipeline_stage_spec.js +++ /dev/null @@ -1,259 +0,0 @@ -import { GlDropdown } from '@gitlab/ui'; -import { nextTick } from 'vue'; -import { mount } from '@vue/test-utils'; -import MockAdapter from 'axios-mock-adapter'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import axios from '~/lib/utils/axios_utils'; -import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; -import eventHub from '~/pipelines/event_hub'; -import waitForPromises from 'helpers/wait_for_promises'; -import { stageReply } from '../../mock_data'; - -const dropdownPath = 'path.json'; - -describe('Pipelines stage component', () => { - let wrapper; - let mock; - let glTooltipDirectiveMock; - - const createComponent = (props = {}) => { - glTooltipDirectiveMock = jest.fn(); - wrapper = mount(PipelineStage, { - attachTo: document.body, - directives: { - GlTooltip: glTooltipDirectiveMock, - }, - propsData: { - stage: { - status: { - group: 'success', - icon: 'status_success', - title: 'success', - }, - dropdown_path: dropdownPath, - }, - updateDropdown: false, - ...props, - }, - }); - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - jest.spyOn(eventHub, '$emit'); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - - eventHub.$emit.mockRestore(); - mock.restore(); - }); - - const findCiActionBtn = () => wrapper.find('.js-ci-action'); - const findCiIcon = () => wrapper.findComponent(CiIcon); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findDropdownToggle = () => wrapper.find('button.dropdown-toggle'); - const findDropdownMenu = () => - wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]'); - const findDropdownMenuTitle = () => - wrapper.find('[data-testid="pipeline-stage-dropdown-menu-title"]'); - const findMergeTrainWarning = () => wrapper.find('[data-testid="warning-message-merge-trains"]'); - const findLoadingState = () => wrapper.find('[data-testid="pipeline-stage-loading-state"]'); - - const openStageDropdown = async () => { - await findDropdownToggle().trigger('click'); - await waitForPromises(); - await nextTick(); - }; - - describe('loading state', () => { - beforeEach(async () => { - createComponent({ updateDropdown: true }); - - mock.onGet(dropdownPath).reply(200, stageReply); - - await openStageDropdown(); - }); - - it('displays loading state while jobs are being fetched', async () => { - jest.runOnlyPendingTimers(); - await nextTick(); - - expect(findLoadingState().exists()).toBe(true); - expect(findLoadingState().text()).toBe(PipelineStage.i18n.loadingText); - }); - - it('does not display loading state after jobs have been fetched', async () => { - await waitForPromises(); - - expect(findLoadingState().exists()).toBe(false); - }); - }); - - describe('default appearance', () => { - beforeEach(() => { - createComponent(); - }); - - it('sets up the tooltip to not have a show delay animation', () => { - expect(glTooltipDirectiveMock.mock.calls[0][1].modifiers.ds0).toBe(true); - }); - - it('renders a dropdown with the status icon', () => { - expect(findDropdown().exists()).toBe(true); - expect(findDropdownToggle().exists()).toBe(true); - expect(findCiIcon().exists()).toBe(true); - }); - - it('renders a borderless ci-icon', () => { - expect(findCiIcon().exists()).toBe(true); - expect(findCiIcon().props('isBorderless')).toBe(true); - expect(findCiIcon().classes('borderless')).toBe(true); - }); - - it('renders a ci-icon with a custom border class', () => { - expect(findCiIcon().exists()).toBe(true); - expect(findCiIcon().classes('gl-border')).toBe(true); - }); - }); - - describe('when user opens dropdown and stage request is successful', () => { - beforeEach(async () => { - mock.onGet(dropdownPath).reply(200, stageReply); - createComponent(); - - await openStageDropdown(); - await jest.runAllTimers(); - await axios.waitForAll(); - }); - - it('renders the received data and emit `clickedDropdown` event', async () => { - expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name); - expect(findDropdownMenuTitle().text()).toContain(stageReply.name); - expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown'); - }); - - it('refreshes when updateDropdown is set to true', async () => { - expect(mock.history.get).toHaveLength(1); - - wrapper.setProps({ updateDropdown: true }); - await axios.waitForAll(); - - expect(mock.history.get).toHaveLength(2); - }); - }); - - describe('when user opens dropdown and stage request fails', () => { - it('should close the dropdown', async () => { - mock.onGet(dropdownPath).reply(500); - createComponent(); - - await openStageDropdown(); - await axios.waitForAll(); - await waitForPromises(); - - expect(findDropdown().classes('show')).toBe(false); - }); - }); - - describe('update endpoint correctly', () => { - beforeEach(async () => { - const copyStage = { ...stageReply }; - copyStage.latest_statuses[0].name = 'this is the updated content'; - mock.onGet('bar.json').reply(200, copyStage); - createComponent({ - stage: { - status: { - group: 'running', - icon: 'status_running', - title: 'running', - }, - dropdown_path: 'bar.json', - }, - }); - await axios.waitForAll(); - }); - - it('should update the stage to request the new endpoint provided', async () => { - await openStageDropdown(); - jest.runOnlyPendingTimers(); - await waitForPromises(); - - expect(findDropdownMenu().text()).toContain('this is the updated content'); - }); - }); - - describe('pipelineActionRequestComplete', () => { - beforeEach(async () => { - mock.onGet(dropdownPath).reply(200, stageReply); - mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200); - - createComponent(); - await waitForPromises(); - await nextTick(); - }); - - const clickCiAction = async () => { - await openStageDropdown(); - jest.runOnlyPendingTimers(); - await waitForPromises(); - - await findCiActionBtn().trigger('click'); - }; - - it('closes dropdown when job item action is clicked', async () => { - const hidden = jest.fn(); - - wrapper.vm.$root.$on('bv::dropdown::hide', hidden); - - expect(hidden).toHaveBeenCalledTimes(0); - - await clickCiAction(); - await waitForPromises(); - - expect(hidden).toHaveBeenCalledTimes(1); - }); - - it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => { - await clickCiAction(); - await waitForPromises(); - - expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1); - }); - }); - - describe('With merge trains enabled', () => { - it('shows a warning on the dropdown', async () => { - mock.onGet(dropdownPath).reply(200, stageReply); - createComponent({ - isMergeTrain: true, - }); - - await openStageDropdown(); - jest.runOnlyPendingTimers(); - await waitForPromises(); - - const warning = findMergeTrainWarning(); - - expect(warning.text()).toBe('Merge train pipeline jobs can not be retried'); - }); - }); - - describe('With merge trains disabled', () => { - beforeEach(async () => { - mock.onGet(dropdownPath).reply(200, stageReply); - createComponent(); - - await openStageDropdown(); - await axios.waitForAll(); - }); - - it('does not show a warning on the dropdown', () => { - const warning = findMergeTrainWarning(); - - expect(warning.exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js index 6e5aa572ec0..a823e029281 100644 --- a/spec/frontend/pipelines/graph/action_component_spec.js +++ b/spec/frontend/pipelines/graph/action_component_spec.js @@ -9,7 +9,7 @@ import ActionComponent from '~/pipelines/components/jobs_shared/action_component describe('pipeline graph action component', () => { let wrapper; let mock; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]'); beforeEach(() => { diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js index 4b2b61c8edd..2abb5f7dc58 100644 --- a/spec/frontend/pipelines/graph/graph_component_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_spec.js @@ -15,9 +15,9 @@ import { describe('graph component', () => { let wrapper; - const findLinkedColumns = () => wrapper.findAll(LinkedPipelinesColumn); - const findLinksLayer = () => wrapper.find(LinksLayer); - const findStageColumns = () => wrapper.findAll(StageColumnComponent); + const findLinkedColumns = () => wrapper.findAllComponents(LinkedPipelinesColumn); + const findLinksLayer = () => wrapper.findComponent(LinksLayer); + const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent); const findStageNameInJob = () => wrapper.find('[data-testid="stage-name-in-job"]'); const defaultProps = { @@ -107,7 +107,7 @@ describe('graph component', () => { }); it('dims unrelated jobs', () => { - const unrelatedJob = wrapper.find(JobItem); + const unrelatedJob = wrapper.findComponent(JobItem); expect(findLinksLayer().emitted().highlightedJobsChange).toHaveLength(1); expect(unrelatedJob.classes('gl-opacity-3')).toBe(true); }); diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js index 3eaf06e0656..587a3c67168 100644 --- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js @@ -30,16 +30,10 @@ import * as Api from '~/pipelines/components/graph_shared/api'; import LinksLayer from '~/pipelines/components/graph_shared/links_layer.vue'; import * as parsingUtils from '~/pipelines/components/parsing_utils'; import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; -import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql'; import * as sentryUtils from '~/pipelines/utils'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import { mockRunningPipelineHeaderData } from '../mock_data'; -import { - mapCallouts, - mockCalloutsResponse, - mockPipelineResponse, - mockPerformanceInsightsResponse, -} from './mock_data'; +import { mapCallouts, mockCalloutsResponse, mockPipelineResponse } from './mock_data'; const defaultProvide = { graphqlResourceEtag: 'frog/amphibirama/etag/', @@ -57,11 +51,11 @@ describe('Pipeline graph wrapper', () => { const getDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]'); const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const getLinksLayer = () => wrapper.findComponent(LinksLayer); - const getGraph = () => wrapper.find(PipelineGraph); + const getGraph = () => wrapper.findComponent(PipelineGraph); const getStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]'); const getAllStageColumnGroupsInColumn = () => - wrapper.find(StageColumnComponent).findAll('[data-testid="stage-column-group"]'); - const getViewSelector = () => wrapper.find(GraphViewSelector); + wrapper.findComponent(StageColumnComponent).findAll('[data-testid="stage-column-group"]'); + const getViewSelector = () => wrapper.findComponent(GraphViewSelector); const getViewSelectorTrip = () => getViewSelector().findComponent(GlAlert); const getLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); @@ -95,15 +89,11 @@ describe('Pipeline graph wrapper', () => { const callouts = mapCallouts(calloutsList); const getUserCalloutsHandler = jest.fn().mockResolvedValue(mockCalloutsResponse(callouts)); const getPipelineHeaderDataHandler = jest.fn().mockResolvedValue(mockRunningPipelineHeaderData); - const getPerformanceInsightsHandler = jest - .fn() - .mockResolvedValue(mockPerformanceInsightsResponse); const requestHandlers = [ [getPipelineHeaderData, getPipelineHeaderDataHandler], [getPipelineDetails, getPipelineDetailsHandler], [getUserCallouts, getUserCalloutsHandler], - [getPerformanceInsights, getPerformanceInsightsHandler], ]; const apolloProvider = createMockApollo(requestHandlers); @@ -309,7 +299,7 @@ describe('Pipeline graph wrapper', () => { const groupsInFirstColumn = mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes.length; expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn); - expect(getStageColumnTitle().text()).toBe('Build'); + expect(getStageColumnTitle().text()).toBe('build'); await getViewSelector().vm.$emit('updateViewType', LAYER_VIEW); expect(getAllStageColumnGroupsInColumn()).toHaveLength(groupsInFirstColumn + 1); expect(getStageColumnTitle().text()).toBe(''); @@ -418,7 +408,7 @@ describe('Pipeline graph wrapper', () => { it('reads the view type from localStorage when available', () => { const viewSelectorNeedsSegment = wrapper - .find(GlButtonGroup) + .findComponent(GlButtonGroup) .findAllComponents(GlButton) .at(1); expect(viewSelectorNeedsSegment.classes()).toContain('selected'); @@ -564,7 +554,7 @@ describe('Pipeline graph wrapper', () => { mock.restore(); }); - it('it calls reportPerformance with expected arguments', () => { + it('calls reportPerformance with expected arguments', () => { expect(markAndMeasure).toHaveBeenCalled(); expect(reportPerformance).toHaveBeenCalled(); expect(reportPerformance).toHaveBeenCalledWith(metricsPath, metricsData); diff --git a/spec/frontend/pipelines/graph/graph_view_selector_spec.js b/spec/frontend/pipelines/graph/graph_view_selector_spec.js index 1397500bdc7..43587bebedf 100644 --- a/spec/frontend/pipelines/graph/graph_view_selector_spec.js +++ b/spec/frontend/pipelines/graph/graph_view_selector_spec.js @@ -1,34 +1,23 @@ import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants'; import GraphViewSelector from '~/pipelines/components/graph/graph_view_selector.vue'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql'; -import { mockPerformanceInsightsResponse } from './mock_data'; - -Vue.use(VueApollo); describe('the graph view selector component', () => { let wrapper; - let trackingSpy; const findDependenciesToggle = () => wrapper.find('[data-testid="show-links-toggle"]'); const findViewTypeSelector = () => wrapper.findComponent(GlButtonGroup); const findStageViewButton = () => findViewTypeSelector().findAllComponents(GlButton).at(0); const findLayerViewButton = () => findViewTypeSelector().findAllComponents(GlButton).at(1); const findSwitcherLoader = () => wrapper.find('[data-testid="switcher-loading-state"]'); - const findToggleLoader = () => findDependenciesToggle().find(GlLoadingIcon); + const findToggleLoader = () => findDependenciesToggle().findComponent(GlLoadingIcon); const findHoverTip = () => wrapper.findComponent(GlAlert); - const findPipelineInsightsBtn = () => wrapper.find('[data-testid="pipeline-insights-btn"]'); const defaultProps = { showLinks: false, tipPreviouslyDismissed: false, type: STAGE_VIEW, - isPipelineComplete: true, }; const defaultData = { @@ -38,14 +27,6 @@ describe('the graph view selector component', () => { showLinksActive: false, }; - const getPerformanceInsightsHandler = jest - .fn() - .mockResolvedValue(mockPerformanceInsightsResponse); - - const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]]; - - const apolloProvider = createMockApollo(requestHandlers); - const createComponent = ({ data = {}, mountFn = shallowMount, props = {} } = {}) => { wrapper = mountFn(GraphViewSelector, { propsData: { @@ -58,7 +39,6 @@ describe('the graph view selector component', () => { ...data, }; }, - apolloProvider, }); }; @@ -222,44 +202,5 @@ describe('the graph view selector component', () => { expect(findHoverTip().exists()).toBe(false); }); }); - - describe('pipeline insights', () => { - it.each` - isPipelineComplete | shouldShow - ${true} | ${true} - ${false} | ${false} - `( - 'button should display $shouldShow if isPipelineComplete is $isPipelineComplete ', - ({ isPipelineComplete, shouldShow }) => { - createComponent({ - props: { - isPipelineComplete, - }, - }); - - expect(findPipelineInsightsBtn().exists()).toBe(shouldShow); - }, - ); - }); - - describe('tracking', () => { - beforeEach(() => { - createComponent(); - - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); - - afterEach(() => { - unmockTracking(); - }); - - it('tracks performance insights button click', () => { - findPipelineInsightsBtn().vm.$emit('click'); - - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_insights_button', { - label: 'performance_insights', - }); - }); - }); }); }); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index 4f0da09fec6..05776ec0706 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -59,7 +59,7 @@ describe('pipeline graph job item', () => { }); }); - it('it should render status and name', () => { + it('should render status and name', () => { expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(wrapper.find('a').exists()).toBe(false); @@ -72,7 +72,7 @@ describe('pipeline graph job item', () => { }); describe('action icon', () => { - it('it should render the action icon', () => { + it('should render the action icon', () => { createWrapper({ job: mockJob }); const actionComponent = findActionComponent(); @@ -82,7 +82,7 @@ describe('pipeline graph job item', () => { expect(actionComponent.attributes('disabled')).not.toBe('disabled'); }); - it('it should render disabled action icon when user cannot run the action', () => { + it('should render disabled action icon when user cannot run the action', () => { createWrapper({ job: mockJobWithUnauthorizedAction }); const actionComponent = findActionComponent(); diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js index d3008c046e8..ec432e98fdf 100644 --- a/spec/frontend/pipelines/graph/job_name_component_spec.js +++ b/spec/frontend/pipelines/graph/job_name_component_spec.js @@ -24,7 +24,7 @@ describe('job name component', () => { }); it('should render an icon with the provided status', () => { - expect(wrapper.find(ciIcon).exists()).toBe(true); + expect(wrapper.findComponent(ciIcon).exists()).toBe(true); expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index 7d1e4774a24..399d52c3dff 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -36,13 +36,13 @@ describe('Linked pipeline', () => { type: UPSTREAM, }; - const findButton = () => wrapper.find(GlButton); + const findButton = () => wrapper.findComponent(GlButton); const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline'); const findCardTooltip = () => wrapper.findComponent(GlTooltip); const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title'); const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button'); - const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' }); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findLinkedPipeline = () => wrapper.findComponent({ ref: 'linkedPipeline' }); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label'); const findPipelineLink = () => wrapper.findByTestId('pipelineLink'); const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline'); @@ -80,7 +80,7 @@ describe('Linked pipeline', () => { }); it('should render an svg within the status container', () => { - const pipelineStatusElement = wrapper.find(CiStatus); + const pipelineStatusElement = wrapper.findComponent(CiStatus); expect(pipelineStatusElement.find('svg').exists()).toBe(true); }); @@ -90,7 +90,7 @@ describe('Linked pipeline', () => { }); it('should have a ci-status child component', () => { - expect(wrapper.find(CiStatus).exists()).toBe(true); + expect(wrapper.findComponent(CiStatus).exists()).toBe(true); }); it('should render the pipeline id', () => { @@ -214,7 +214,7 @@ describe('Linked pipeline', () => { await findRetryButton().trigger('click'); }); - it('calls the retry mutation ', () => { + it('calls the retry mutation', () => { expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1); expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ mutation: RetryPipelineMutation, @@ -255,7 +255,7 @@ describe('Linked pipeline', () => { createWrapper({ propsData: cancelablePipeline }); }); - it('shows only the cancel button ', () => { + it('shows only the cancel button', () => { expect(findCancelButton().exists()).toBe(true); expect(findRetryButton().exists()).toBe(false); }); @@ -375,7 +375,7 @@ describe('Linked pipeline', () => { ${'mouseover'} | ${'mouseout'} ${'focus'} | ${'blur'} `( - 'applies the class on $activateEventName and removes it on $deactivateEventName ', + 'applies the class on $activateEventName and removes it on $deactivateEventName', async ({ activateEventName, deactivateEventName }) => { const shadowClass = 'gl-shadow-none!'; diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js index 46000711110..63e2d8707ea 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js @@ -38,8 +38,8 @@ describe('Linked Pipelines Column', () => { let wrapper; const findLinkedColumnTitle = () => wrapper.find('[data-testid="linked-column-title"]'); - const findLinkedPipelineElements = () => wrapper.findAll(LinkedPipeline); - const findPipelineGraph = () => wrapper.find(PipelineGraph); + const findLinkedPipelineElements = () => wrapper.findAllComponents(LinkedPipeline); + const findPipelineGraph = () => wrapper.findComponent(PipelineGraph); const findExpandButton = () => wrapper.find('[data-testid="expand-pipeline-button"]'); Vue.use(VueApollo); diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js index 959bbcefc98..6124d67af09 100644 --- a/spec/frontend/pipelines/graph/mock_data.js +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -1038,245 +1038,3 @@ export const triggerJob = { action: null, }, }; - -export const mockPerformanceInsightsResponse = { - data: { - project: { - __typename: 'Project', - id: 'gid://gitlab/Project/20', - pipeline: { - __typename: 'Pipeline', - id: 'gid://gitlab/Ci::Pipeline/97', - jobs: { - __typename: 'CiJobConnection', - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - }, - nodes: [ - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Bridge/2502', - duration: null, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2502-2502', - detailsPath: '/root/lots-of-jobs-project/-/pipelines/98', - }, - name: 'trigger_job', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/303', - name: 'deploy', - }, - startedAt: null, - queuedDuration: 424850.376278, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2501', - duration: 10, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2501-2501', - detailsPath: '/root/ci-project/-/jobs/2501', - }, - name: 'artifact_job', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/303', - name: 'deploy', - }, - startedAt: '2022-07-01T16:31:41Z', - queuedDuration: 2.621553, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2500', - duration: 4, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2500-2500', - detailsPath: '/root/ci-project/-/jobs/2500', - }, - name: 'coverage_job', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/302', - name: 'test', - }, - startedAt: '2022-07-01T16:31:33Z', - queuedDuration: 14.388869, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2499', - duration: 4, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2499-2499', - detailsPath: '/root/ci-project/-/jobs/2499', - }, - name: 'test_job_two', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/302', - name: 'test', - }, - startedAt: '2022-07-01T16:31:28Z', - queuedDuration: 15.792664, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2498', - duration: 4, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2498-2498', - detailsPath: '/root/ci-project/-/jobs/2498', - }, - name: 'test_job_one', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/302', - name: 'test', - }, - startedAt: '2022-07-01T16:31:17Z', - queuedDuration: 8.317072, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2497', - duration: 5, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'failed-2497-2497', - detailsPath: '/root/ci-project/-/jobs/2497', - }, - name: 'allow_failure_test_job', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/302', - name: 'test', - }, - startedAt: '2022-07-01T16:31:22Z', - queuedDuration: 3.547553, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2496', - duration: null, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'manual-2496-2496', - detailsPath: '/root/ci-project/-/jobs/2496', - }, - name: 'test_manual_job', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/302', - name: 'test', - }, - startedAt: null, - queuedDuration: null, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2495', - duration: 5, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2495-2495', - detailsPath: '/root/ci-project/-/jobs/2495', - }, - name: 'large_log_output', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/301', - name: 'build', - }, - startedAt: '2022-07-01T16:31:11Z', - queuedDuration: 79.128625, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2494', - duration: 5, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2494-2494', - detailsPath: '/root/ci-project/-/jobs/2494', - }, - name: 'build_job', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/301', - name: 'build', - }, - startedAt: '2022-07-01T16:31:05Z', - queuedDuration: 73.286895, - }, - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Build/2493', - duration: 16, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2493-2493', - detailsPath: '/root/ci-project/-/jobs/2493', - }, - name: 'wait_job', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/301', - name: 'build', - }, - startedAt: '2022-07-01T16:30:48Z', - queuedDuration: 56.258856, - }, - ], - }, - }, - }, - }, -}; - -export const mockPerformanceInsightsNextPageResponse = { - data: { - project: { - __typename: 'Project', - id: 'gid://gitlab/Project/20', - pipeline: { - __typename: 'Pipeline', - id: 'gid://gitlab/Ci::Pipeline/97', - jobs: { - __typename: 'CiJobConnection', - pageInfo: { - __typename: 'PageInfo', - hasNextPage: true, - }, - nodes: [ - { - __typename: 'CiJob', - id: 'gid://gitlab/Ci::Bridge/2502', - duration: null, - detailedStatus: { - __typename: 'DetailedStatus', - id: 'success-2502-2502', - detailsPath: '/root/lots-of-jobs-project/-/pipelines/98', - }, - name: 'trigger_job', - stage: { - __typename: 'CiStage', - id: 'gid://gitlab/Ci::Stage/303', - name: 'deploy', - }, - startedAt: null, - queuedDuration: 424850.376278, - }, - ], - }, - }, - }, - }, -}; diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js index 99e8ea9d0a4..19f597a7267 100644 --- a/spec/frontend/pipelines/graph/stage_column_component_spec.js +++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js @@ -42,8 +42,8 @@ describe('stage column component', () => { const findStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]'); const findStageColumnGroup = () => wrapper.find('[data-testid="stage-column-group"]'); const findAllStageColumnGroups = () => wrapper.findAll('[data-testid="stage-column-group"]'); - const findJobItem = () => wrapper.find(JobItem); - const findActionComponent = () => wrapper.find(ActionComponent); + const findJobItem = () => wrapper.findComponent(JobItem); + const findActionComponent = () => wrapper.findComponent(ActionComponent); const createComponent = ({ method = shallowMount, props = {} } = {}) => { wrapper = method(StageColumnComponent, { @@ -126,9 +126,9 @@ describe('stage column component', () => { }); }); - it('capitalizes and escapes name', () => { - expect(findStageColumnTitle().text()).toBe( - 'Test <img src=x onerror=alert(document.domain)>', + it('escapes name', () => { + expect(findStageColumnTitle().html()).toContain( + 'test <img src=x onerror=alert(document.domain)>', ); }); diff --git a/spec/frontend/pipelines/graph_shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js index 44ab60cbee7..e2699d6ff2e 100644 --- a/spec/frontend/pipelines/graph_shared/links_layer_spec.js +++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js @@ -6,7 +6,7 @@ import { generateResponse, mockPipelineResponse } from '../graph/mock_data'; describe('links layer component', () => { let wrapper; - const findLinksInner = () => wrapper.find(LinksInner); + const findLinksInner = () => wrapper.findComponent(LinksInner); const pipeline = generateResponse(mockPipelineResponse, 'root/fungi-xoxo'); const containerId = `pipeline-links-container-${pipeline.id}`; diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js index 859be8d342c..e583c0798f5 100644 --- a/spec/frontend/pipelines/header_component_spec.js +++ b/spec/frontend/pipelines/header_component_spec.js @@ -21,12 +21,12 @@ describe('Pipeline details header', () => { let glModalDirective; let mutate = jest.fn(); - const findAlert = () => wrapper.find(GlAlert); - const findDeleteModal = () => wrapper.find(GlModal); + const findAlert = () => wrapper.findComponent(GlAlert); + const findDeleteModal = () => wrapper.findComponent(GlModal); const findRetryButton = () => wrapper.find('[data-testid="retryPipeline"]'); const findCancelButton = () => wrapper.find('[data-testid="cancelPipeline"]'); const findDeleteButton = () => wrapper.find('[data-testid="deletePipeline"]'); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const defaultProvideOptions = { pipelineId: '14', diff --git a/spec/frontend/pipelines/performance_insights_modal_spec.js b/spec/frontend/pipelines/performance_insights_modal_spec.js deleted file mode 100644 index 8c802be7718..00000000000 --- a/spec/frontend/pipelines/performance_insights_modal_spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import { GlAlert, GlLink, GlModal } from '@gitlab/ui'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import waitForPromises from 'helpers/wait_for_promises'; -import PerformanceInsightsModal from '~/pipelines/components/performance_insights_modal.vue'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { trimText } from 'helpers/text_helper'; -import getPerformanceInsights from '~/pipelines/graphql/queries/get_performance_insights.query.graphql'; -import { - mockPerformanceInsightsResponse, - mockPerformanceInsightsNextPageResponse, -} from './graph/mock_data'; - -Vue.use(VueApollo); - -describe('Performance insights modal', () => { - let wrapper; - - const findModal = () => wrapper.findComponent(GlModal); - const findAlert = () => wrapper.findComponent(GlAlert); - const findLink = () => wrapper.findComponent(GlLink); - const findLimitText = () => wrapper.findByTestId('limit-alert-text'); - const findQueuedCardData = () => wrapper.findByTestId('insights-queued-card-data'); - const findQueuedCardLink = () => wrapper.findByTestId('insights-queued-card-link'); - const findExecutedCardData = () => wrapper.findByTestId('insights-executed-card-data'); - const findExecutedCardLink = () => wrapper.findByTestId('insights-executed-card-link'); - const findSlowJobsStage = (index) => wrapper.findAllByTestId('insights-slow-job-stage').at(index); - const findSlowJobsLink = (index) => wrapper.findAllByTestId('insights-slow-job-link').at(index); - - const getPerformanceInsightsHandler = jest - .fn() - .mockResolvedValue(mockPerformanceInsightsResponse); - - const getPerformanceInsightsNextPageHandler = jest - .fn() - .mockResolvedValue(mockPerformanceInsightsNextPageResponse); - - const requestHandlers = [[getPerformanceInsights, getPerformanceInsightsHandler]]; - - const createComponent = (handlers = requestHandlers) => { - wrapper = shallowMountExtended(PerformanceInsightsModal, { - provide: { - pipelineIid: '1', - pipelineProjectPath: 'root/ci-project', - }, - apolloProvider: createMockApollo(handlers), - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('without next page', () => { - beforeEach(async () => { - createComponent(); - - await waitForPromises(); - }); - - it('displays modal', () => { - expect(findModal().exists()).toBe(true); - }); - - it('displays alert', () => { - expect(findAlert().exists()).toBe(true); - }); - - it('displays feedback issue link', () => { - expect(findLink().text()).toBe('Feedback issue'); - expect(findLink().attributes('href')).toBe( - 'https://gitlab.com/gitlab-org/gitlab/-/issues/365902', - ); - }); - - it('does not display limit text', () => { - expect(findLimitText().exists()).toBe(false); - }); - - describe('queued duration card', () => { - it('displays card data', () => { - expect(trimText(findQueuedCardData().text())).toBe('4.9 days'); - }); - it('displays card link', () => { - expect(findQueuedCardLink().attributes('href')).toBe( - '/root/lots-of-jobs-project/-/pipelines/98', - ); - }); - }); - - describe('executed duration card', () => { - it('displays card data', () => { - expect(trimText(findExecutedCardData().text())).toBe('trigger_job'); - }); - it('displays card link', () => { - expect(findExecutedCardLink().attributes('href')).toBe( - '/root/lots-of-jobs-project/-/pipelines/98', - ); - }); - }); - - describe('slow jobs', () => { - it.each` - index | expectedStage | expectedName | expectedLink - ${0} | ${'build'} | ${'wait_job'} | ${'/root/ci-project/-/jobs/2493'} - ${1} | ${'deploy'} | ${'artifact_job'} | ${'/root/ci-project/-/jobs/2501'} - ${2} | ${'test'} | ${'allow_failure_test_job'} | ${'/root/ci-project/-/jobs/2497'} - ${3} | ${'build'} | ${'large_log_output'} | ${'/root/ci-project/-/jobs/2495'} - ${4} | ${'build'} | ${'build_job'} | ${'/root/ci-project/-/jobs/2494'} - `( - 'should display slow job correctly', - ({ index, expectedStage, expectedName, expectedLink }) => { - expect(findSlowJobsStage(index).text()).toBe(expectedStage); - expect(findSlowJobsLink(index).text()).toBe(expectedName); - expect(findSlowJobsLink(index).attributes('href')).toBe(expectedLink); - }, - ); - }); - }); - - describe('with next page', () => { - it('displays limit text when there is a next page', async () => { - createComponent([[getPerformanceInsights, getPerformanceInsightsNextPageHandler]]); - - await waitForPromises(); - - expect(findLimitText().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js index 1b89e322d31..d9199f3b0f7 100644 --- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js +++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js @@ -34,7 +34,7 @@ describe('pipeline graph component', () => { }; const findAlert = () => wrapper.findComponent(GlAlert); - const findAllJobPills = () => wrapper.findAll(JobPill); + const findAllJobPills = () => wrapper.findAllComponents(JobPill); const findAllStageNames = () => wrapper.findAllComponents(StageName); const findLinksLayer = () => wrapper.findComponent(LinksLayer); const findPipelineGraph = () => wrapper.find('[data-testid="graph-container"]'); diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js index f554166da33..149b40330e2 100644 --- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js +++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js @@ -1,12 +1,14 @@ import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import axios from '~/lib/utils/axios_utils'; import PipelineMultiActions, { i18n, } from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue'; +import { TRACKING_CATEGORIES } from '~/pipelines/constants'; describe('Pipeline Multi Actions Dropdown', () => { let wrapper; @@ -136,4 +138,22 @@ describe('Pipeline Multi Actions Dropdown', () => { }); }); }); + + describe('tracking', () => { + afterEach(() => { + unmockTracking(); + }); + + it('tracks artifacts dropdown click', () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + + createComponent(); + + findDropdown().vm.$emit('show'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_artifacts_dropdown', { + label: TRACKING_CATEGORIES.table, + }); + }); + }); }); diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js index 25a97ecf49d..1d66607e72b 100644 --- a/spec/frontend/pipelines/pipeline_url_spec.js +++ b/spec/frontend/pipelines/pipeline_url_spec.js @@ -1,12 +1,15 @@ +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { TRACKING_CATEGORIES } from '~/pipelines/constants'; import { mockPipeline, mockPipelineBranch, mockPipelineTag } from './mock_data'; const projectPath = 'test/test'; describe('Pipeline Url Component', () => { let wrapper; + let trackingSpy; const findTableCell = () => wrapper.findByTestId('pipeline-url-table-cell'); const findPipelineUrlLink = () => wrapper.findByTestId('pipeline-url-link'); @@ -14,6 +17,7 @@ describe('Pipeline Url Component', () => { const findCommitShortSha = () => wrapper.findByTestId('commit-short-sha'); const findCommitIcon = () => wrapper.findByTestId('commit-icon'); const findCommitIconType = () => wrapper.findByTestId('commit-icon-type'); + const findCommitRefName = () => wrapper.findByTestId('commit-ref-name'); const findCommitTitleContainer = () => wrapper.findByTestId('commit-title-container'); const findCommitTitle = (commitWrapper) => commitWrapper.find('[data-testid="commit-title"]'); @@ -31,7 +35,6 @@ describe('Pipeline Url Component', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); it('should render pipeline url table cell', () => { @@ -49,7 +52,7 @@ describe('Pipeline Url Component', () => { }); it('should render the commit title, commit reference and commit-short-sha', () => { - createComponent({}, true); + createComponent(); const commitWrapper = findCommitTitleContainer(); @@ -83,7 +86,7 @@ describe('Pipeline Url Component', () => { }); it('should render commit icon tooltip', () => { - createComponent({}, true); + createComponent(); expect(findCommitIcon().attributes('title')).toBe('Commit'); }); @@ -94,8 +97,68 @@ describe('Pipeline Url Component', () => { ${mockPipelineBranch()} | ${'Branch'} ${mockPipeline()} | ${'Merge Request'} `('should render tooltip $expectedTitle for commit icon type', ({ pipeline, expectedTitle }) => { - createComponent(pipeline, true); + createComponent(pipeline); expect(findCommitIconType().attributes('title')).toBe(expectedTitle); }); + + describe('tracking', () => { + beforeEach(() => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks pipeline id click', () => { + createComponent(); + + findPipelineUrlLink().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_pipeline_id', { + label: TRACKING_CATEGORIES.table, + }); + }); + + it('tracks merge request ref click', () => { + createComponent(); + + findRefName().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_mr_ref', { + label: TRACKING_CATEGORIES.table, + }); + }); + + it('tracks commit ref name click', () => { + createComponent(mockPipelineBranch()); + + findCommitRefName().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_name', { + label: TRACKING_CATEGORIES.table, + }); + }); + + it('tracks commit title click', () => { + createComponent(mockPipelineBranch()); + + findCommitTitle(findCommitTitleContainer()).vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_title', { + label: TRACKING_CATEGORIES.table, + }); + }); + + it('tracks commit short sha click', () => { + createComponent(mockPipelineBranch()); + + findCommitShortSha().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_commit_sha', { + label: TRACKING_CATEGORIES.table, + }); + }); + }); }); diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js index 9b2ee6b8278..fdfced38dca 100644 --- a/spec/frontend/pipelines/pipelines_actions_spec.js +++ b/spec/frontend/pipelines/pipelines_actions_spec.js @@ -2,6 +2,7 @@ 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 createFlash from '~/flash'; @@ -9,6 +10,7 @@ import axios from '~/lib/utils/axios_utils'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; +import { TRACKING_CATEGORIES } from '~/pipelines/constants'; jest.mock('~/flash'); jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => { @@ -29,9 +31,9 @@ describe('Pipelines Actions dropdown', () => { }); }; - const findDropdown = () => wrapper.find(GlDropdown); - const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem); - const findAllCountdowns = () => wrapper.findAll(GlCountdown); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findAllCountdowns = () => wrapper.findAllComponents(GlCountdown); beforeEach(() => { mock = new MockAdapter(axios); @@ -96,6 +98,22 @@ describe('Pipelines Actions dropdown', () => { expect(createFlash).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', () => { diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js index 2d876841e06..e3e54716a7b 100644 --- a/spec/frontend/pipelines/pipelines_artifacts_spec.js +++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js @@ -30,8 +30,9 @@ describe('Pipelines Artifacts dropdown', () => { }; const findDropdown = () => wrapper.findComponent(GlDropdown); - const findFirstGlDropdownItem = () => wrapper.find(GlDropdownItem); - const findAllGlDropdownItems = () => wrapper.find(GlDropdown).findAll(GlDropdownItem); + const findFirstGlDropdownItem = () => wrapper.findComponent(GlDropdownItem); + const findAllGlDropdownItems = () => + wrapper.findComponent(GlDropdown).findAllComponents(GlDropdownItem); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 0bed24e588e..cc2ff90de57 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -7,6 +7,7 @@ import { nextTick } from 'vue'; import mockPipelinesResponse from 'test_fixtures/pipelines/pipelines.json'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; +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'; @@ -16,7 +17,7 @@ import NavigationControls from '~/pipelines/components/pipelines_list/nav_contro import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue'; import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue'; import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; -import { RAW_TEXT_WARNING } from '~/pipelines/constants'; +import { RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/pipelines/constants'; import Store from '~/pipelines/stores/pipelines_store'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; @@ -37,6 +38,7 @@ const mockPipelineWithStages = mockPipelinesResponse.pipelines.find( describe('Pipelines', () => { let wrapper; let mock; + let trackingSpy; const paths = { emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', @@ -123,7 +125,7 @@ describe('Pipelines', () => { }); it('shows loading state when the app is loading', () => { - expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); }); it('does not display tabs when the first request has not yet been made', () => { @@ -236,6 +238,8 @@ describe('Pipelines', () => { count: mockPipelinesResponse.count, }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + goToTab('finished'); await waitForPromises(); @@ -256,6 +260,12 @@ describe('Pipelines', () => { `${window.location.pathname}?scope=finished&page=1`, ); }); + + it('tracks tab change click', () => { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_filter_tabs', { + label: TRACKING_CATEGORIES.tabs, + }); + }); }); describe('when the scope in the tab is empty', () => { @@ -375,7 +385,7 @@ describe('Pipelines', () => { const [firstPage, secondPage] = chunk(mockPipelinesResponse.pipelines, mockPageSize); const goToPage = (page) => { - findTablePagination().find(GlPagination).vm.$emit('input', page); + findTablePagination().findComponent(GlPagination).vm.$emit('input', page); }; beforeEach(async () => { @@ -583,7 +593,7 @@ describe('Pipelines', () => { 'This project is not currently set up to run pipelines.', ); - expect(findEmptyState().find(GlButton).exists()).toBe(false); + expect(findEmptyState().findComponent(GlButton).exists()).toBe(false); }); it('does not render tabs or buttons', () => { diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js index 7b49baa5a20..044683ce533 100644 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ b/spec/frontend/pipelines/pipelines_table_spec.js @@ -2,8 +2,9 @@ import '~/commons'; import { GlTableLite } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import fixture from 'test_fixtures/pipelines/pipelines.json'; +import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; +import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue'; import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue'; import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue'; @@ -13,6 +14,7 @@ import { PipelineKeyOptions, BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, + TRACKING_CATEGORIES, } from '~/pipelines/constants'; import eventHub from '~/pipelines/event_hub'; @@ -23,6 +25,7 @@ jest.mock('~/pipelines/event_hub'); describe('Pipelines Table', () => { let pipeline; let wrapper; + let trackingSpy; const defaultProps = { pipelines: [], @@ -69,6 +72,7 @@ describe('Pipelines Table', () => { afterEach(() => { wrapper.destroy(); + wrapper = null; }); @@ -96,10 +100,6 @@ describe('Pipelines Table', () => { it('should render a status badge', () => { expect(findStatusBadge().exists()).toBe(true); }); - - it('should render status badge with correct path', () => { - expect(findStatusBadge().attributes('href')).toBe(pipeline.path); - }); }); describe('pipeline cell', () => { @@ -113,40 +113,28 @@ describe('Pipelines Table', () => { }); describe('stages cell', () => { - it('should render a pipeline mini graph', () => { + it('should render pipeline mini graph', () => { expect(findPipelineMiniGraph().exists()).toBe(true); }); it('should render the right number of stages', () => { const stagesLength = pipeline.details.stages.length; - expect( - findPipelineMiniGraph().findAll('[data-testid="mini-pipeline-graph-dropdown"]'), - ).toHaveLength(stagesLength); + expect(findPipelineMiniGraph().props('stages').length).toBe(stagesLength); }); describe('when pipeline does not have stages', () => { beforeEach(() => { pipeline = createMockPipeline(); - pipeline.details.stages = null; + pipeline.details.stages = []; createComponent({ pipelines: [pipeline] }); }); it('stages are not rendered', () => { - expect(findPipelineMiniGraph().exists()).toBe(false); + expect(findPipelineMiniGraph().props('stages')).toHaveLength(0); }); }); - it('should not update dropdown', () => { - expect(findPipelineMiniGraph().props('updateDropdown')).toBe(false); - }); - - it('when update graph dropdown is set, should update graph dropdown', () => { - createComponent({ pipelines: [pipeline], updateGraphDropdown: true }); - - expect(findPipelineMiniGraph().props('updateDropdown')).toBe(true); - }); - it('when action request is complete, should refresh table', () => { findPipelineMiniGraph().vm.$emit('pipelineActionRequestComplete'); @@ -179,5 +167,47 @@ describe('Pipelines Table', () => { expect(findTriggerer().exists()).toBe(true); }); }); + + describe('tracking', () => { + beforeEach(() => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks status badge click', () => { + findStatusBadge().vm.$emit('ciStatusBadgeClick'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', { + label: TRACKING_CATEGORIES.table, + }); + }); + + it('tracks retry pipeline button click', () => { + findRetryBtn().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', { + label: TRACKING_CATEGORIES.table, + }); + }); + + it('tracks cancel pipeline button click', () => { + findCancelBtn().vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', { + label: TRACKING_CATEGORIES.table, + }); + }); + + it('tracks pipeline mini graph stage click', () => { + findPipelineMiniGraph().vm.$emit('miniGraphStageClick'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_minigraph', { + label: TRACKING_CATEGORIES.table, + }); + }); + }); }); }); 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 c372ac06c35..da13df833e7 100644 --- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js @@ -26,10 +26,10 @@ describe('Test reports suite table', () => { const noCasesMessage = () => wrapper.findByTestId('no-test-cases'); const artifactsExpiredMessage = () => wrapper.findByTestId('artifacts-expired'); - const artifactsExpiredEmptyState = () => wrapper.find(GlEmptyState); + const artifactsExpiredEmptyState = () => wrapper.findComponent(GlEmptyState); const allCaseRows = () => wrapper.findAllByTestId('test-case-row'); const findCaseRowAtIndex = (index) => wrapper.findAllByTestId('test-case-row').at(index); - const findLinkForRow = (row) => row.find(GlLink); + const findLinkForRow = (row) => row.findComponent(GlLink); const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`); const createComponent = ({ suite = testSuite, perPage = 20, errorMessage } = {}) => { @@ -113,7 +113,7 @@ describe('Test reports suite table', () => { const filePath = `${blobPath}/${relativeFile}`; const row = findCaseRowAtIndex(0); const fileLink = findLinkForRow(row); - const button = row.find(GlButton); + const button = row.findComponent(GlButton); expect(fileLink.attributes('href')).toBe(filePath); expect(row.text()).toContain(file); @@ -134,7 +134,7 @@ describe('Test reports suite table', () => { }); it('renders a pagination component', () => { - expect(wrapper.find(GlPagination).exists()).toBe(true); + expect(wrapper.findComponent(GlPagination).exists()).toBe(true); }); }); diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js index 0e1229f7067..cfe9ff564dc 100644 --- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js +++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js @@ -44,7 +44,7 @@ describe('Test reports summary table', () => { describe('when test reports are supplied', () => { beforeEach(() => createComponent()); - const findErrorIcon = () => wrapper.find({ ref: 'suiteErrorIcon' }); + const findErrorIcon = () => wrapper.findComponent({ ref: 'suiteErrorIcon' }); it('renders the correct number of rows', () => { expect(noSuitesToShow().exists()).toBe(false); diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js index 3de7995b476..f0da0df2ba6 100644 --- a/spec/frontend/pipelines/time_ago_spec.js +++ b/spec/frontend/pipelines/time_ago_spec.js @@ -48,7 +48,7 @@ describe('Timeago component', () => { }); it('should render duration and timer svg', () => { - const icon = duration().find(GlIcon); + const icon = duration().findComponent(GlIcon); expect(duration().exists()).toBe(true); expect(icon.props('name')).toBe('timer'); @@ -71,7 +71,7 @@ describe('Timeago component', () => { }); it('should render time and calendar icon', () => { - const icon = finishedAt().find(GlIcon); + const icon = finishedAt().findComponent(GlIcon); const time = finishedAt().find('time'); expect(finishedAt().exists()).toBe(true); 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 ba478363d04..caa66502e11 100644 --- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js @@ -9,9 +9,10 @@ import { branches, mockBranchesAfterMap } from '../mock_data'; describe('Pipeline Branch Name Token', () => { let wrapper; - const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); + const findAllFilteredSearchSuggestions = () => + wrapper.findAllComponents(GlFilteredSearchSuggestion); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const getBranchSuggestions = () => findAllFilteredSearchSuggestions().wrappers.map((w) => w.text()); diff --git a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js index b8abf2c1727..60abb63a7e0 100644 --- a/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_source_token_spec.js @@ -7,8 +7,9 @@ import PipelineSourceToken from '~/pipelines/components/pipelines_list/tokens/pi describe('Pipeline Source Token', () => { let wrapper; - const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion); + const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); + const findAllFilteredSearchSuggestions = () => + wrapper.findAllComponents(GlFilteredSearchSuggestion); const 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 2c5fa8b00e2..94f9a37f707 100644 --- a/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_status_token_spec.js @@ -6,9 +6,10 @@ import PipelineStatusToken from '~/pipelines/components/pipelines_list/tokens/pi describe('Pipeline Status Token', () => { let wrapper; - const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion); - const findAllGlIcons = () => wrapper.findAll(GlIcon); + const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); + const findAllFilteredSearchSuggestions = () => + wrapper.findAllComponents(GlFilteredSearchSuggestion); + const findAllGlIcons = () => wrapper.findAllComponents(GlIcon); const 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 596a9218c39..7311a5d2f5a 100644 --- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js @@ -7,9 +7,10 @@ import { tags, mockTagsAfterMap } from '../mock_data'; describe('Pipeline Branch Name Token', () => { let wrapper; - const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); + const findAllFilteredSearchSuggestions = () => + wrapper.findAllComponents(GlFilteredSearchSuggestion); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const stubs = { GlFilteredSearchToken: { 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 397dbdf95a9..c763bfe1b27 100644 --- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js +++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js @@ -8,9 +8,10 @@ import { users } from '../mock_data'; describe('Pipeline Trigger Author Token', () => { let wrapper; - const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken); - const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findFilteredSearchToken = () => wrapper.findComponent(GlFilteredSearchToken); + const findAllFilteredSearchSuggestions = () => + wrapper.findAllComponents(GlFilteredSearchSuggestion); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const defaultProps = { config: { diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js index a82390fae22..1c23a7e4fcf 100644 --- a/spec/frontend/pipelines/utils_spec.js +++ b/spec/frontend/pipelines/utils_spec.js @@ -8,14 +8,10 @@ import { removeOrphanNodes, getMaxNodes, } from '~/pipelines/components/parsing_utils'; -import { createNodeDict, calculateJobStats, calculateSlowestFiveJobs } from '~/pipelines/utils'; +import { createNodeDict } from '~/pipelines/utils'; import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data'; -import { - generateResponse, - mockPipelineResponse, - mockPerformanceInsightsResponse, -} from './graph/mock_data'; +import { generateResponse, mockPipelineResponse } from './graph/mock_data'; describe('DAG visualization parsing utilities', () => { const nodeDict = createNodeDict(mockParsedGraphQLNodes); @@ -162,40 +158,4 @@ describe('DAG visualization parsing utilities', () => { expect(columns).toMatchSnapshot(); }); }); - - describe('performance insights', () => { - const { - data: { - project: { - pipeline: { jobs }, - }, - }, - } = mockPerformanceInsightsResponse; - - describe('calculateJobStats', () => { - const expectedJob = jobs.nodes[0]; - - it('returns the job that spent this longest time queued', () => { - expect(calculateJobStats(jobs, 'queuedDuration')).toEqual(expectedJob); - }); - - it('returns the job that was executed last', () => { - expect(calculateJobStats(jobs, 'startedAt')).toEqual(expectedJob); - }); - }); - - describe('calculateSlowestFiveJobs', () => { - it('returns the slowest five jobs of the pipeline', () => { - const expectedJobs = [ - jobs.nodes[9], - jobs.nodes[1], - jobs.nodes[5], - jobs.nodes[7], - jobs.nodes[8], - ]; - - expect(calculateSlowestFiveJobs(jobs)).toEqual(expectedJobs); - }); - }); - }); }); diff --git a/spec/frontend/popovers/components/popovers_spec.js b/spec/frontend/popovers/components/popovers_spec.js index 6fdcd34ae83..eba6b95214d 100644 --- a/spec/frontend/popovers/components/popovers_spec.js +++ b/spec/frontend/popovers/components/popovers_spec.js @@ -31,7 +31,7 @@ describe('popovers/components/popovers.vue', () => { return target; }; - const allPopovers = () => wrapper.findAll(GlPopover); + const allPopovers = () => wrapper.findAllComponents(GlPopover); afterEach(() => { wrapper.destroy(); @@ -42,7 +42,7 @@ describe('popovers/components/popovers.vue', () => { it('attaches popovers to the targets specified', async () => { const target = createPopoverTarget(); await buildWrapper(target); - expect(wrapper.find(GlPopover).props('target')).toBe(target); + expect(wrapper.findComponent(GlPopover).props('target')).toBe(target); }); it('does not attach a popover twice to the same element', async () => { @@ -52,7 +52,7 @@ describe('popovers/components/popovers.vue', () => { await nextTick(); - expect(wrapper.findAll(GlPopover)).toHaveLength(1); + expect(wrapper.findAllComponents(GlPopover)).toHaveLength(1); }); describe('supports HTML content', () => { @@ -66,7 +66,7 @@ describe('popovers/components/popovers.vue', () => { `('$description', async ({ content, render }) => { await buildWrapper(createPopoverTarget({ content, html: true })); - const html = wrapper.find(GlPopover).html(); + const html = wrapper.findComponent(GlPopover).html(); expect(html).toContain(render); }); }); @@ -78,7 +78,7 @@ describe('popovers/components/popovers.vue', () => { `('sets $option to $value when data-$option is set in target', async ({ option, value }) => { await buildWrapper(createPopoverTarget({ [option]: value })); - expect(wrapper.find(GlPopover).props(option)).toBe(value); + expect(wrapper.findComponent(GlPopover).props(option)).toBe(value); }); }); 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 ad62d84c43c..e4a316e1ee7 100644 --- a/spec/frontend/profile/account/components/delete_account_modal_spec.js +++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js @@ -53,7 +53,7 @@ describe('DeleteAccountModal component', () => { input: vm.$el.querySelector(`[name="${confirmation}"]`), }; }; - const findModal = () => wrapper.find(GlModalStub); + const findModal = () => wrapper.findComponent(GlModalStub); describe('with password confirmation', () => { beforeEach(async () => { diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js index 0e56bccf27e..e331eed1863 100644 --- a/spec/frontend/profile/account/components/update_username_spec.js +++ b/spec/frontend/profile/account/components/update_username_spec.js @@ -44,7 +44,7 @@ describe('UpdateUsername component', () => { }); const findElements = () => { - const modal = wrapper.find(GlModal); + const modal = wrapper.findComponent(GlModal); return { modal, @@ -149,7 +149,7 @@ describe('UpdateUsername component', () => { await expect(wrapper.vm.onConfirm()).rejects.toThrow(); - expect(createFlash).toBeCalledWith({ + expect(createFlash).toHaveBeenCalledWith({ message: 'Invalid username', }); }); @@ -161,7 +161,7 @@ describe('UpdateUsername component', () => { await expect(wrapper.vm.onConfirm()).rejects.toThrow(); - expect(createFlash).toBeCalledWith({ + expect(createFlash).toHaveBeenCalledWith({ message: 'An error occurred while updating your username, please try again.', }); }); diff --git a/spec/frontend/profile/preferences/components/integration_view_spec.js b/spec/frontend/profile/preferences/components/integration_view_spec.js index 92c53b8c91b..f650bee7fda 100644 --- a/spec/frontend/profile/preferences/components/integration_view_spec.js +++ b/spec/frontend/profile/preferences/components/integration_view_spec.js @@ -98,6 +98,6 @@ describe('IntegrationView component', () => { it('should render the help text', () => { wrapper = createComponent(); - expect(wrapper.find(IntegrationHelpText).exists()).toBe(true); + expect(wrapper.findComponent(IntegrationHelpText).exists()).toBe(true); }); }); diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js index 4d2dcf83d3b..89ce838a383 100644 --- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js +++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js @@ -90,7 +90,7 @@ describe('ProfilePreferences component', () => { it('should not render Integrations section', () => { wrapper = createComponent(); - const views = wrapper.findAll(IntegrationView); + const views = wrapper.findAllComponents(IntegrationView); const divider = findIntegrationsDivider(); const heading = findIntegrationsHeading(); @@ -103,7 +103,7 @@ describe('ProfilePreferences component', () => { wrapper = createComponent({ provide: { integrationViews } }); const divider = findIntegrationsDivider(); const heading = findIntegrationsHeading(); - const views = wrapper.findAll(IntegrationView); + const views = wrapper.findAllComponents(IntegrationView); expect(divider.exists()).toBe(true); expect(heading.exists()).toBe(true); diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js index 79e9dab935d..20c312ec771 100644 --- a/spec/frontend/projects/commit/components/form_modal_spec.js +++ b/spec/frontend/projects/commit/components/form_modal_spec.js @@ -99,7 +99,9 @@ describe('CommitFormModal', () => { createComponent(shallowMount, {}, { prependedText: '_prepended_text_' }); expect(findPrependedText().exists()).toBe(true); - expect(findPrependedText().find(GlSprintf).attributes('message')).toBe('_prepended_text_'); + expect(findPrependedText().findComponent(GlSprintf).attributes('message')).toBe( + '_prepended_text_', + ); }); it('Does not show prepended text', () => { @@ -124,7 +126,7 @@ describe('CommitFormModal', () => { createComponent(shallowMount, { pushCode: false }); expect(findAppendedText().exists()).toBe(true); - expect(findAppendedText().find(GlSprintf).attributes('message')).toContain( + expect(findAppendedText().findComponent(GlSprintf).attributes('message')).toContain( mockData.modalPropsData.i18n.branchInFork, ); }); @@ -133,7 +135,7 @@ describe('CommitFormModal', () => { createComponent(shallowMount, { pushCode: false, branchCollaboration: true }); expect(findAppendedText().exists()).toBe(true); - expect(findAppendedText().find(GlSprintf).attributes('message')).toContain( + expect(findAppendedText().findComponent(GlSprintf).attributes('message')).toContain( mockData.modalPropsData.i18n.existingBranch, ); }); diff --git a/spec/frontend/projects/commit/store/mutations_spec.js b/spec/frontend/projects/commit/store/mutations_spec.js index 60abf0fddad..40174b3057a 100644 --- a/spec/frontend/projects/commit/store/mutations_spec.js +++ b/spec/frontend/projects/commit/store/mutations_spec.js @@ -26,7 +26,7 @@ describe('Commit form modal mutations', () => { }); describe('CLEAR_MODAL', () => { - it('should clear modal state ', () => { + it('should clear modal state', () => { stateCopy = { branch: '_main_', defaultBranch: '_default_branch_' }; mutations[types.CLEAR_MODAL](stateCopy); diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js index 57e5ef0ed1d..907e0e226b6 100644 --- a/spec/frontend/projects/commits/components/author_select_spec.js +++ b/spec/frontend/projects/commits/components/author_select_spec.js @@ -58,11 +58,11 @@ describe('Author Select', () => { resetHTMLFixture(); }); - const findDropdownContainer = () => wrapper.find({ ref: 'dropdownContainer' }); - const findDropdown = () => wrapper.find(GlDropdown); - const findDropdownHeader = () => wrapper.find(GlDropdownSectionHeader); - const findSearchBox = () => wrapper.find(GlSearchBoxByType); - const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findDropdownContainer = () => wrapper.findComponent({ ref: 'dropdownContainer' }); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findDropdownHeader = () => wrapper.findComponent(GlDropdownSectionHeader); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); describe('user is searching via "filter by commit message"', () => { it('disables dropdown container', async () => { diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js index c9ffdf20c32..2dbecf7cc61 100644 --- a/spec/frontend/projects/compare/components/app_spec.js +++ b/spec/frontend/projects/compare/components/app_spec.js @@ -58,7 +58,7 @@ describe('CompareApp component', () => { }); it('render Source and Target BranchDropdown components', () => { - const revisionCards = wrapper.findAll(RevisionCard); + const revisionCards = wrapper.findAllComponents(RevisionCard); expect(revisionCards.length).toBe(2); expect(revisionCards.at(0).props('revisionText')).toBe('Source'); @@ -66,7 +66,7 @@ describe('CompareApp component', () => { }); describe('compare button', () => { - const findCompareButton = () => wrapper.find(GlButton); + const findCompareButton = () => wrapper.findComponent(GlButton); it('renders button', () => { expect(findCompareButton().exists()).toBe(true); diff --git a/spec/frontend/projects/compare/components/repo_dropdown_spec.js b/spec/frontend/projects/compare/components/repo_dropdown_spec.js index 98aec347e4b..21cca857c6a 100644 --- a/spec/frontend/projects/compare/components/repo_dropdown_spec.js +++ b/spec/frontend/projects/compare/components/repo_dropdown_spec.js @@ -21,7 +21,7 @@ describe('RepoDropdown component', () => { wrapper = null; }); - const findGlDropdown = () => wrapper.find(GlDropdown); + const findGlDropdown = () => wrapper.findComponent(GlDropdown); const findHiddenInput = () => wrapper.find('input[type="hidden"]'); describe('Source Revision', () => { @@ -73,7 +73,7 @@ describe('RepoDropdown component', () => { }); it('emits `selectProject` event when another target project is selected', async () => { - findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click'); + findGlDropdown().findAllComponents(GlDropdownItem).at(0).vm.$emit('click'); await nextTick(); expect(wrapper.emitted('selectProject')[0][0]).toEqual({ diff --git a/spec/frontend/projects/compare/components/revision_card_spec.js b/spec/frontend/projects/compare/components/revision_card_spec.js index a741393fcf3..b23bd91ceda 100644 --- a/spec/frontend/projects/compare/components/revision_card_spec.js +++ b/spec/frontend/projects/compare/components/revision_card_spec.js @@ -32,10 +32,10 @@ describe('RepoDropdown component', () => { }); it('renders RepoDropdown component', () => { - expect(wrapper.findAll(RepoDropdown).exists()).toBe(true); + expect(wrapper.findAllComponents(RepoDropdown).exists()).toBe(true); }); it('renders RevisionDropdown component', () => { - expect(wrapper.findAll(RevisionDropdown).exists()).toBe(true); + expect(wrapper.findAllComponents(RevisionDropdown).exists()).toBe(true); }); }); 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 102f95f65da..f64af1aa994 100644 --- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js +++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js @@ -38,7 +38,7 @@ describe('RevisionDropdown component', () => { axiosMock.restore(); }); - const findGlDropdown = () => wrapper.find(GlDropdown); + const findGlDropdown = () => wrapper.findComponent(GlDropdown); it('sets hidden input', () => { expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe( @@ -99,7 +99,7 @@ describe('RevisionDropdown component', () => { }); it('emits a "selectRevision" event when a revision is selected', async () => { - const findGlDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findGlDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); const findFirstGlDropdownItem = () => findGlDropdownItems().at(0); // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js index c8a90848492..35e32fd3da0 100644 --- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js +++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js @@ -35,8 +35,8 @@ describe('RevisionDropdown component', () => { axiosMock.restore(); }); - const findGlDropdown = () => wrapper.find(GlDropdown); - const findSearchBox = () => wrapper.find(GlSearchBoxByType); + const findGlDropdown = () => wrapper.findComponent(GlDropdown); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); it('sets hidden input', () => { createComponent(); @@ -144,7 +144,7 @@ describe('RevisionDropdown component', () => { wrapper.vm.branches = ['some-branch']; await nextTick(); - findGlDropdown().findAll(GlDropdownItem).at(0).vm.$emit('click'); + findGlDropdown().findAllComponents(GlDropdownItem).at(0).vm.$emit('click'); expect(wrapper.emitted('selectRevision')[0][0]).toEqual({ direction: 'to', diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js index a3bc4931eb3..49e3218e5bc 100644 --- a/spec/frontend/projects/components/project_delete_button_spec.js +++ b/spec/frontend/projects/components/project_delete_button_spec.js @@ -8,7 +8,7 @@ jest.mock('lodash/uniqueId', () => () => 'fakeUniqueId'); describe('Project remove modal', () => { let wrapper; - const findSharedDeleteButton = () => wrapper.find(SharedDeleteButton); + const findSharedDeleteButton = () => wrapper.findComponent(SharedDeleteButton); const defaultProps = { confirmPhrase: 'foo', diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js index 45c39ee91d8..097b18025a3 100644 --- a/spec/frontend/projects/components/shared/delete_button_spec.js +++ b/spec/frontend/projects/components/shared/delete_button_spec.js @@ -11,7 +11,7 @@ describe('Project remove modal', () => { const findFormElement = () => wrapper.find('form'); const findConfirmButton = () => wrapper.find('.js-modal-action-primary'); const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]'); - const findModal = () => wrapper.find(GlModal); + const findModal = () => wrapper.findComponent(GlModal); const findTitle = () => wrapper.find('[data-testid="delete-alert-title"]'); const findAlertBody = () => wrapper.find('[data-testid="delete-alert-body"]'); diff --git a/spec/frontend/projects/details/upload_button_spec.js b/spec/frontend/projects/details/upload_button_spec.js index d7308963088..50638755260 100644 --- a/spec/frontend/projects/details/upload_button_spec.js +++ b/spec/frontend/projects/details/upload_button_spec.js @@ -32,11 +32,11 @@ describe('UploadButton', () => { }); it('displays an upload button', () => { - expect(wrapper.find(GlButton).exists()).toBe(true); + expect(wrapper.findComponent(GlButton).exists()).toBe(true); }); it('contains a modal', () => { - const modal = wrapper.find(UploadBlobModal); + const modal = wrapper.findComponent(UploadBlobModal); expect(modal.exists()).toBe(true); expect(modal.props('modalId')).toBe(MODAL_ID); @@ -44,7 +44,7 @@ describe('UploadButton', () => { describe('when clickinig the upload file button', () => { beforeEach(() => { - wrapper.find(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); }); it('opens the modal', () => { diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js index 7b9011fa3d9..e3aaf760d1e 100644 --- a/spec/frontend/projects/pipelines/charts/components/app_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js @@ -47,15 +47,16 @@ describe('ProjectsPipelinesChartsApp', () => { wrapper.destroy(); }); - const findGlTabs = () => wrapper.find(GlTabs); - const findAllGlTabs = () => wrapper.findAll(GlTab); + const findGlTabs = () => wrapper.findComponent(GlTabs); + const findAllGlTabs = () => wrapper.findAllComponents(GlTab); const findGlTabAtIndex = (index) => findAllGlTabs().at(index); - const findLeadTimeCharts = () => wrapper.find(LeadTimeChartsStub); - const findTimeToRestoreServiceCharts = () => wrapper.find(TimeToRestoreServiceChartsStub); - const findChangeFailureRateCharts = () => wrapper.find(ChangeFailureRateChartsStub); - const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub); - const findPipelineCharts = () => wrapper.find(PipelineCharts); - const findProjectQualitySummary = () => wrapper.find(ProjectQualitySummaryStub); + const findLeadTimeCharts = () => wrapper.findComponent(LeadTimeChartsStub); + const findTimeToRestoreServiceCharts = () => + wrapper.findComponent(TimeToRestoreServiceChartsStub); + const findChangeFailureRateCharts = () => wrapper.findComponent(ChangeFailureRateChartsStub); + const findDeploymentFrequencyCharts = () => wrapper.findComponent(DeploymentFrequencyChartsStub); + const findPipelineCharts = () => wrapper.findComponent(PipelineCharts); + const findProjectQualitySummary = () => wrapper.findComponent(ProjectQualitySummaryStub); describe('when all charts are available', () => { beforeEach(() => { 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 7bb289408b8..8c18d2992ea 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 @@ -81,7 +81,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', ( it('should select a different chart on change', async () => { findSegmentedControl().vm.$emit('input', 1); - const chart = wrapper.find(CiCdAnalyticsAreaChart); + const chart = wrapper.findComponent(CiCdAnalyticsAreaChart); await nextTick(); @@ -92,7 +92,7 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', ( it('should not display charts if there are no charts', () => { wrapper = createWrapper({ charts: [] }); - expect(wrapper.find(CiCdAnalyticsAreaChart).exists()).toBe(false); + expect(wrapper.findComponent(CiCdAnalyticsAreaChart).exists()).toBe(false); }); describe('slots', () => { 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 3c91b913e67..8fb59f38ee1 100644 --- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js +++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js @@ -44,7 +44,7 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => { describe('overall statistics', () => { it('displays the statistics list', () => { - const list = wrapper.find(StatisticsList); + const list = wrapper.findComponent(StatisticsList); expect(list.exists()).toBe(true); expect(list.props('counts')).toEqual({ @@ -56,9 +56,9 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => { }); it('displays the commit duration chart', () => { - const chart = wrapper.find(GlColumnChart); + const chart = wrapper.findComponent(GlColumnChart); - expect(chart.exists()).toBeTruthy(); + expect(chart.exists()).toBe(true); expect(chart.props('yAxisTitle')).toBe('Minutes'); expect(chart.props('xAxisTitle')).toBe('Commit'); expect(chart.props('bars')).toBe(wrapper.vm.timesChartTransformedData); @@ -68,12 +68,12 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => { describe('pipelines charts', () => { it('displays the charts components', () => { - expect(wrapper.find(CiCdAnalyticsCharts).exists()).toBe(true); + expect(wrapper.findComponent(CiCdAnalyticsCharts).exists()).toBe(true); }); describe('displays individual correctly', () => { it('renders with the correct data', () => { - const charts = wrapper.find(CiCdAnalyticsCharts); + const charts = wrapper.findComponent(CiCdAnalyticsCharts); expect(charts.props()).toEqual({ charts: wrapper.vm.areaCharts, chartOptions: wrapper.vm.$options.areaChartOptions, 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 1db48ce05d7..1b06f7874a3 100644 --- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js +++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js @@ -134,7 +134,7 @@ describe('Access Level Dropdown', () => { await waitForPromises(); }); - it('renders headers for each section ', () => { + it('renders headers for each section', () => { expect(findAllDropdownHeaders()).toHaveLength(4); }); @@ -164,7 +164,7 @@ describe('Access Level Dropdown', () => { expect(findDropdown().props('toggleClass')).toBe('gl-text-gray-500!'); }); - it('when no items selected, displays a default fallback label and has default CSS class ', () => { + it('when no items selected, displays a default fallback label and has default CSS class', () => { expect(findDropdownToggleLabel()).toBe(i18n.selectUsers); expect(findDropdown().props('toggleClass')).toBe('gl-text-gray-500!'); }); @@ -217,7 +217,7 @@ describe('Access Level Dropdown', () => { }); describe('selecting an item', () => { - it('selects the item on click and deselects on the next click ', async () => { + it('selects the item on click and deselects on the next click', async () => { createComponent(); await waitForPromises(); @@ -230,7 +230,7 @@ describe('Access Level Dropdown', () => { expect(item.props('isChecked')).toBe(false); }); - it('emits a formatted update on selection ', async () => { + it('emits a formatted update on selection', async () => { // ids: the items appear in that order in the dropdown // 1 2 3 - roles // 4 5 6 - groups 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 0a05832ceb6..329060b9d10 100644 --- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js +++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js @@ -27,9 +27,9 @@ describe('projects/settings/components/shared_runners', () => { }); }; - const findErrorAlert = () => wrapper.find(GlAlert); - const findSharedRunnersToggle = () => wrapper.find(GlToggle); - const findToggleTooltip = () => wrapper.find(GlTooltip); + const findErrorAlert = () => wrapper.findComponent(GlAlert); + 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'); 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 e12c3aeedd6..e920cd48163 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js @@ -1,18 +1,55 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import BranchRules from '~/projects/settings/repository/branch_rules/app.vue'; +import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue'; +import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue'; +import branchRulesQuery from '~/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'; +import createFlash from '~/flash'; +import { branchRulesMockResponse, propsDataMock } from './mock_data'; + +jest.mock('~/flash'); + +Vue.use(VueApollo); describe('Branch rules app', () => { let wrapper; + let fakeApollo; + + const branchRulesQuerySuccessHandler = jest.fn().mockResolvedValue(branchRulesMockResponse); + + const createComponent = async ({ queryHandler = branchRulesQuerySuccessHandler } = {}) => { + fakeApollo = createMockApollo([[branchRulesQuery, queryHandler]]); + + wrapper = mountExtended(BranchRules, { + apolloProvider: fakeApollo, + propsData: { + ...propsDataMock, + }, + }); - const createComponent = () => { - wrapper = mountExtended(BranchRules); + await waitForPromises(); }; - const findTitle = () => wrapper.find('strong'); + const findAllBranchRules = () => wrapper.findAllComponents(BranchRule); + const findEmptyState = () => wrapper.findByTestId('empty'); beforeEach(() => createComponent()); - it('renders a title', () => { - expect(findTitle().text()).toBe('Branch'); + it('displays an error if branch rules query fails', async () => { + await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); + expect(createFlash).toHaveBeenCalledWith({ message: i18n.queryError }); + }); + + it('displays an empty state if no branch rules are present', async () => { + await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); + expect(findEmptyState().text()).toBe(i18n.emptyState); + }); + + it('renders branch rules', () => { + const { nodes } = branchRulesMockResponse.data.project.branchRules; + expect(findAllBranchRules().at(0).text()).toBe(nodes[0].name); + expect(findAllBranchRules().at(1).text()).toBe(nodes[1].name); }); }); 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 new file mode 100644 index 00000000000..924dab60704 --- /dev/null +++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js @@ -0,0 +1,58 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BranchRule, { + i18n, +} from '~/projects/settings/repository/branch_rules/components/branch_rule.vue'; + +const defaultProps = { + name: 'main', + isDefault: true, + isProtected: true, + approvalDetails: ['requires approval from TEST', '2 status checks'], +}; + +describe('Branch rule', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(BranchRule, { propsData: { ...defaultProps, ...props } }); + }; + + const findDefaultBadge = () => wrapper.findByText(i18n.defaultLabel); + const findProtectedBadge = () => wrapper.findByText(i18n.protectedLabel); + const findBranchName = () => wrapper.findByText(defaultProps.name); + const findProtectionDetailsList = () => wrapper.findByRole('list'); + const findProtectionDetailsListItems = () => wrapper.findAllByRole('listitem'); + + beforeEach(() => createComponent()); + + it('renders the branch name', () => { + expect(findBranchName().exists()).toBe(true); + }); + + describe('badges', () => { + it('renders both default and protected badges', () => { + expect(findDefaultBadge().exists()).toBe(true); + expect(findProtectedBadge().exists()).toBe(true); + }); + + it('does not render default badge if isDefault is set to false', () => { + createComponent({ isDefault: false }); + expect(findDefaultBadge().exists()).toBe(false); + }); + + it('does not render protected badge if isProtected is set to false', () => { + createComponent({ isProtected: false }); + expect(findProtectedBadge().exists()).toBe(false); + }); + }); + + it('does not render the protection details list of no details are present', () => { + createComponent({ approvalDetails: null }); + expect(findProtectionDetailsList().exists()).toBe(false); + }); + + it('renders the protection details list items', () => { + expect(findProtectionDetailsListItems().at(0).text()).toBe(defaultProps.approvalDetails[0]); + expect(findProtectionDetailsListItems().at(1).text()).toBe(defaultProps.approvalDetails[1]); + }); +}); diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js new file mode 100644 index 00000000000..14ed35f047d --- /dev/null +++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js @@ -0,0 +1,25 @@ +export const branchRulesMockResponse = { + data: { + project: { + id: '123', + __typename: 'Project', + branchRules: { + __typename: 'BranchRuleConnection', + nodes: [ + { + name: 'main', + __typename: 'BranchRule', + }, + { + name: 'test-*', + __typename: 'BranchRule', + }, + ], + }, + }, + }, +}; + +export const propsDataMock = { + projectPath: 'some/project/path', +}; 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 62224612387..13f3eea277a 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 @@ -1,4 +1,4 @@ -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; @@ -23,11 +23,16 @@ describe('ServiceDeskRoot', () => { selectedTemplate: 'Bug', selectedFileTemplateProjectId: 42, templates: ['Bug', 'Documentation'], + publicProject: false, }; - const getAlertText = () => wrapper.find(GlAlert).text(); + const getAlertText = () => wrapper.findComponent(GlAlert).text(); - const createComponent = () => shallowMount(ServiceDeskRoot, { provide: provideData }); + const createComponent = (customInject = {}) => + shallowMount(ServiceDeskRoot, { + provide: { ...provideData, ...customInject }, + stubs: { GlSprintf }, + }); beforeEach(() => { axiosMock = new AxiosMockAdapter(axios); @@ -46,7 +51,7 @@ describe('ServiceDeskRoot', () => { it('is rendered', () => { wrapper = createComponent(); - expect(wrapper.find(ServiceDeskSetting).props()).toEqual({ + expect(wrapper.findComponent(ServiceDeskSetting).props()).toEqual({ customEmail: provideData.customEmail, customEmailEnabled: provideData.customEmailEnabled, incomingEmail: provideData.initialIncomingEmail, @@ -60,12 +65,31 @@ describe('ServiceDeskRoot', () => { }); }); + it('shows alert about email inference when current project is public', () => { + wrapper = createComponent({ + publicProject: true, + }); + + const alertEl = wrapper.find('[data-testid="public-project-alert"]'); + expect(alertEl.exists()).toBe(true); + expect(alertEl.text()).toContain( + 'This project is public. Non-members can guess the Service Desk email address, because it contains the group and project name.', + ); + + 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', + ); + expect(alertBodyLink.text()).toBe('How do I create a custom email address?'); + }); + describe('toggle event', () => { describe('when toggling service desk on', () => { beforeEach(async () => { wrapper = createComponent(); - wrapper.find(ServiceDeskSetting).vm.$emit('toggle', true); + wrapper.findComponent(ServiceDeskSetting).vm.$emit('toggle', true); await waitForPromises(); }); @@ -87,7 +111,7 @@ describe('ServiceDeskRoot', () => { beforeEach(async () => { wrapper = createComponent(); - wrapper.find(ServiceDeskSetting).vm.$emit('toggle', false); + wrapper.findComponent(ServiceDeskSetting).vm.$emit('toggle', false); await waitForPromises(); }); @@ -119,7 +143,7 @@ describe('ServiceDeskRoot', () => { projectKey: 'key', }; - wrapper.find(ServiceDeskSetting).vm.$emit('save', payload); + wrapper.findComponent(ServiceDeskSetting).vm.$emit('save', payload); await waitForPromises(); }); @@ -150,7 +174,7 @@ describe('ServiceDeskRoot', () => { projectKey: 'key', }; - wrapper.find(ServiceDeskSetting).vm.$emit('save', payload); + wrapper.findComponent(ServiceDeskSetting).vm.$emit('save', payload); await waitForPromises(); }); 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 aac1a418142..7c3f4e76ae5 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 @@ -8,13 +8,14 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; describe('ServiceDeskSetting', () => { let wrapper; - const findButton = () => wrapper.find(GlButton); - const findClipboardButton = () => wrapper.find(ClipboardButton); + const findButton = () => wrapper.findComponent(GlButton); + const findClipboardButton = () => wrapper.findComponent(ClipboardButton); const findIncomingEmail = () => wrapper.findByTestId('incoming-email'); const findIncomingEmailLabel = () => wrapper.findByTestId('incoming-email-label'); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); - const findTemplateDropdown = () => wrapper.find(GlDropdown); - const findToggle = () => wrapper.find(GlToggle); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findTemplateDropdown = () => wrapper.findComponent(GlDropdown); + const findToggle = () => wrapper.findComponent(GlToggle); + const findSuffixFormGroup = () => wrapper.findByTestId('suffix-form-group'); const createComponent = ({ props = {} } = {}) => extendedWrapper( @@ -51,6 +52,32 @@ describe('ServiceDeskSetting', () => { expect(findLoadingIcon().exists()).toBe(true); expect(findIncomingEmail().exists()).toBe(false); }); + + it('should display help text', () => { + expect(findSuffixFormGroup().text()).toContain( + 'To add a custom suffix, set up a Service Desk email address', + ); + expect(findSuffixFormGroup().text()).not.toContain( + 'Add a suffix to Service Desk email address', + ); + }); + }); + }); + + describe('when customEmailEnabled', () => { + beforeEach(() => { + wrapper = createComponent({ + props: { customEmailEnabled: true }, + }); + }); + + it('should not display help text', () => { + expect(findSuffixFormGroup().text()).not.toContain( + 'To add a custom suffix, set up a Service Desk email address', + ); + expect(findSuffixFormGroup().text()).toContain( + 'Add a suffix to Service Desk email address', + ); }); }); 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 cdb355f5a9b..6adcfbe8157 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 @@ -7,7 +7,7 @@ import { TEMPLATES } from './mock_data'; describe('ServiceDeskTemplateDropdown', () => { let wrapper; - const findTemplateDropdown = () => wrapper.find(GlDropdown); + const findTemplateDropdown = () => wrapper.findComponent(GlDropdown); const createComponent = ({ props = {} } = {}) => extendedWrapper( @@ -53,7 +53,7 @@ describe('ServiceDeskTemplateDropdown', () => { props: { templates: TEMPLATES }, }); - const headerItems = wrapper.findAll(GlDropdownSectionHeader); + const headerItems = wrapper.findAllComponents(GlDropdownSectionHeader); expect(headerItems).toHaveLength(1); expect(headerItems.at(0).text()).toBe(TEMPLATES[0]); @@ -68,7 +68,7 @@ describe('ServiceDeskTemplateDropdown', () => { const expectedTemplates = templates[1]; - const items = wrapper.findAll(GlDropdownItem); + const items = wrapper.findAllComponents(GlDropdownItem); const dropdownList = expectedTemplates.map((_, index) => items.at(index).text()); expect(items).toHaveLength(expectedTemplates.length); diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js index 882cb2c1199..6c5af5a2625 100644 --- a/spec/frontend/ref/components/ref_selector_spec.js +++ b/spec/frontend/ref/components/ref_selector_spec.js @@ -93,20 +93,20 @@ describe('Ref selector component', () => { const findNoResults = () => wrapper.find('[data-testid="no-results"]'); - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); - const findSearchBox = () => wrapper.find(GlSearchBoxByType); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]'); - const findBranchDropdownItems = () => findBranchesSection().findAll(GlDropdownItem); + const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem); const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0); const findTagsSection = () => wrapper.find('[data-testid="tags-section"]'); - const findTagDropdownItems = () => findTagsSection().findAll(GlDropdownItem); + const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem); const findFirstTagDropdownItem = () => findTagDropdownItems().at(0); const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]'); - const findCommitDropdownItems = () => findCommitsSection().findAll(GlDropdownItem); + const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem); const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0); // @@ -530,13 +530,13 @@ describe('Ref selector component', () => { }); it('renders a checkmark by the selected item', async () => { - expect(findFirstBranchDropdownItem().find(GlIcon).element).toHaveClass( + expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).toHaveClass( 'gl-visibility-hidden', ); await selectFirstBranch(); - expect(findFirstBranchDropdownItem().find(GlIcon).element).not.toHaveClass( + expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).not.toHaveClass( 'gl-visibility-hidden', ); }); @@ -684,7 +684,8 @@ describe('Ref selector component', () => { describe('validation state', () => { const invalidClass = 'gl-inset-border-1-red-500!'; - const isInvalidClassApplied = () => wrapper.find(GlDropdown).props('toggleClass')[invalidClass]; + const isInvalidClassApplied = () => + wrapper.findComponent(GlDropdown).props('toggleClass')[invalidClass]; describe('valid state', () => { describe('when the state prop is not provided', () => { diff --git a/spec/frontend/related_issues/components/related_issuable_input_spec.js b/spec/frontend/related_issues/components/related_issuable_input_spec.js index 7d11e3cffb0..f6a13856042 100644 --- a/spec/frontend/related_issues/components/related_issuable_input_spec.js +++ b/spec/frontend/related_issues/components/related_issuable_input_spec.js @@ -33,7 +33,7 @@ describe('RelatedIssuableInput', () => { it('shows placeholder text', () => { const wrapper = shallowMount(RelatedIssuableInput, { propsData }); - expect(wrapper.find({ ref: 'input' }).element.placeholder).toBe( + expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe( 'Paste issue link or <#issue id>', ); }); @@ -54,7 +54,7 @@ describe('RelatedIssuableInput', () => { }, }); - expect(wrapper.find({ ref: 'input' }).element.value).toBe(''); + expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe(''); }); it('does not have GfmAutoComplete', () => { @@ -85,7 +85,7 @@ describe('RelatedIssuableInput', () => { await nextTick(); - expect(document.activeElement).toBe(wrapper.find({ ref: 'input' }).element); + expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element); }); }); @@ -100,7 +100,7 @@ describe('RelatedIssuableInput', () => { const newInputValue = 'filling in things'; const untouchedRawReferences = newInputValue.trim().split(/\s/); const touchedReference = untouchedRawReferences.pop(); - const input = wrapper.find({ ref: 'input' }); + const input = wrapper.findComponent({ ref: 'input' }); input.element.value = newInputValue; input.element.selectionStart = newInputValue.length; diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js index cb044b9e891..649d8eef6ec 100644 --- a/spec/frontend/releases/components/app_edit_new_spec.js +++ b/spec/frontend/releases/components/app_edit_new_spec.js @@ -220,7 +220,7 @@ describe('Release edit/new component', () => { }); it('renders a checkbox to include release notes', () => { - expect(wrapper.find(GlFormCheckbox).exists()).toBe(true); + expect(wrapper.findComponent(GlFormCheckbox).exists()).toBe(true); }); }); @@ -238,7 +238,7 @@ describe('Release edit/new component', () => { beforeEach(factory); it('renders the asset links portion of the form', () => { - expect(wrapper.find(AssetLinksForm).exists()).toBe(true); + expect(wrapper.findComponent(AssetLinksForm).exists()).toBe(true); }); }); diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js index c2ea6900d6e..9ca25b3b69a 100644 --- a/spec/frontend/releases/components/app_show_spec.js +++ b/spec/frontend/releases/components/app_show_spec.js @@ -36,8 +36,8 @@ describe('Release show component', () => { wrapper = null; }); - const findLoadingSkeleton = () => wrapper.find(ReleaseSkeletonLoader); - const findReleaseBlock = () => wrapper.find(ReleaseBlock); + const findLoadingSkeleton = () => wrapper.findComponent(ReleaseSkeletonLoader); + const findReleaseBlock = () => wrapper.findComponent(ReleaseBlock); const expectLoadingIndicator = () => { it('renders a loading indicator', () => { diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js index 17f079ba5a6..1ff5766b074 100644 --- a/spec/frontend/releases/components/asset_links_form_spec.js +++ b/spec/frontend/releases/components/asset_links_form_spec.js @@ -73,7 +73,7 @@ describe('Release edit component', () => { it('calls the "addEmptyAssetLink" store method when the "Add another link" button is clicked', () => { expect(actions.addEmptyAssetLink).not.toHaveBeenCalled(); - wrapper.find({ ref: 'addAnotherLinkButton' }).vm.$emit('click'); + wrapper.findComponent({ ref: 'addAnotherLinkButton' }).vm.$emit('click'); expect(actions.addEmptyAssetLink).toHaveBeenCalledTimes(1); }); @@ -92,7 +92,7 @@ describe('Release edit component', () => { let newUrl; beforeEach(() => { - input = wrapper.find({ ref: 'urlInput' }).element; + input = wrapper.findComponent({ ref: 'urlInput' }).element; linkIdToUpdate = release.assets.links[0].id; newUrl = 'updated url'; }); @@ -118,7 +118,7 @@ describe('Release edit component', () => { it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => { expectStoreMethodNotToBeCalled(); - wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl); + wrapper.findComponent({ ref: 'urlInput' }).vm.$emit('change', newUrl); expectStoreMethodToBeCalled(); }); @@ -150,7 +150,7 @@ describe('Release edit component', () => { let newName; beforeEach(() => { - input = wrapper.find({ ref: 'nameInput' }).element; + input = wrapper.findComponent({ ref: 'nameInput' }).element; linkIdToUpdate = release.assets.links[0].id; newName = 'updated name'; }); @@ -176,7 +176,7 @@ describe('Release edit component', () => { it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => { expectStoreMethodNotToBeCalled(); - wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName); + wrapper.findComponent({ ref: 'nameInput' }).vm.$emit('change', newName); expectStoreMethodToBeCalled(); }); @@ -208,7 +208,7 @@ describe('Release edit component', () => { expect(actions.updateAssetLinkType).not.toHaveBeenCalled(); - wrapper.find({ ref: 'typeSelect' }).vm.$emit('change', newType); + wrapper.findComponent({ ref: 'typeSelect' }).vm.$emit('change', newType); expect(actions.updateAssetLinkType).toHaveBeenCalledTimes(1); expect(actions.updateAssetLinkType).toHaveBeenCalledWith(expect.anything(), { @@ -225,7 +225,7 @@ describe('Release edit component', () => { }); it('selects the default asset type', () => { - const selected = wrapper.find({ ref: 'typeSelect' }).element.value; + const selected = wrapper.findComponent({ ref: 'typeSelect' }).element.value; expect(selected).toBe(DEFAULT_ASSET_LINK_TYPE); }); diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js index f0d02884305..2db1e9e38a2 100644 --- a/spec/frontend/releases/components/evidence_block_spec.js +++ b/spec/frontend/releases/components/evidence_block_spec.js @@ -32,19 +32,19 @@ describe('Evidence Block', () => { }); it('renders the evidence icon', () => { - expect(wrapper.find(GlIcon).props('name')).toBe('review-list'); + expect(wrapper.findComponent(GlIcon).props('name')).toBe('review-list'); }); it('renders the title for the dowload link', () => { - expect(wrapper.find(GlLink).text()).toBe(`v1.1-evidences-1.json`); + expect(wrapper.findComponent(GlLink).text()).toBe(`v1.1-evidences-1.json`); }); it('renders the correct hover text for the download', () => { - expect(wrapper.find(GlLink).attributes('title')).toBe('Download evidence JSON'); + expect(wrapper.findComponent(GlLink).attributes('title')).toBe('Download evidence JSON'); }); it('renders the correct file link for download', () => { - expect(wrapper.find(GlLink).attributes().download).toBe(`v1.1-evidences-1.json`); + expect(wrapper.findComponent(GlLink).attributes().download).toBe(`v1.1-evidences-1.json`); }); describe('sha text', () => { @@ -62,15 +62,15 @@ describe('Evidence Block', () => { describe('copy to clipboard button', () => { it('renders button', () => { - expect(wrapper.find(ClipboardButton).exists()).toBe(true); + expect(wrapper.findComponent(ClipboardButton).exists()).toBe(true); }); it('renders the correct hover text', () => { - expect(wrapper.find(ClipboardButton).attributes('title')).toBe('Copy evidence SHA'); + expect(wrapper.findComponent(ClipboardButton).attributes('title')).toBe('Copy evidence SHA'); }); it('copies the sha', () => { - expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe( + expect(wrapper.findComponent(ClipboardButton).attributes('data-clipboard-text')).toBe( release.evidences[0].sha, ); }); diff --git a/spec/frontend/releases/components/issuable_stats_spec.js b/spec/frontend/releases/components/issuable_stats_spec.js index 8fc0779da14..3ac75e138ee 100644 --- a/spec/frontend/releases/components/issuable_stats_spec.js +++ b/spec/frontend/releases/components/issuable_stats_spec.js @@ -16,9 +16,11 @@ describe('~/releases/components/issuable_stats.vue', () => { }); }; - const findOpenStatLink = () => wrapper.find('[data-testid="open-stat"]').find(GlLink); - const findMergedStatLink = () => wrapper.find('[data-testid="merged-stat"]').find(GlLink); - const findClosedStatLink = () => wrapper.find('[data-testid="closed-stat"]').find(GlLink); + const findOpenStatLink = () => wrapper.find('[data-testid="open-stat"]').findComponent(GlLink); + const findMergedStatLink = () => + wrapper.find('[data-testid="merged-stat"]').findComponent(GlLink); + const findClosedStatLink = () => + wrapper.find('[data-testid="closed-stat"]').findComponent(GlLink); beforeEach(() => { defaultProps = { diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js index c63689e11ac..4f94e4dfd55 100644 --- a/spec/frontend/releases/components/release_block_assets_spec.js +++ b/spec/frontend/releases/components/release_block_assets_spec.js @@ -44,7 +44,7 @@ describe('Release block assets', () => { }); it('renders the accordion as expanded by default', () => { - const accordion = wrapper.find(GlCollapse); + const accordion = wrapper.findComponent(GlCollapse); expect(accordion.exists()).toBe(true); expect(accordion.isVisible()).toBe(true); diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js index 848e802df4b..8f4efad197f 100644 --- a/spec/frontend/releases/components/release_block_footer_spec.js +++ b/spec/frontend/releases/components/release_block_footer_spec.js @@ -38,16 +38,16 @@ describe('Release block footer', () => { }); const commitInfoSection = () => wrapper.find('.js-commit-info'); - const commitInfoSectionLink = () => commitInfoSection().find(GlLink); + const commitInfoSectionLink = () => commitInfoSection().findComponent(GlLink); const tagInfoSection = () => wrapper.find('.js-tag-info'); - const tagInfoSectionLink = () => tagInfoSection().find(GlLink); + const tagInfoSectionLink = () => tagInfoSection().findComponent(GlLink); const authorDateInfoSection = () => wrapper.find('.js-author-date-info'); describe('with all props provided', () => { beforeEach(() => factory()); it('renders the commit icon', () => { - const commitIcon = commitInfoSection().find(GlIcon); + const commitIcon = commitInfoSection().findComponent(GlIcon); expect(commitIcon.exists()).toBe(true); expect(commitIcon.props('name')).toBe('commit'); @@ -62,14 +62,14 @@ describe('Release block footer', () => { }); it('renders the tag icon', () => { - const commitIcon = tagInfoSection().find(GlIcon); + const commitIcon = tagInfoSection().findComponent(GlIcon); expect(commitIcon.exists()).toBe(true); expect(commitIcon.props('name')).toBe('tag'); }); it('renders the tag name with a link', () => { - const commitLink = tagInfoSection().find(GlLink); + const commitLink = tagInfoSection().findComponent(GlLink); expect(commitLink.exists()).toBe(true); expect(commitLink.text()).toBe(release.tagName); @@ -120,7 +120,7 @@ describe('Release block footer', () => { }); it("renders a link to the author's profile", () => { - const authorLink = authorDateInfoSection().find(GlLink); + const authorLink = authorDateInfoSection().findComponent(GlLink); expect(authorLink.exists()).toBe(true); expect(authorLink.attributes('href')).toBe(release.author.webUrl); diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js index c9921185bad..fc421776d60 100644 --- a/spec/frontend/releases/components/release_block_header_spec.js +++ b/spec/frontend/releases/components/release_block_header_spec.js @@ -30,7 +30,7 @@ describe('Release block header', () => { }); const findHeader = () => wrapper.find('h2'); - const findHeaderLink = () => findHeader().find(GlLink); + const findHeaderLink = () => findHeader().findComponent(GlLink); const findEditButton = () => wrapper.find('.js-edit-button'); const findBadge = () => wrapper.findComponent(GlBadge); 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 84a0080965b..541d487091c 100644 --- a/spec/frontend/releases/components/release_block_milestone_info_spec.js +++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js @@ -43,7 +43,7 @@ describe('Release block milestone info', () => { }); it('renders a progress bar that displays the correct percentage', () => { - const progressBar = milestoneProgressBarContainer().find(GlProgressBar); + const progressBar = milestoneProgressBarContainer().findComponent(GlProgressBar); expect(progressBar.exists()).toBe(true); expect(progressBar.attributes()).toEqual( @@ -58,7 +58,7 @@ describe('Release block milestone info', () => { expect(milestoneListContainer().text()).toMatchInterpolatedText('Milestones 12.3 • 12.4'); milestones.forEach((m, i) => { - const milestoneLink = milestoneListContainer().findAll(GlLink).at(i); + const milestoneLink = milestoneListContainer().findAllComponents(GlLink).at(i); expect(milestoneLink.text()).toBe(m.title); expect(milestoneLink.attributes('href')).toBe(m.webUrl); @@ -72,7 +72,7 @@ describe('Release block milestone info', () => { expect(issuesContainerText).toContain(`Issues ${totalIssueCount}`); - const badge = issuesContainer().find(GlBadge); + const badge = issuesContainer().findComponent(GlBadge); expect(badge.text()).toBe(totalIssueCount.toString()); expect(issuesContainerText).toContain('Open: 5 • Closed: 4'); @@ -107,7 +107,7 @@ describe('Release block milestone info', () => { }); const clickShowMoreFewerButton = async () => { - milestoneListContainer().find(GlButton).trigger('click'); + milestoneListContainer().findComponent(GlButton).trigger('click'); await nextTick(); }; diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index 17e2af687a6..096c3db8902 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -74,7 +74,7 @@ describe('Release block', () => { }); it('renders the footer', () => { - expect(wrapper.find(ReleaseBlockFooter).exists()).toBe(true); + expect(wrapper.findComponent(ReleaseBlockFooter).exists()).toBe(true); }); }); @@ -133,7 +133,7 @@ describe('Release block', () => { describe('evidence block', () => { it('renders the evidence block when the evidence is available', () => { return factory(release).then(() => { - expect(wrapper.find(EvidenceBlock).exists()).toBe(true); + expect(wrapper.findComponent(EvidenceBlock).exists()).toBe(true); }); }); @@ -141,7 +141,7 @@ describe('Release block', () => { release.evidences = []; return factory(release).then(() => { - expect(wrapper.find(EvidenceBlock).exists()).toBe(false); + expect(wrapper.findComponent(EvidenceBlock).exists()).toBe(false); }); }); }); diff --git a/spec/frontend/releases/components/release_skeleton_loader_spec.js b/spec/frontend/releases/components/release_skeleton_loader_spec.js index 7f81081ff6c..76dfe0d9777 100644 --- a/spec/frontend/releases/components/release_skeleton_loader_spec.js +++ b/spec/frontend/releases/components/release_skeleton_loader_spec.js @@ -10,6 +10,6 @@ describe('release_skeleton_loader.vue', () => { }); it('renders a GlSkeletonLoader', () => { - expect(wrapper.find(GlSkeletonLoader).exists()).toBe(true); + expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true); }); }); diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js index f45a28392b7..8105aa4f6f2 100644 --- a/spec/frontend/releases/components/tag_field_exsting_spec.js +++ b/spec/frontend/releases/components/tag_field_exsting_spec.js @@ -20,7 +20,7 @@ describe('releases/components/tag_field_existing', () => { }); }; - const findInput = () => wrapper.find(GlFormInput); + const findInput = () => wrapper.findComponent(GlFormInput); const findHelp = () => wrapper.find('[data-testid="tag-name-help"]'); beforeEach(() => { diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js index 9f500c318ea..b8047cae8c2 100644 --- a/spec/frontend/releases/components/tag_field_new_spec.js +++ b/spec/frontend/releases/components/tag_field_new_spec.js @@ -79,12 +79,12 @@ describe('releases/components/tag_field_new', () => { }); const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]'); - const findTagNameDropdown = () => findTagNameFormGroup().find(RefSelectorStub); + const findTagNameDropdown = () => findTagNameFormGroup().findComponent(RefSelectorStub); const findCreateFromFormGroup = () => wrapper.find('[data-testid="create-from-field"]'); - const findCreateFromDropdown = () => findCreateFromFormGroup().find(RefSelectorStub); + const findCreateFromDropdown = () => findCreateFromFormGroup().findComponent(RefSelectorStub); - const findCreateNewTagOption = () => wrapper.find(GlDropdownItem); + const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem); describe('"Tag name" field', () => { describe('rendering and behavior', () => { diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js index e7b9aa4abbb..85a40f02c53 100644 --- a/spec/frontend/releases/components/tag_field_spec.js +++ b/spec/frontend/releases/components/tag_field_spec.js @@ -21,8 +21,8 @@ describe('releases/components/tag_field', () => { wrapper = shallowMount(TagField, { store }); }; - const findTagFieldNew = () => wrapper.find(TagFieldNew); - const findTagFieldExisting = () => wrapper.find(TagFieldExisting); + const findTagFieldNew = () => wrapper.findComponent(TagFieldNew); + const findTagFieldExisting = () => wrapper.findComponent(TagFieldExisting); afterEach(() => { wrapper.destroy(); diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js index ce3b690213c..48fba3adb24 100644 --- a/spec/frontend/releases/stores/modules/detail/actions_spec.js +++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js @@ -352,6 +352,32 @@ describe('Release edit/new actions', () => { }); }); + describe('when the GraphQL returns errors as data', () => { + beforeEach(() => { + gqClient.mutate.mockResolvedValue({ data: { releaseCreate: { errors: ['Yikes!'] } } }); + }); + + it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => { + return testAction(actions.createRelease, undefined, state, [ + { + type: types.RECEIVE_SAVE_RELEASE_ERROR, + payload: expect.any(Error), + }, + ]); + }); + + it(`shows a flash message`, () => { + return actions + .createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} }) + .then(() => { + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Yikes!', + }); + }); + }); + }); + describe('when the GraphQL network request fails', () => { beforeEach(() => { gqClient.mutate.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 4ac6eaebaa2..2982dc5c46c 100644 --- a/spec/frontend/releases/stores/modules/detail/getters_spec.js +++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js @@ -320,7 +320,9 @@ describe('Release edit/new getters', () => { it(description, () => { const expectedVariablesObject = { input: expect.objectContaining(expectedVariables) }; - const actualVariables = getters.releaseUpdateMutatationVariables(state); + const actualVariables = getters.releaseUpdateMutatationVariables(state, { + releasedAtChanged: Object.hasOwn(state.release, 'releasedAt'), + }); expect(actualVariables).toEqual(expectedVariablesObject); }); @@ -409,4 +411,19 @@ describe('Release edit/new getters', () => { }, ); }); + + describe('releasedAtChange', () => { + it('is false if the released at date has not changed', () => { + const date = new Date(); + expect( + getters.releasedAtChanged({ originalReleasedAt: date, release: { releasedAt: date } }), + ).toBe(false); + }); + + it('is true if the date changed', () => { + const originalReleasedAt = new Date(); + const releasedAt = new Date(2022, 5, 30); + expect(getters.releasedAtChanged({ originalReleasedAt, release: { releasedAt } })).toBe(true); + }); + }); }); diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js index 60b57c7a7ff..8bbf550b77d 100644 --- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js +++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js @@ -36,6 +36,12 @@ describe('Release edit/new mutations', () => { }, }); }); + + it('saves the original released at date as well', () => { + mutations[types.INITIALIZE_EMPTY_RELEASE](state); + + expect(state.originalReleasedAt).toEqual(new Date()); + }); }); describe(`${types.REQUEST_RELEASE}`, () => { @@ -57,6 +63,7 @@ describe('Release edit/new mutations', () => { expect(state.release).toEqual(release); expect(state.originalRelease).toEqual(release); + expect(state.originalReleasedAt).toEqual(release.releasedAt); }); }); diff --git a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js index ddabb7194cb..d835ca4c733 100644 --- a/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js +++ b/spec/frontend/reports/accessibility_report/components/accessibility_issue_body_spec.js @@ -41,7 +41,7 @@ describe('CustomMetricsForm', () => { }); it('Displays the issue message', () => { - const description = wrapper.find({ ref: 'accessibility-issue-description' }).text(); + const description = wrapper.findComponent({ ref: 'accessibility-issue-description' }).text(); expect(description).toContain(`Message: ${issue.message}`); }); @@ -49,7 +49,7 @@ describe('CustomMetricsForm', () => { describe('When an issue code is present', () => { it('Creates the correct URL for learning more about the issue code', () => { const learnMoreUrl = wrapper - .find({ ref: 'accessibility-issue-learn-more' }) + .findComponent({ ref: 'accessibility-issue-learn-more' }) .attributes('href'); expect(learnMoreUrl).toBe(issue.learnMoreUrl); @@ -66,7 +66,7 @@ describe('CustomMetricsForm', () => { it('Creates a URL leading to the overview documentation page', () => { const learnMoreUrl = wrapper - .find({ ref: 'accessibility-issue-learn-more' }) + .findComponent({ ref: 'accessibility-issue-learn-more' }) .attributes('href'); expect(learnMoreUrl).toBe('https://www.w3.org/TR/WCAG20-TECHS/Overview.html'); @@ -83,7 +83,7 @@ describe('CustomMetricsForm', () => { it('Creates a URL leading to the overview documentation page', () => { const learnMoreUrl = wrapper - .find({ ref: 'accessibility-issue-learn-more' }) + .findComponent({ ref: 'accessibility-issue-learn-more' }) .attributes('href'); expect(learnMoreUrl).toBe('https://www.w3.org/TR/WCAG20-TECHS/Overview.html'); diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js index 34b1cdd92bc..9d3535291eb 100644 --- a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js +++ b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js @@ -114,7 +114,7 @@ describe('Grouped accessibility reports app', () => { }); it('renders custom accessibility issue body', () => { - const issueBody = wrapper.find(AccessibilityIssueBody); + const issueBody = wrapper.findComponent(AccessibilityIssueBody); expect(issueBody.props('issue').code).toBe(mockReport.new_errors[0].code); expect(issueBody.props('issue').message).toBe(mockReport.new_errors[0].message); diff --git a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js index 17f07ac2b8f..c32b52d9e77 100644 --- a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js +++ b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js @@ -8,7 +8,7 @@ describe('code quality issue body issue body', () => { let wrapper; const findSeverityIcon = () => wrapper.findByTestId('codequality-severity-icon'); - const findGlIcon = () => wrapper.find(GlIcon); + const findGlIcon = () => wrapper.findComponent(GlIcon); const codequalityIssue = { name: diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js index b61b65c2713..962ff068b92 100644 --- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js +++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js @@ -30,7 +30,7 @@ describe('Grouped code quality reports app', () => { }; const findWidget = () => wrapper.find('.js-codequality-widget'); - const findIssueBody = () => wrapper.find(CodequalityIssueBody); + const findIssueBody = () => wrapper.findComponent(CodequalityIssueBody); beforeEach(() => { const { state, ...storeConfig } = getStoreConfig(); diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/reports/components/grouped_issues_list_spec.js index 95ef0bcbcc7..6c0275dc47d 100644 --- a/spec/frontend/reports/components/grouped_issues_list_spec.js +++ b/spec/frontend/reports/components/grouped_issues_list_spec.js @@ -30,7 +30,7 @@ describe('Grouped Issues List', () => { }, }); - expect(wrapper.find(SmartVirtualList).props()).toMatchSnapshot(); + expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot(); }); describe('without data', () => { @@ -43,7 +43,7 @@ describe('Grouped Issues List', () => { }); it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => { - expect(wrapper.find(ReportItem).exists()).toBe(false); + expect(wrapper.findComponent(ReportItem).exists()).toBe(false); }); }); @@ -67,7 +67,7 @@ describe('Grouped Issues List', () => { propsData: { [`${issueName}Issues`]: issues }, }); - expect(wrapper.findAll(ReportItem)).toHaveLength(issues.length); + expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length); }); it('renders a report item with the correct props', () => { @@ -81,7 +81,7 @@ describe('Grouped Issues List', () => { }, }); - expect(wrapper.find(ReportItem).props()).toMatchSnapshot(); + expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot(); }); }); }); diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js index a7243c5377b..b52c163eb26 100644 --- a/spec/frontend/reports/components/report_item_spec.js +++ b/spec/frontend/reports/components/report_item_spec.js @@ -16,7 +16,7 @@ describe('ReportItem', () => { }, }); - expect(wrapper.find(IssueStatusIcon).exists()).toBe(false); + expect(wrapper.findComponent(IssueStatusIcon).exists()).toBe(false); }); it('shows status icon when unspecified', () => { @@ -28,7 +28,7 @@ describe('ReportItem', () => { }, }); - expect(wrapper.find(IssueStatusIcon).exists()).toBe(true); + expect(wrapper.findComponent(IssueStatusIcon).exists()).toBe(true); }); }); }); diff --git a/spec/frontend/reports/grouped_test_report/components/modal_spec.js b/spec/frontend/reports/grouped_test_report/components/modal_spec.js index 3de81f754fd..e8564d2428d 100644 --- a/spec/frontend/reports/grouped_test_report/components/modal_spec.js +++ b/spec/frontend/reports/grouped_test_report/components/modal_spec.js @@ -40,7 +40,9 @@ describe('Grouped Test Reports Modal', () => { }); it('renders code block', () => { - expect(wrapper.find(CodeBlock).props().code).toEqual(modalDataStructure.system_output.value); + expect(wrapper.findComponent(CodeBlock).props().code).toEqual( + modalDataStructure.system_output.value, + ); }); it('renders link', () => { diff --git a/spec/frontend/reports/grouped_test_report/store/actions_spec.js b/spec/frontend/reports/grouped_test_report/store/actions_spec.js index 5876827c548..7469c31cf84 100644 --- a/spec/frontend/reports/grouped_test_report/store/actions_spec.js +++ b/spec/frontend/reports/grouped_test_report/store/actions_spec.js @@ -61,7 +61,7 @@ describe('Reports Store Actions', () => { }); describe('success', () => { - it('dispatches requestReports and receiveReportsSuccess ', () => { + it('dispatches requestReports and receiveReportsSuccess', () => { mock .onGet(`${TEST_HOST}/endpoint.json`) .replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] }); @@ -89,7 +89,7 @@ describe('Reports Store Actions', () => { mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); }); - it('dispatches requestReports and receiveReportsError ', () => { + it('dispatches requestReports and receiveReportsError', () => { return testAction( fetchReports, null, diff --git a/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json b/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json new file mode 100644 index 00000000000..28ee7d194b9 --- /dev/null +++ b/spec/frontend/reports/mock_data/new_failures_with_null_files_report.json @@ -0,0 +1,40 @@ +{ + "summary": { "total": 11, "resolved": 0, "errored": 0, "failed": 2 }, + "suites": [ + { + "name": "rspec:pg", + "summary": { "total": 8, "resolved": 0, "errored": 0, "failed": 2 }, + "new_failures": [ + { + "result": "failure", + "name": "Test#sum when a is 1 and b is 2 returns summary", + "file": null, + "execution_time": 0.009411, + "system_output": "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in '" + }, + { + "result": "failure", + "name": "Test#sum when a is 100 and b is 200 returns summary", + "file": null, + "execution_time": 0.000162, + "system_output": "Failure/Error: is_expected.to eq(300)\n\n expected: 300\n got: -100\n\n (compared using ==)\n./spec/test_spec.rb:21:in `block (4 levels) in '" + } + ], + "resolved_failures": [], + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + }, + { + "name": "java ant", + "summary": { "total": 3, "resolved": 0, "errored": 0, "failed": 0 }, + "new_failures": [], + "resolved_failures": [], + "existing_failures": [], + "new_errors": [], + "resolved_errors": [], + "existing_errors": [] + } + ] +} 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 cb56f392ec9..01494cb6a24 100644 --- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap +++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap @@ -20,7 +20,8 @@ exports[`Repository last commit component renders commit widget 1`] = ` class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-flex-grow-1 gl-min-w-0" >
{ }); it('renders both the replace and delete button', () => { - expect(wrapper.findAll(GlButton)).toHaveLength(2); + expect(wrapper.findAllComponents(GlButton)).toHaveLength(2); }); it('renders the buttons in the correct order', () => { - expect(wrapper.findAll(GlButton).at(0).text()).toBe('Replace'); - expect(wrapper.findAll(GlButton).at(1).text()).toBe('Delete'); + expect(wrapper.findAllComponents(GlButton).at(0).text()).toBe('Replace'); + expect(wrapper.findAllComponents(GlButton).at(1).text()).toBe('Delete'); }); it('triggers the UploadBlobModal from the replace button', () => { @@ -97,14 +97,14 @@ describe('BlobButtonGroup component', () => { findReplaceButton().trigger('click'); expect(findUploadBlobModal().vm.show).not.toHaveBeenCalled(); - expect(wrapper.emitted().fork).toBeTruthy(); + 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(wrapper.emitted().fork).toBeTruthy(); + 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 0f7cf4e61b2..6ece72c41bb 100644 --- a/spec/frontend/repository/components/blob_content_viewer_spec.js +++ b/spec/frontend/repository/components/blob_content_viewer_spec.js @@ -17,7 +17,8 @@ import { loadViewer } from '~/repository/components/blob_viewers'; import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue'; import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue'; import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue'; -import blobInfoQuery from '~/repository/queries/blob_info.query.graphql'; +import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql'; +import projectInfoQuery from '~/repository/queries/project_info.query.graphql'; import userInfoQuery from '~/repository/queries/user_info.query.graphql'; import applicationInfoQuery from '~/repository/queries/application_info.query.graphql'; import CodeIntelligence from '~/code_navigation/components/app.vue'; @@ -45,8 +46,9 @@ jest.mock('~/lib/utils/common_utils'); jest.mock('~/blob/line_highlighter'); let wrapper; -let mockResolver; +let blobInfoMockResolver; let userInfoMockResolver; +let projectInfoMockResolver; let applicationInfoMockResolver; const mockAxios = new MockAdapter(axios); @@ -74,22 +76,40 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute highlightJs = true, } = mockData; - const project = { + const blobInfo = { ...projectMock, + repository: { + empty, + blobs: { nodes: [blob] }, + }, + }; + + const projectInfo = { + __typename: 'Project', + id: '123', userPermissions: { pushCode, forkProject, downloadCode, createMergeRequestIn, }, - repository: { - empty, - blobs: { nodes: [blob] }, + pathLocks: { + nodes: [ + { + id: 'test', + path: 'locked_file.js', + user: { id: '123', username: 'root' }, + }, + ], }, }; - mockResolver = jest.fn().mockResolvedValue({ - data: { isBinary, project }, + projectInfoMockResolver = jest.fn().mockResolvedValue({ + data: { project: projectInfo }, + }); + + blobInfoMockResolver = jest.fn().mockResolvedValue({ + data: { isBinary, project: blobInfo }, }); userInfoMockResolver = jest.fn().mockResolvedValue({ @@ -101,8 +121,9 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute }); const fakeApollo = createMockApollo([ - [blobInfoQuery, mockResolver], + [blobInfoQuery, blobInfoMockResolver], [userInfoQuery, userInfoMockResolver], + [projectInfoQuery, projectInfoMockResolver], [applicationInfoQuery, applicationInfoMockResolver], ]); @@ -129,7 +150,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ project, isBinary }); + wrapper.setData({ project: blobInfo, isBinary }); await waitForPromises(); }; @@ -504,14 +525,16 @@ describe('Blob content viewer component', () => { async ({ highlightJs, shouldFetchRawText }) => { await createComponent({ highlightJs }); - expect(mockResolver).toHaveBeenCalledWith(expect.objectContaining({ shouldFetchRawText })); + expect(blobInfoMockResolver).toHaveBeenCalledWith( + expect.objectContaining({ shouldFetchRawText }), + ); }, ); it('is called with originalBranch value if the prop has a value', async () => { await createComponent({ inject: { originalBranch: 'some-branch' } }); - expect(mockResolver).toHaveBeenCalledWith( + expect(blobInfoMockResolver).toHaveBeenCalledWith( expect.objectContaining({ ref: 'some-branch', }), @@ -521,7 +544,7 @@ describe('Blob content viewer component', () => { it('is called with ref value if the originalBranch prop has no value', async () => { await createComponent(); - expect(mockResolver).toHaveBeenCalledWith( + expect(blobInfoMockResolver).toHaveBeenCalledWith( expect.objectContaining({ ref: 'default-ref', }), diff --git a/spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js index 7d43e4e660b..c6b9737dde2 100644 --- a/spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js +++ b/spec/frontend/repository/components/blob_viewers/csv_viewer_spec.js @@ -21,7 +21,7 @@ describe('CSV Viewer', () => { it('renders a Source Editor component', () => { createComponent(); expect(findCsvViewerComp().exists()).toBe(true); - expect(findCsvViewerComp().props('remoteFile')).toBeTruthy(); + expect(findCsvViewerComp().props('remoteFile')).toBe(true); expect(findCsvViewerComp().props('csv')).toBe(DEFAULT_BLOB_DATA.rawPath); }); }); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index 40b32904589..c2f34f79f89 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -39,8 +39,8 @@ describe('Repository breadcrumbs component', () => { }); }; - const findUploadBlobModal = () => wrapper.find(UploadBlobModal); - const findNewDirectoryModal = () => wrapper.find(NewDirectoryModal); + const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal); + const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal); afterEach(() => { wrapper.destroy(); @@ -55,7 +55,7 @@ describe('Repository breadcrumbs component', () => { `('renders $linkCount links for path $path', ({ path, linkCount }) => { factory(path); - expect(wrapper.findAll(RouterLinkStub).length).toEqual(linkCount); + expect(wrapper.findAllComponents(RouterLinkStub).length).toEqual(linkCount); }); it.each` @@ -68,14 +68,14 @@ describe('Repository breadcrumbs component', () => { 'links to the correct router path when routeName is $routeName', ({ routeName, path, linkTo }) => { factory(path, {}, { name: routeName }); - expect(wrapper.findAll(RouterLinkStub).at(3).props('to')).toEqual(linkTo); + expect(wrapper.findAllComponents(RouterLinkStub).at(3).props('to')).toEqual(linkTo); }, ); it('escapes hash in directory path', () => { factory('app/assets/javascripts#'); - expect(wrapper.findAll(RouterLinkStub).at(3).props('to')).toEqual( + expect(wrapper.findAllComponents(RouterLinkStub).at(3).props('to')).toEqual( '/-/tree/app/assets/javascripts%23', ); }); @@ -83,7 +83,9 @@ describe('Repository breadcrumbs component', () => { it('renders last link as active', () => { factory('app/assets'); - expect(wrapper.findAll(RouterLinkStub).at(2).attributes('aria-current')).toEqual('page'); + expect(wrapper.findAllComponents(RouterLinkStub).at(2).attributes('aria-current')).toEqual( + 'page', + ); }); it('does not render add to tree dropdown when permissions are false', async () => { @@ -95,7 +97,7 @@ describe('Repository breadcrumbs component', () => { await nextTick(); - expect(wrapper.find(GlDropdown).exists()).toBe(false); + expect(wrapper.findComponent(GlDropdown).exists()).toBe(false); }); it.each` @@ -109,7 +111,7 @@ 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.find(GlDropdown).exists()).toBe(isRendered); + expect(wrapper.findComponent(GlDropdown).exists()).toBe(isRendered); }, ); @@ -122,7 +124,7 @@ describe('Repository breadcrumbs component', () => { await nextTick(); - expect(wrapper.find(GlDropdown).exists()).toBe(true); + expect(wrapper.findComponent(GlDropdown).exists()).toBe(true); }); describe('renders the upload blob modal', () => { diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js index 785783b2e75..b5996816ad8 100644 --- a/spec/frontend/repository/components/delete_blob_modal_spec.js +++ b/spec/frontend/repository/components/delete_blob_modal_spec.js @@ -84,7 +84,7 @@ describe('DeleteBlobModal', () => { ${GlToggle} | ${'true'} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} ${GlToggle} | ${undefined} | ${true} | ${'same-branch'} | ${'same-branch'} | ${false} `( - 'has the correct form fields ', + 'has the correct form fields', ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => { createComponent({ canPushCode, diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js index 3783b34e33a..bf9528953b6 100644 --- a/spec/frontend/repository/components/last_commit_spec.js +++ b/spec/frontend/repository/components/last_commit_spec.js @@ -190,11 +190,16 @@ describe('Repository last commit component', () => { }); it('expands commit description when clicking expander', async () => { + expect(findCommitRowDescription().classes('d-block')).toBe(false); + expect(findTextExpander().classes('open')).toBe(false); + expect(findTextExpander().props('selected')).toBe(false); + findTextExpander().vm.$emit('click'); await nextTick(); - expect(findCommitRowDescription().isVisible()).toBe(true); - expect(findTextExpander().classes()).toContain('open'); + expect(findCommitRowDescription().classes('d-block')).toBe(true); + expect(findTextExpander().classes('open')).toBe(true); + expect(findTextExpander().props('selected')).toBe(true); }); }); diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js index e1c50d63851..aaf751a9a8d 100644 --- a/spec/frontend/repository/components/new_directory_modal_spec.js +++ b/spec/frontend/repository/components/new_directory_modal_spec.js @@ -107,7 +107,7 @@ describe('NewDirectoryModal', () => { ${findMrToggle} | ${'true'} | ${true} | ${'new-target-branch'} | ${'master'} | ${true} ${findMrToggle} | ${'true'} | ${true} | ${'master'} | ${'master'} | ${true} `( - 'has the correct form fields ', + 'has the correct form fields', ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => { createComponent({ canPushCode, diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js index 0d9bfc62ed5..e4eba65795e 100644 --- a/spec/frontend/repository/components/preview/index_spec.js +++ b/spec/frontend/repository/components/preview/index_spec.js @@ -68,6 +68,6 @@ describe('Repository file preview component', () => { vm.setData({ loading: 1 }); await nextTick(); - expect(vm.find(GlLoadingIcon).exists()).toBe(true); + expect(vm.findComponent(GlLoadingIcon).exists()).toBe(true); }); }); diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js index ff0371b5c07..697d2dcc7f5 100644 --- a/spec/frontend/repository/components/table/index_spec.js +++ b/spec/frontend/repository/components/table/index_spec.js @@ -38,6 +38,7 @@ const MOCK_BLOBS = [ const MOCK_COMMITS = [ { fileName: 'blob.md', + filePath: 'test_dir/blob.md', type: 'blob', commit: { message: 'Updated blob.md', @@ -45,6 +46,7 @@ const MOCK_COMMITS = [ }, { fileName: 'blob2.md', + filePath: 'test_dir/blob2.md', type: 'blob', commit: { message: 'Updated blob2.md', @@ -52,11 +54,20 @@ const MOCK_COMMITS = [ }, { fileName: 'blob3.md', + filePath: 'test_dir/blob3.md', type: 'blob', commit: { message: 'Updated blob3.md', }, }, + { + fileName: 'root_blob.md', + filePath: '/root_blob.md', + type: 'blob', + commit: { + message: 'Updated root_blob.md', + }, + }, ]; function factory({ path, isLoading = false, hasMore = true, entries = {}, commits = [] }) { @@ -77,6 +88,8 @@ function factory({ path, isLoading = false, hasMore = true, entries = {}, commit }); } +const findTableRows = () => vm.findAllComponents(TableRow); + describe('Repository table component', () => { afterEach(() => { vm.destroy(); @@ -108,14 +121,14 @@ describe('Repository table component', () => { it('renders table rows', () => { factory({ - path: '/', + path: 'test_dir', entries: { blobs: MOCK_BLOBS, }, commits: MOCK_COMMITS, }); - const rows = vm.findAll(TableRow); + const rows = findTableRows(); expect(rows.length).toEqual(3); expect(rows.at(2).attributes().mode).toEqual('120000'); @@ -123,6 +136,28 @@ describe('Repository table component', () => { expect(rows.at(2).props().commitInfo).toEqual(MOCK_COMMITS[2]); }); + it('renders correct commit info for blobs in the root', () => { + factory({ + path: '/', + entries: { + blobs: [ + { + id: '126abc', + sha: '126abc', + flatPath: 'root_blob.md', + name: 'root_blob.md', + type: 'blob', + webUrl: 'http://test.com', + mode: '120000', + }, + ], + }, + commits: MOCK_COMMITS, + }); + + expect(findTableRows().at(0).props().commitInfo).toEqual(MOCK_COMMITS[3]); + }); + describe('Show more button', () => { const showMoreButton = () => vm.find(GlButton); diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js index bf024baa627..505ff7f3dd6 100644 --- a/spec/frontend/repository/components/upload_blob_modal_spec.js +++ b/spec/frontend/repository/components/upload_blob_modal_spec.js @@ -217,7 +217,7 @@ describe('UploadBlobModal', () => { createComponent(); }); - it('displays the default "Upload new file" modal title ', () => { + it('displays the default "Upload new file" modal title', () => { expect(findModal().props('title')).toBe('Upload new file'); }); diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js index e3b4dcb8acc..c1309539b6d 100644 --- a/spec/frontend/repository/log_tree_spec.js +++ b/spec/frontend/repository/log_tree_spec.js @@ -30,7 +30,7 @@ describe('resolveCommit', () => { { fileName: 'index.js', filePath: '/app/assets/index.js' }, ]; - resolveCommit(commits, '', resolver); + resolveCommit(commits, '/', resolver); expect(resolver.resolve).toHaveBeenCalledWith({ fileName: 'index.js', @@ -107,14 +107,14 @@ describe('fetchLogsTree', () => { })); it('calls entry resolver', () => - fetchLogsTree(client, '', '0', resolver).then(() => { + fetchLogsTree(client, 'test', '0', resolver).then(() => { expect(resolver.resolve).toHaveBeenCalledWith( expect.objectContaining({ __typename: 'LogTreeCommit', commitPath: 'https://test.com', committedDate: '2019-01-01', fileName: 'index.js', - filePath: '/index.js', + filePath: 'test/index.js', message: 'testing message', sha: '123', }), @@ -122,7 +122,7 @@ describe('fetchLogsTree', () => { })); it('writes query to client', async () => { - await fetchLogsTree(client, '', '0', resolver); + await fetchLogsTree(client, '/', '0', resolver); expect(client.readQuery({ query: commitsQuery })).toEqual({ commits: [ expect.objectContaining({ diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index 4db295fe0b7..cda47a5b0a5 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -1,4 +1,5 @@ export const simpleViewerMock = { + __typename: 'RepositoryBlob', id: '1', name: 'some_file.js', size: 123, diff --git a/spec/frontend/repository/utils/commit_spec.js b/spec/frontend/repository/utils/commit_spec.js index b3dd5118308..65728e9cb24 100644 --- a/spec/frontend/repository/utils/commit_spec.js +++ b/spec/frontend/repository/utils/commit_spec.js @@ -15,7 +15,7 @@ const mockData = [ describe('normalizeData', () => { it('normalizes data into LogTreeCommit object', () => { - expect(normalizeData(mockData, '')).toEqual([ + expect(normalizeData(mockData, '/')).toEqual([ { sha: '123', message: 'testing message', diff --git a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js b/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js deleted file mode 100644 index ffe3599ac64..00000000000 --- a/spec/frontend/runner/admin_runner_edit/admin_runner_edit_app_spec.js +++ /dev/null @@ -1,113 +0,0 @@ -import { mount, shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createAlert } from '~/flash'; - -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import RunnerHeader from '~/runner/components/runner_header.vue'; -import RunnerUpdateForm from '~/runner/components/runner_update_form.vue'; -import runnerFormQuery from '~/runner/graphql/edit/runner_form.query.graphql'; -import AdminRunnerEditApp from '~//runner/admin_runner_edit/admin_runner_edit_app.vue'; -import { captureException } from '~/runner/sentry_utils'; - -import { runnerFormData } from '../mock_data'; - -jest.mock('~/flash'); -jest.mock('~/runner/sentry_utils'); - -const mockRunner = runnerFormData.data.runner; -const mockRunnerGraphqlId = mockRunner.id; -const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`; -const mockRunnerPath = `/admin/runners/${mockRunnerId}`; - -Vue.use(VueApollo); - -describe('AdminRunnerEditApp', () => { - let wrapper; - let mockRunnerQuery; - - const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); - const findRunnerUpdateForm = () => wrapper.findComponent(RunnerUpdateForm); - - const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { - wrapper = mountFn(AdminRunnerEditApp, { - apolloProvider: createMockApollo([[runnerFormQuery, mockRunnerQuery]]), - propsData: { - runnerId: mockRunnerId, - runnerPath: mockRunnerPath, - ...props, - }, - }); - - return waitForPromises(); - }; - - beforeEach(() => { - mockRunnerQuery = jest.fn().mockResolvedValue(runnerFormData); - }); - - afterEach(() => { - mockRunnerQuery.mockReset(); - wrapper.destroy(); - }); - - it('expect GraphQL ID to be requested', async () => { - await createComponentWithApollo(); - - expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId }); - }); - - it('displays the runner id and creation date', async () => { - await createComponentWithApollo({ mountFn: mount }); - - expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`); - expect(findRunnerHeader().text()).toContain('created'); - }); - - it('displays the runner type and status', async () => { - await createComponentWithApollo({ mountFn: mount }); - - expect(findRunnerHeader().text()).toContain(`never contacted`); - expect(findRunnerHeader().text()).toContain(`shared`); - }); - - it('displays a loading runner form', () => { - createComponentWithApollo(); - - expect(findRunnerUpdateForm().props()).toMatchObject({ - runner: null, - loading: true, - runnerPath: mockRunnerPath, - }); - }); - - it('displays the runner form', async () => { - await createComponentWithApollo(); - - expect(findRunnerUpdateForm().props()).toMatchObject({ - loading: false, - runnerPath: mockRunnerPath, - }); - expect(findRunnerUpdateForm().props('runner')).toEqual(mockRunner); - }); - - describe('When there is an error', () => { - beforeEach(async () => { - mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!')); - await createComponentWithApollo(); - }); - - it('error is reported to sentry', () => { - expect(captureException).toHaveBeenCalledWith({ - error: new Error('Error!'), - component: 'AdminRunnerEditApp', - }); - }); - - it('error is shown to the user', () => { - expect(createAlert).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js index 509681c5a77..7ab4aeee9bc 100644 --- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js +++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js @@ -164,7 +164,7 @@ describe('AdminRunnerShowApp', () => { }); }); - describe('when runner does not have an edit url ', () => { + describe('when runner does not have an edit url', () => { beforeEach(async () => { mockRunnerQueryResult({ editAdminUrl: null, diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js index 97341be7d5d..55a298e1695 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -17,6 +17,7 @@ import { updateHistory } from '~/lib/utils/url_utility'; import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config'; import { createLocalState } from '~/runner/graphql/list/local_state'; import AdminRunnersApp from '~/runner/admin_runners/admin_runners_app.vue'; +import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue'; @@ -33,6 +34,12 @@ import { CREATED_ASC, CREATED_DESC, DEFAULT_SORT, + I18N_STATUS_ONLINE, + I18N_STATUS_OFFLINE, + I18N_STATUS_STALE, + I18N_INSTANCE_TYPE, + I18N_GROUP_TYPE, + I18N_PROJECT_TYPE, INSTANCE_TYPE, PARAM_KEY_PAUSED, PARAM_KEY_STATUS, @@ -80,6 +87,7 @@ describe('AdminRunnersApp', () => { let localMutations; let showToast; + const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner); const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); @@ -139,6 +147,11 @@ describe('AdminRunnersApp', () => { wrapper.destroy(); }); + it('shows the feedback banner', () => { + createComponent(); + expect(findRunnerStackedLayoutBanner().exists()).toBe(true); + }); + it('shows the runner setup instructions', () => { createComponent(); @@ -156,21 +169,16 @@ describe('AdminRunnersApp', () => { }); it('shows the runner tabs', () => { - expect(findRunnerTypeTabs().text()).toMatchInterpolatedText( - `All ${mockRunnersCount} Instance ${mockRunnersCount} Group ${mockRunnersCount} Project ${mockRunnersCount}`, + const tabs = findRunnerTypeTabs().text(); + expect(tabs).toMatchInterpolatedText( + `All ${mockRunnersCount} ${I18N_INSTANCE_TYPE} ${mockRunnersCount} ${I18N_GROUP_TYPE} ${mockRunnersCount} ${I18N_PROJECT_TYPE} ${mockRunnersCount}`, ); }); it('shows the total', () => { - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Online runners')} ${mockRunnersCount}`, - ); - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Offline runners')} ${mockRunnersCount}`, - ); - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Stale runners')} ${mockRunnersCount}`, - ); + expect(findRunnerStats().text()).toContain(`${I18N_STATUS_ONLINE} ${mockRunnersCount}`); + expect(findRunnerStats().text()).toContain(`${I18N_STATUS_OFFLINE} ${mockRunnersCount}`); + expect(findRunnerStats().text()).toContain(`${I18N_STATUS_STALE} ${mockRunnersCount}`); }); }); diff --git a/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js new file mode 100644 index 00000000000..21ec9f61f37 --- /dev/null +++ b/spec/frontend/runner/components/cells/runner_stacked_summary_cell_spec.js @@ -0,0 +1,164 @@ +import { __ } from '~/locale'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import RunnerStackedSummaryCell from '~/runner/components/cells/runner_stacked_summary_cell.vue'; +import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import RunnerTags from '~/runner/components/runner_tags.vue'; +import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; + +import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants'; + +import { allRunnersData } from '../../mock_data'; + +const mockRunner = allRunnersData.data.runners.nodes[0]; + +describe('RunnerTypeCell', () => { + let wrapper; + + const findLockIcon = () => wrapper.findByTestId('lock-icon'); + const findRunnerTags = () => wrapper.findComponent(RunnerTags); + const findRunnerSummaryField = (icon) => + wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon) + .wrappers[0]; + + const createComponent = (runner, options) => { + wrapper = mountExtended(RunnerStackedSummaryCell, { + propsData: { + runner: { + ...mockRunner, + ...runner, + }, + }, + stubs: { + RunnerSummaryField, + }, + ...options, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays the runner name as id and short token', () => { + expect(wrapper.text()).toContain( + `#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`, + ); + }); + + it('Does not display the locked icon', () => { + expect(findLockIcon().exists()).toBe(false); + }); + + it('Displays the locked icon for locked runners', () => { + createComponent({ + runnerType: PROJECT_TYPE, + locked: true, + }); + + expect(findLockIcon().exists()).toBe(true); + }); + + it('Displays the runner type', () => { + createComponent({ + runnerType: INSTANCE_TYPE, + locked: true, + }); + + expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE); + }); + + it('Displays the runner version', () => { + expect(wrapper.text()).toContain(mockRunner.version); + }); + + it('Displays the runner description', () => { + expect(wrapper.text()).toContain(mockRunner.description); + }); + + it('Displays last contact', () => { + createComponent({ + contactedAt: '2022-01-02', + }); + + expect(findRunnerSummaryField('clock').find(TimeAgo).props('time')).toBe('2022-01-02'); + }); + + it('Displays empty last contact', () => { + createComponent({ + contactedAt: null, + }); + + expect(findRunnerSummaryField('clock').find(TimeAgo).exists()).toBe(false); + expect(findRunnerSummaryField('clock').text()).toContain(__('Never')); + }); + + it('Displays ip address', () => { + createComponent({ + ipAddress: '127.0.0.1', + }); + + expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1'); + }); + + it('Displays no ip address', () => { + createComponent({ + ipAddress: null, + }); + + expect(findRunnerSummaryField('disk')).toBeUndefined(); + }); + + it('Displays job count', () => { + expect(findRunnerSummaryField('pipeline').text()).toContain(`${mockRunner.jobCount}`); + }); + + it('Formats large job counts', () => { + createComponent({ + jobCount: 1000, + }); + + expect(findRunnerSummaryField('pipeline').text()).toContain('1,000'); + }); + + it('Formats large job counts with a plus symbol', () => { + createComponent({ + jobCount: 1001, + }); + + expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+'); + }); + + it('Displays created at', () => { + expect(findRunnerSummaryField('calendar').find(TimeAgo).props('time')).toBe( + mockRunner.createdAt, + ); + }); + + it('Displays tag list', () => { + createComponent({ + tagList: ['shell', 'linux'], + }); + + expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']); + }); + + it('Displays a custom slot', () => { + const slotContent = 'My custom runner name'; + + createComponent( + {}, + { + slots: { + 'runner-name': slotContent, + }, + }, + ); + + expect(wrapper.text()).toContain(slotContent); + }); +}); diff --git a/spec/frontend/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/runner/components/cells/runner_status_cell_spec.js index 0f5133d0ae2..1d4e3762c91 100644 --- a/spec/frontend/runner/components/cells/runner_status_cell_spec.js +++ b/spec/frontend/runner/components/cells/runner_status_cell_spec.js @@ -3,7 +3,14 @@ import RunnerStatusCell from '~/runner/components/cells/runner_status_cell.vue'; import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue'; import RunnerPausedBadge from '~/runner/components/runner_paused_badge.vue'; -import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE } from '~/runner/constants'; +import { + I18N_PAUSED, + I18N_STATUS_ONLINE, + I18N_STATUS_OFFLINE, + INSTANCE_TYPE, + STATUS_ONLINE, + STATUS_OFFLINE, +} from '~/runner/constants'; describe('RunnerStatusCell', () => { let wrapper; @@ -31,8 +38,8 @@ describe('RunnerStatusCell', () => { it('Displays online status', () => { createComponent(); - expect(wrapper.text()).toMatchInterpolatedText('online'); - expect(findStatusBadge().text()).toBe('online'); + expect(wrapper.text()).toContain(I18N_STATUS_ONLINE); + expect(findStatusBadge().text()).toBe(I18N_STATUS_ONLINE); }); it('Displays offline status', () => { @@ -42,8 +49,8 @@ describe('RunnerStatusCell', () => { }, }); - expect(wrapper.text()).toMatchInterpolatedText('offline'); - expect(findStatusBadge().text()).toBe('offline'); + expect(wrapper.text()).toMatchInterpolatedText(I18N_STATUS_OFFLINE); + expect(findStatusBadge().text()).toBe(I18N_STATUS_OFFLINE); }); it('Displays paused status', () => { @@ -54,8 +61,8 @@ describe('RunnerStatusCell', () => { }, }); - expect(wrapper.text()).toMatchInterpolatedText('online paused'); - expect(findPausedBadge().text()).toBe('paused'); + expect(wrapper.text()).toMatchInterpolatedText(`${I18N_STATUS_ONLINE} ${I18N_PAUSED}`); + expect(findPausedBadge().text()).toBe(I18N_PAUSED); }); it('Is empty when data is missing', () => { diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js deleted file mode 100644 index b06ab652212..00000000000 --- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js +++ /dev/null @@ -1,91 +0,0 @@ -import { __ } from '~/locale'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue'; -import { INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants'; - -const mockId = '1'; -const mockShortSha = '2P6oDVDm'; -const mockDescription = 'runner-1'; -const mockIpAddress = '0.0.0.0'; - -describe('RunnerTypeCell', () => { - let wrapper; - - const findLockIcon = () => wrapper.findByTestId('lock-icon'); - - const createComponent = (runner, options) => { - wrapper = mountExtended(RunnerSummaryCell, { - propsData: { - runner: { - id: `gid://gitlab/Ci::Runner/${mockId}`, - shortSha: mockShortSha, - description: mockDescription, - ipAddress: mockIpAddress, - runnerType: INSTANCE_TYPE, - ...runner, - }, - }, - ...options, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays the runner name as id and short token', () => { - expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`); - }); - - it('Displays the runner type', () => { - expect(wrapper.text()).toContain('shared'); - }); - - it('Does not display the locked icon', () => { - expect(findLockIcon().exists()).toBe(false); - }); - - it('Displays the locked icon for locked runners', () => { - createComponent({ - runnerType: PROJECT_TYPE, - locked: true, - }); - - expect(findLockIcon().exists()).toBe(true); - }); - - it('Displays the runner description', () => { - expect(wrapper.text()).toContain(mockDescription); - }); - - it('Displays ip address', () => { - expect(wrapper.text()).toContain(`${__('IP Address')} ${mockIpAddress}`); - }); - - it('Displays no ip address', () => { - createComponent({ - ipAddress: null, - }); - - expect(wrapper.text()).not.toContain(__('IP Address')); - }); - - it('Displays a custom slot', () => { - const slotContent = 'My custom runner summary'; - - createComponent( - {}, - { - slots: { - 'runner-name': slotContent, - }, - }, - ); - - expect(wrapper.text()).toContain(slotContent); - }); -}); diff --git a/spec/frontend/runner/components/cells/runner_summary_field_spec.js b/spec/frontend/runner/components/cells/runner_summary_field_spec.js new file mode 100644 index 00000000000..b49addf112f --- /dev/null +++ b/spec/frontend/runner/components/cells/runner_summary_field_spec.js @@ -0,0 +1,49 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RunnerSummaryField from '~/runner/components/cells/runner_summary_field.vue'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; + +describe('RunnerSummaryField', () => { + let wrapper; + + const findIcon = () => wrapper.findComponent(GlIcon); + const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value; + + const createComponent = ({ props, ...options } = {}) => { + wrapper = shallowMount(RunnerSummaryField, { + propsData: { + icon: '', + tooltip: '', + ...props, + }, + directives: { + GlTooltip: createMockDirective(), + }, + ...options, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('shows content in slot', () => { + createComponent({ + slots: { default: 'content' }, + }); + + expect(wrapper.text()).toBe('content'); + }); + + it('shows icon', () => { + createComponent({ props: { icon: 'git' } }); + + expect(findIcon().props('name')).toBe('git'); + }); + + it('shows tooltip', () => { + createComponent({ props: { tooltip: 'tooltip' } }); + + expect(getTooltipValue()).toBe('tooltip'); + }); +}); diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js index 552ee29b6f9..f2281223a25 100644 --- a/spec/frontend/runner/components/runner_details_spec.js +++ b/spec/frontend/runner/components/runner_details_spec.js @@ -25,7 +25,12 @@ describe('RunnerDetails', () => { const findDetailGroups = () => wrapper.findComponent(RunnerGroups); - const createComponent = ({ props = {}, stubs, mountFn = shallowMountExtended } = {}) => { + const createComponent = ({ + props = {}, + stubs, + mountFn = shallowMountExtended, + enforceRunnerTokenExpiresAt = false, + } = {}) => { wrapper = mountFn(RunnerDetails, { propsData: { ...props, @@ -34,6 +39,9 @@ describe('RunnerDetails', () => { RunnerDetail, ...stubs, }, + provide: { + glFeatures: { enforceRunnerTokenExpiresAt }, + }, }); }; @@ -63,6 +71,8 @@ describe('RunnerDetails', () => { ${'Maximum job timeout'} | ${{ maximumTimeout: 0 }} | ${'0 seconds'} ${'Maximum job timeout'} | ${{ maximumTimeout: 59 }} | ${'59 seconds'} ${'Maximum job timeout'} | ${{ maximumTimeout: 10 * 60 + 5 }} | ${'10 minutes 5 seconds'} + ${'Token expiry'} | ${{ tokenExpiresAt: mockOneHourAgo }} | ${'1 hour ago'} + ${'Token expiry'} | ${{ tokenExpiresAt: null }} | ${'Never expires'} `('"$field" field', ({ field, runner, expectedValue }) => { beforeEach(() => { createComponent({ @@ -72,6 +82,7 @@ describe('RunnerDetails', () => { ...runner, }, }, + enforceRunnerTokenExpiresAt: true, stubs: { GlIntersperse, GlSprintf, @@ -124,5 +135,22 @@ describe('RunnerDetails', () => { expect(findDetailGroups().props('runner')).toEqual(mockGroupRunner); }); }); + + describe('Token expiration field', () => { + it.each` + case | flag | shown + ${'is shown when feature flag is enabled'} | ${true} | ${true} + ${'is not shown when feature flag is disabled'} | ${false} | ${false} + `('$case', ({ flag, shown }) => { + createComponent({ + props: { + runner: mockGroupRunner, + }, + enforceRunnerTokenExpiresAt: flag, + }); + + expect(findDd('Token expiry', wrapper).exists()).toBe(shown); + }); + }); }); }); diff --git a/spec/frontend/runner/components/runner_header_spec.js b/spec/frontend/runner/components/runner_header_spec.js index 8799c218b06..701d39108cb 100644 --- a/spec/frontend/runner/components/runner_header_spec.js +++ b/spec/frontend/runner/components/runner_header_spec.js @@ -1,6 +1,6 @@ import { GlSprintf } from '@gitlab/ui'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants'; +import { I18N_STATUS_ONLINE, I18N_GROUP_TYPE, GROUP_TYPE, STATUS_ONLINE } from '~/runner/constants'; import { TYPE_CI_RUNNER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -49,7 +49,7 @@ describe('RunnerHeader', () => { }, }); - expect(findRunnerStatusBadge().text()).toContain('online'); + expect(findRunnerStatusBadge().text()).toContain(I18N_STATUS_ONLINE); }); it('displays the runner type', () => { @@ -60,7 +60,7 @@ describe('RunnerHeader', () => { }, }); - expect(findRunnerTypeBadge().text()).toContain('group'); + expect(findRunnerTypeBadge().text()).toContain(I18N_GROUP_TYPE); }); it('displays the runner id', () => { diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index 7b58a81bb0d..54a9e713721 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -7,6 +7,7 @@ import { import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import RunnerList from '~/runner/components/runner_list.vue'; import RunnerStatusPopover from '~/runner/components/runner_status_popover.vue'; +import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/runner/constants'; import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data'; const mockRunners = allRunnersData.data.runners.nodes; @@ -22,7 +23,10 @@ describe('RunnerList', () => { const findCell = ({ row = 0, fieldKey }) => extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`)); - const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMountExtended) => { + const createComponent = ( + { props = {}, provide = {}, ...options } = {}, + mountFn = shallowMountExtended, + ) => { wrapper = mountFn(RunnerList, { propsData: { runners: mockRunners, @@ -32,6 +36,7 @@ describe('RunnerList', () => { provide: { onlineContactTimeoutSecs, staleTimeoutSecs, + ...provide, }, ...options, }); @@ -60,10 +65,6 @@ describe('RunnerList', () => { expect(headerLabels).toEqual([ 'Status', 'Runner', - 'Version', - 'Jobs', - 'Tags', - 'Last contact', '', // actions has no label ]); }); @@ -83,24 +84,28 @@ describe('RunnerList', () => { }); it('Displays details of a runner', () => { - const { id, description, version, shortSha } = mockRunners[0]; - createComponent({}, mountExtended); + const { id, description, version, shortSha } = mockRunners[0]; + const numericId = getIdFromGraphQLId(id); + // Badges - expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText('never contacted'); + expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText( + I18N_STATUS_NEVER_CONTACTED, + ); // Runner summary - expect(findCell({ fieldKey: 'summary' }).text()).toContain( - `#${getIdFromGraphQLId(id)} (${shortSha})`, - ); - expect(findCell({ fieldKey: 'summary' }).text()).toContain(description); + const summary = findCell({ fieldKey: 'summary' }).text(); - // Other fields - expect(findCell({ fieldKey: 'version' }).text()).toBe(version); - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0'); - expect(findCell({ fieldKey: 'tagList' }).text()).toBe(''); - expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String)); + expect(summary).toContain(`#${numericId} (${shortSha})`); + expect(summary).toContain(I18N_PROJECT_TYPE); + + expect(summary).toContain(version); + expect(summary).toContain(description); + + expect(summary).toContain('Last contact'); + expect(summary).toContain('0'); // job count + expect(summary).toContain('Created'); // Actions expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true); @@ -159,42 +164,6 @@ describe('RunnerList', () => { }); }); - describe('Table data formatting', () => { - let mockRunnersCopy; - - beforeEach(() => { - mockRunnersCopy = [ - { - ...mockRunners[0], - }, - ]; - }); - - it('Formats job counts', () => { - mockRunnersCopy[0].jobCount = 1; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1'); - }); - - it('Formats large job counts', () => { - mockRunnersCopy[0].jobCount = 1000; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000'); - }); - - it('Formats large job counts with a plus symbol', () => { - mockRunnersCopy[0].jobCount = 1001; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+'); - }); - }); - it('Shows runner identifier', () => { const { id, shortSha } = mockRunners[0]; const numericId = getIdFromGraphQLId(id); diff --git a/spec/frontend/runner/components/runner_paused_badge_spec.js b/spec/frontend/runner/components/runner_paused_badge_spec.js index 18cfcfae864..c1c7351aab2 100644 --- a/spec/frontend/runner/components/runner_paused_badge_spec.js +++ b/spec/frontend/runner/components/runner_paused_badge_spec.js @@ -2,6 +2,7 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import RunnerStatePausedBadge from '~/runner/components/runner_paused_badge.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import { I18N_PAUSED } from '~/runner/constants'; describe('RunnerTypeBadge', () => { let wrapper; @@ -29,8 +30,8 @@ describe('RunnerTypeBadge', () => { }); it('renders paused state', () => { - expect(wrapper.text()).toBe('paused'); - expect(findBadge().props('variant')).toBe('danger'); + expect(wrapper.text()).toBe(I18N_PAUSED); + expect(findBadge().props('variant')).toBe('warning'); }); it('renders tooltip', () => { diff --git a/spec/frontend/runner/components/runner_projects_spec.js b/spec/frontend/runner/components/runner_projects_spec.js index c988fb8477d..eca042cae86 100644 --- a/spec/frontend/runner/components/runner_projects_spec.js +++ b/spec/frontend/runner/components/runner_projects_spec.js @@ -1,4 +1,4 @@ -import { GlSkeletonLoader } from '@gitlab/ui'; +import { GlSearchBoxByType, GlSkeletonLoader } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; @@ -8,7 +8,9 @@ import { createAlert } from '~/flash'; import { sprintf } from '~/locale'; import { I18N_ASSIGNED_PROJECTS, - I18N_NONE, + I18N_CLEAR_FILTER_PROJECTS, + I18N_FILTER_PROJECTS, + I18N_NO_PROJECTS_FOUND, RUNNER_DETAILS_PROJECTS_PAGE_SIZE, } from '~/runner/constants'; import RunnerProjects from '~/runner/components/runner_projects.vue'; @@ -35,6 +37,7 @@ describe('RunnerProjects', () => { const findHeading = () => wrapper.find('h3'); const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoader); + const findGlSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); const findRunnerAssignedItems = () => wrapper.findAllComponents(RunnerAssignedItem); const findRunnerPagination = () => wrapper.findComponent(RunnerPagination); @@ -64,10 +67,21 @@ describe('RunnerProjects', () => { expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1); expect(mockRunnerProjectsQuery).toHaveBeenCalledWith({ id: mockRunner.id, + search: '', first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, }); }); + it('Shows a filter box', () => { + createComponent(); + + expect(findGlSearchBoxByType().attributes()).toMatchObject({ + clearbuttontitle: I18N_CLEAR_FILTER_PROJECTS, + debounce: '500', + placeholder: I18N_FILTER_PROJECTS, + }); + }); + describe('When there are projects assigned', () => { beforeEach(async () => { mockRunnerProjectsQuery.mockResolvedValueOnce(runnerProjectsData); @@ -110,6 +124,7 @@ describe('RunnerProjects', () => { expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(2); expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({ id: mockRunner.id, + search: '', first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, after: 'AFTER_CURSOR', }); @@ -123,10 +138,51 @@ describe('RunnerProjects', () => { expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(3); expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({ id: mockRunner.id, + search: '', last: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, before: 'BEFORE_CURSOR', }); }); + + it('When user filters after paginating, the first page is requested', async () => { + findGlSearchBoxByType().vm.$emit('input', 'my search'); + await waitForPromises(); + + expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(3); + expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({ + id: mockRunner.id, + search: 'my search', + first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, + }); + }); + }); + + describe('When user filters', () => { + it('Filtered results are requested', async () => { + expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1); + + findGlSearchBoxByType().vm.$emit('input', 'my search'); + await waitForPromises(); + + expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(2); + expect(mockRunnerProjectsQuery).toHaveBeenLastCalledWith({ + id: mockRunner.id, + search: 'my search', + first: RUNNER_DETAILS_PROJECTS_PAGE_SIZE, + }); + }); + + it('Filtered results are not requested for short searches', async () => { + expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1); + + findGlSearchBoxByType().vm.$emit('input', 'm'); + await waitForPromises(); + + findGlSearchBoxByType().vm.$emit('input', 'my'); + await waitForPromises(); + + expect(mockRunnerProjectsQuery).toHaveBeenCalledTimes(1); + }); }); }); @@ -136,10 +192,11 @@ describe('RunnerProjects', () => { expect(findGlSkeletonLoading().exists()).toBe(true); - expect(wrapper.findByText(I18N_NONE).exists()).toBe(false); + expect(wrapper.findByText(I18N_NO_PROJECTS_FOUND).exists()).toBe(false); expect(findRunnerAssignedItems().length).toBe(0); expect(findRunnerPagination().attributes('disabled')).toBe('true'); + expect(findGlSearchBoxByType().props('isLoading')).toBe(true); }); }); @@ -168,7 +225,7 @@ describe('RunnerProjects', () => { }); it('Shows a "None" label', () => { - expect(wrapper.findByText(I18N_NONE).exists()).toBe(true); + expect(wrapper.findByText(I18N_NO_PROJECTS_FOUND).exists()).toBe(true); }); }); diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js new file mode 100644 index 00000000000..1a8aced9292 --- /dev/null +++ b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js @@ -0,0 +1,39 @@ +import { nextTick } from 'vue'; +import { GlBanner } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; + +describe('RunnerStackedLayoutBanner', () => { + let wrapper; + + const findBanner = () => wrapper.findComponent(GlBanner); + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + + const createComponent = ({ ...options } = {}, mountFn = shallowMount) => { + wrapper = mountFn(RunnerStackedLayoutBanner, { + ...options, + }); + }; + + it('Displays a banner', () => { + createComponent(); + + expect(findBanner().props()).toMatchObject({ + svgPath: expect.stringContaining('data:image/svg+xml;utf8,'), + title: expect.any(String), + buttonText: expect.any(String), + buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'), + }); + expect(findLocalStorageSync().exists()).toBe(true); + }); + + it('Does not display a banner when dismissed', async () => { + findLocalStorageSync().vm.$emit('input', true); + + await nextTick(); + + expect(findBanner().exists()).toBe(false); + expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal + }); +}); diff --git a/spec/frontend/runner/components/runner_status_badge_spec.js b/spec/frontend/runner/components/runner_status_badge_spec.js index bb833bd7d5a..9ab6378304f 100644 --- a/spec/frontend/runner/components/runner_status_badge_spec.js +++ b/spec/frontend/runner/components/runner_status_badge_spec.js @@ -3,12 +3,16 @@ import { shallowMount } from '@vue/test-utils'; import RunnerStatusBadge from '~/runner/components/runner_status_badge.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { + I18N_STATUS_ONLINE, + I18N_STATUS_NEVER_CONTACTED, + I18N_STATUS_OFFLINE, + I18N_STATUS_STALE, + I18N_NEVER_CONTACTED_TOOLTIP, + I18N_STALE_NEVER_CONTACTED_TOOLTIP, STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE, STATUS_NEVER_CONTACTED, - I18N_NEVER_CONTACTED_TOOLTIP, - I18N_STALE_NEVER_CONTACTED_TOOLTIP, } from '~/runner/constants'; describe('RunnerTypeBadge', () => { @@ -46,7 +50,7 @@ describe('RunnerTypeBadge', () => { it('renders online state', () => { createComponent(); - expect(wrapper.text()).toBe('online'); + expect(wrapper.text()).toBe(I18N_STATUS_ONLINE); expect(findBadge().props('variant')).toBe('success'); expect(getTooltip().value).toBe('Runner is online; last contact was 1 minute ago'); }); @@ -59,7 +63,7 @@ describe('RunnerTypeBadge', () => { }, }); - expect(wrapper.text()).toBe('never contacted'); + expect(wrapper.text()).toBe(I18N_STATUS_NEVER_CONTACTED); expect(findBadge().props('variant')).toBe('muted'); expect(getTooltip().value).toBe(I18N_NEVER_CONTACTED_TOOLTIP); }); @@ -72,7 +76,7 @@ describe('RunnerTypeBadge', () => { }, }); - expect(wrapper.text()).toBe('offline'); + expect(wrapper.text()).toBe(I18N_STATUS_OFFLINE); expect(findBadge().props('variant')).toBe('muted'); expect(getTooltip().value).toBe('Runner is offline; last contact was 1 day ago'); }); @@ -85,7 +89,7 @@ describe('RunnerTypeBadge', () => { }, }); - expect(wrapper.text()).toBe('stale'); + expect(wrapper.text()).toBe(I18N_STATUS_STALE); expect(findBadge().props('variant')).toBe('warning'); expect(getTooltip().value).toBe('Runner is stale; last contact was 1 year ago'); }); @@ -98,7 +102,7 @@ describe('RunnerTypeBadge', () => { }, }); - expect(wrapper.text()).toBe('stale'); + expect(wrapper.text()).toBe(I18N_STATUS_STALE); expect(findBadge().props('variant')).toBe('warning'); expect(getTooltip().value).toBe(I18N_STALE_NEVER_CONTACTED_TOOLTIP); }); @@ -112,7 +116,7 @@ describe('RunnerTypeBadge', () => { }, }); - expect(wrapper.text()).toBe('online'); + expect(wrapper.text()).toBe(I18N_STATUS_ONLINE); expect(getTooltip().value).toBe('Runner is online; last contact was never'); }); diff --git a/spec/frontend/runner/components/runner_tag_spec.js b/spec/frontend/runner/components/runner_tag_spec.js index bd05d4b2cfe..391c17f81cb 100644 --- a/spec/frontend/runner/components/runner_tag_spec.js +++ b/spec/frontend/runner/components/runner_tag_spec.js @@ -1,6 +1,8 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; + +import { RUNNER_TAG_BADGE_VARIANT } from '~/runner/constants'; import RunnerTag from '~/runner/components/runner_tag.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; @@ -48,7 +50,7 @@ describe('RunnerTag', () => { it('Displays tags with correct style', () => { expect(findBadge().props()).toMatchObject({ size: 'sm', - variant: 'neutral', + variant: RUNNER_TAG_BADGE_VARIANT, }); }); diff --git a/spec/frontend/runner/components/runner_tags_spec.js b/spec/frontend/runner/components/runner_tags_spec.js index da89a659432..c6bfabdb18a 100644 --- a/spec/frontend/runner/components/runner_tags_spec.js +++ b/spec/frontend/runner/components/runner_tags_spec.js @@ -34,7 +34,6 @@ describe('RunnerTags', () => { it('Displays tags with correct style', () => { expect(findBadge().props('size')).toBe('sm'); - expect(findBadge().props('variant')).toBe('neutral'); }); it('Displays tags with md size', () => { @@ -50,7 +49,6 @@ describe('RunnerTags', () => { props: { tagList: null }, }); - expect(wrapper.text()).toBe(''); - expect(findBadge().exists()).toBe(false); + expect(wrapper.html()).toEqual(''); }); }); diff --git a/spec/frontend/runner/components/runner_type_badge_spec.js b/spec/frontend/runner/components/runner_type_badge_spec.js index 7bb0a2e6e2f..fe922fb9d18 100644 --- a/spec/frontend/runner/components/runner_type_badge_spec.js +++ b/spec/frontend/runner/components/runner_type_badge_spec.js @@ -2,7 +2,14 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import RunnerTypeBadge from '~/runner/components/runner_type_badge.vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; +import { + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + I18N_INSTANCE_TYPE, + I18N_GROUP_TYPE, + I18N_PROJECT_TYPE, +} from '~/runner/constants'; describe('RunnerTypeBadge', () => { let wrapper; @@ -27,9 +34,9 @@ describe('RunnerTypeBadge', () => { describe.each` type | text - ${INSTANCE_TYPE} | ${'shared'} - ${GROUP_TYPE} | ${'group'} - ${PROJECT_TYPE} | ${'specific'} + ${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE} + ${GROUP_TYPE} | ${I18N_GROUP_TYPE} + ${PROJECT_TYPE} | ${I18N_PROJECT_TYPE} `('displays $type runner', ({ type, text }) => { beforeEach(() => { createComponent({ props: { type } }); @@ -37,7 +44,7 @@ describe('RunnerTypeBadge', () => { it(`as "${text}" with an "info" variant`, () => { expect(findBadge().text()).toBe(text); - expect(findBadge().props('variant')).toBe('info'); + expect(findBadge().props('variant')).toBe('muted'); }); it('with a tooltip', () => { diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js index 22d2a9e60f7..45ab8684332 100644 --- a/spec/frontend/runner/components/runner_type_tabs_spec.js +++ b/spec/frontend/runner/components/runner_type_tabs_spec.js @@ -28,7 +28,7 @@ const mockCount = (type, multiplier = 1) => { describe('RunnerTypeTabs', () => { let wrapper; - const findTabs = () => wrapper.findAll(GlTab); + const findTabs = () => wrapper.findAllComponents(GlTab); const findActiveTab = () => findTabs() .filter((tab) => tab.attributes('active') === 'true') diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index 3037364d941..7b67a89f989 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -1,6 +1,7 @@ import Vue, { nextTick } from 'vue'; import { GlForm, GlSkeletonLoader } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; +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'; @@ -47,6 +48,7 @@ describe('RunnerUpdateForm', () => { const findSubmit = () => wrapper.find('[type="submit"]'); const findSubmitDisabledAttr = () => findSubmit().attributes('disabled'); + const findCancelBtn = () => wrapper.findByRole('link', { name: __('Cancel') }); const submitForm = () => findForm().trigger('submit'); const submitFormAndWait = () => submitForm().then(waitForPromises); @@ -117,6 +119,11 @@ describe('RunnerUpdateForm', () => { expect(mockRunner).toMatchObject(getFieldsModel()); }); + it('Form shows a cancel button', () => { + expect(runnerUpdateHandler).not.toHaveBeenCalled(); + expect(findCancelBtn().attributes('href')).toBe(mockRunnerPath); + }); + it('Form prevent multiple submissions', async () => { await submitForm(); diff --git a/spec/frontend/runner/components/stat/runner_stats_spec.js b/spec/frontend/runner/components/stat/runner_stats_spec.js index 7f1f22be94f..4afbe453903 100644 --- a/spec/frontend/runner/components/stat/runner_stats_spec.js +++ b/spec/frontend/runner/components/stat/runner_stats_spec.js @@ -1,13 +1,20 @@ import { shallowMount, mount } from '@vue/test-utils'; -import { s__ } from '~/locale'; import RunnerStats from '~/runner/components/stat/runner_stats.vue'; import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue'; -import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants'; +import { + I18N_STATUS_ONLINE, + I18N_STATUS_OFFLINE, + I18N_STATUS_STALE, + INSTANCE_TYPE, + STATUS_ONLINE, + STATUS_OFFLINE, + STATUS_STALE, +} from '~/runner/constants'; describe('RunnerStats', () => { let wrapper; - const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat).wrappers; + const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat); const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => { wrapper = mountFn(RunnerStats, { @@ -46,16 +53,28 @@ describe('RunnerStats', () => { }); const text = wrapper.text(); - expect(text).toMatch(`${s__('Runners|Online runners')} 3`); - expect(text).toMatch(`${s__('Runners|Offline runners')} 2`); - expect(text).toMatch(`${s__('Runners|Stale runners')} 1`); + expect(text).toContain(`${I18N_STATUS_ONLINE} 3`); + expect(text).toContain(`${I18N_STATUS_OFFLINE} 2`); + expect(text).toContain(`${I18N_STATUS_STALE} 1`); + }); + + it('Skips query for other stats', () => { + createComponent({ + props: { + variables: { status: STATUS_ONLINE }, + }, + }); + + expect(findSingleStats().at(0).props('skip')).toBe(false); + expect(findSingleStats().at(1).props('skip')).toBe(true); + expect(findSingleStats().at(2).props('skip')).toBe(true); }); it('Displays all counts for filtered searches', () => { const mockVariables = { paused: true }; createComponent({ props: { variables: mockVariables } }); - findSingleStats().forEach((stat) => { + findSingleStats().wrappers.forEach((stat) => { expect(stat.props('variables')).toMatchObject(mockVariables); }); }); diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index 57d64202219..a17502c7eec 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -15,6 +15,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory } from '~/lib/utils/url_utility'; import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config'; +import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_bar.vue'; import RunnerList from '~/runner/components/runner_list.vue'; @@ -28,6 +29,9 @@ import { CREATED_ASC, CREATED_DESC, DEFAULT_SORT, + I18N_STATUS_ONLINE, + I18N_STATUS_OFFLINE, + I18N_STATUS_STALE, INSTANCE_TYPE, GROUP_TYPE, PARAM_KEY_PAUSED, @@ -74,6 +78,7 @@ jest.mock('~/lib/utils/url_utility', () => ({ describe('GroupRunnersApp', () => { let wrapper; + const findRunnerStackedLayoutBanner = () => wrapper.findComponent(RunnerStackedLayoutBanner); const findRunnerStats = () => wrapper.findComponent(RunnerStats); const findRunnerActionsCell = () => wrapper.findComponent(RunnerActionsCell); const findRegistrationDropdown = () => wrapper.findComponent(RegistrationDropdown); @@ -122,6 +127,11 @@ describe('GroupRunnersApp', () => { wrapper.destroy(); }); + it('shows the feedback banner', () => { + createComponent(); + expect(findRunnerStackedLayoutBanner().exists()).toBe(true); + }); + it('shows the runner tabs with a runner count for each type', async () => { await createComponent({ mountFn: mountExtended }); @@ -153,15 +163,10 @@ describe('GroupRunnersApp', () => { groupFullPath: mockGroupFullPath, }); - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Online runners')} ${mockGroupRunnersCount}`, - ); - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Offline runners')} ${mockGroupRunnersCount}`, - ); - expect(findRunnerStats().text()).toContain( - `${s__('Runners|Stale runners')} ${mockGroupRunnersCount}`, - ); + const text = findRunnerStats().text(); + expect(text).toContain(`${I18N_STATUS_ONLINE} ${mockGroupRunnersCount}`); + expect(text).toContain(`${I18N_STATUS_OFFLINE} ${mockGroupRunnersCount}`); + expect(text).toContain(`${I18N_STATUS_STALE} ${mockGroupRunnersCount}`); }); it('shows the runners list', async () => { @@ -396,4 +401,36 @@ describe('GroupRunnersApp', () => { }); }); }); + + describe('when user has permission to register group runner', () => { + beforeEach(() => { + createComponent({ + propsData: { + registrationToken: mockRegistrationToken, + groupFullPath: mockGroupFullPath, + groupRunnersLimitedCount: mockGroupRunnersCount, + }, + }); + }); + + it('shows the register group runner button', () => { + expect(findRegistrationDropdown().exists()).toBe(true); + }); + }); + + describe('when user has no permission to register group runner', () => { + beforeEach(() => { + createComponent({ + propsData: { + registrationToken: null, + groupFullPath: mockGroupFullPath, + groupRunnersLimitedCount: mockGroupRunnersCount, + }, + }); + }); + + it('does not show the register group runner button', () => { + expect(findRegistrationDropdown().exists()).toBe(false); + }); + }); }); diff --git a/spec/frontend/runner/runner_edit/runner_edit_app_spec.js b/spec/frontend/runner/runner_edit/runner_edit_app_spec.js new file mode 100644 index 00000000000..fb118817d51 --- /dev/null +++ b/spec/frontend/runner/runner_edit/runner_edit_app_spec.js @@ -0,0 +1,114 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/flash'; + +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import RunnerHeader from '~/runner/components/runner_header.vue'; +import RunnerUpdateForm from '~/runner/components/runner_update_form.vue'; +import runnerFormQuery from '~/runner/graphql/edit/runner_form.query.graphql'; +import RunnerEditApp from '~//runner/runner_edit/runner_edit_app.vue'; +import { captureException } from '~/runner/sentry_utils'; +import { I18N_STATUS_NEVER_CONTACTED, I18N_INSTANCE_TYPE } from '~/runner/constants'; + +import { runnerFormData } from '../mock_data'; + +jest.mock('~/flash'); +jest.mock('~/runner/sentry_utils'); + +const mockRunner = runnerFormData.data.runner; +const mockRunnerGraphqlId = mockRunner.id; +const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`; +const mockRunnerPath = `/admin/runners/${mockRunnerId}`; + +Vue.use(VueApollo); + +describe('RunnerEditApp', () => { + let wrapper; + let mockRunnerQuery; + + const findRunnerHeader = () => wrapper.findComponent(RunnerHeader); + const findRunnerUpdateForm = () => wrapper.findComponent(RunnerUpdateForm); + + const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(RunnerEditApp, { + apolloProvider: createMockApollo([[runnerFormQuery, mockRunnerQuery]]), + propsData: { + runnerId: mockRunnerId, + runnerPath: mockRunnerPath, + ...props, + }, + }); + + return waitForPromises(); + }; + + beforeEach(() => { + mockRunnerQuery = jest.fn().mockResolvedValue(runnerFormData); + }); + + afterEach(() => { + mockRunnerQuery.mockReset(); + wrapper.destroy(); + }); + + it('expect GraphQL ID to be requested', async () => { + await createComponentWithApollo(); + + expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId }); + }); + + it('displays the runner id and creation date', async () => { + await createComponentWithApollo({ mountFn: mount }); + + expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`); + expect(findRunnerHeader().text()).toContain('created'); + }); + + it('displays the runner type and status', async () => { + await createComponentWithApollo({ mountFn: mount }); + + expect(findRunnerHeader().text()).toContain(I18N_STATUS_NEVER_CONTACTED); + expect(findRunnerHeader().text()).toContain(I18N_INSTANCE_TYPE); + }); + + it('displays a loading runner form', () => { + createComponentWithApollo(); + + expect(findRunnerUpdateForm().props()).toMatchObject({ + runner: null, + loading: true, + runnerPath: mockRunnerPath, + }); + }); + + it('displays the runner form', async () => { + await createComponentWithApollo(); + + expect(findRunnerUpdateForm().props()).toMatchObject({ + loading: false, + runnerPath: mockRunnerPath, + }); + expect(findRunnerUpdateForm().props('runner')).toEqual(mockRunner); + }); + + describe('When there is an error', () => { + beforeEach(async () => { + mockRunnerQuery = jest.fn().mockRejectedValueOnce(new Error('Error!')); + await createComponentWithApollo(); + }); + + it('error is reported to sentry', () => { + expect(captureException).toHaveBeenCalledWith({ + error: new Error('Error!'), + component: 'RunnerEditApp', + }); + }); + + it('error is shown to the user', () => { + expect(createAlert).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/runner/utils_spec.js b/spec/frontend/runner/utils_spec.js index 1db9815dfd8..33de1345f85 100644 --- a/spec/frontend/runner/utils_spec.js +++ b/spec/frontend/runner/utils_spec.js @@ -1,4 +1,4 @@ -import { formatJobCount, tableField, getPaginationVariables } from '~/runner/utils'; +import { formatJobCount, tableField, getPaginationVariables, parseInterval } from '~/runner/utils'; describe('~/runner/utils', () => { describe('formatJobCount', () => { @@ -66,4 +66,15 @@ describe('~/runner/utils', () => { expect(getPaginationVariables(pagination, pageSize)).toEqual(variables); }); }); + + describe('parseInterval', () => { + it.each` + case | argument | returnValue + ${'parses integer'} | ${'86400'} | ${86400} + ${'returns null for undefined'} | ${undefined} | ${null} + ${'returns null for null'} | ${null} | ${null} + `('$case', ({ argument, returnValue }) => { + expect(parseInterval(argument)).toStrictEqual(returnValue); + }); + }); }); diff --git a/spec/frontend/search/sidebar/components/radio_filter_spec.js b/spec/frontend/search/sidebar/components/radio_filter_spec.js index 39d5ee581ec..c0a8259b4fe 100644 --- a/spec/frontend/search/sidebar/components/radio_filter_spec.js +++ b/spec/frontend/search/sidebar/components/radio_filter_spec.js @@ -44,7 +44,7 @@ describe('RadioFilter', () => { }); const findGlRadioButtonGroup = () => wrapper.find(GlFormRadioGroup); - const findGlRadioButtons = () => findGlRadioButtonGroup().findAll(GlFormRadio); + const findGlRadioButtons = () => findGlRadioButtonGroup().findAllComponents(GlFormRadio); const findGlRadioButtonsText = () => findGlRadioButtons().wrappers.map((w) => w.text()); describe('template', () => { diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js index 04520a3e704..0e8eebba3cb 100644 --- a/spec/frontend/search/sort/components/app_spec.js +++ b/spec/frontend/search/sort/components/app_spec.js @@ -46,7 +46,7 @@ describe('GlobalSearchSort', () => { const findSortButtonGroup = () => wrapper.find(GlButtonGroup); const findSortDropdown = () => wrapper.find(GlDropdown); const findSortDirectionButton = () => wrapper.find(GlButton); - const findDropdownItems = () => findSortDropdown().findAll(GlDropdownItem); + const findDropdownItems = () => findSortDropdown().findAllComponents(GlDropdownItem); const findDropdownItemsText = () => findDropdownItems().wrappers.map((w) => w.text()); describe('template', () => { diff --git a/spec/frontend/set_status_modal/set_status_form_spec.js b/spec/frontend/set_status_modal/set_status_form_spec.js new file mode 100644 index 00000000000..8e1623eedf5 --- /dev/null +++ b/spec/frontend/set_status_modal/set_status_form_spec.js @@ -0,0 +1,167 @@ +import $ from 'jquery'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import SetStatusForm from '~/set_status_modal/set_status_form.vue'; +import EmojiPicker from '~/emoji/components/picker.vue'; +import { timeRanges } from '~/vue_shared/constants'; +import { sprintf } from '~/locale'; +import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; + +describe('SetStatusForm', () => { + let wrapper; + + const defaultPropsData = { + defaultEmoji: 'speech_balloon', + emoji: 'thumbsup', + message: 'Foo bar', + availability: false, + }; + + const createComponent = async ({ propsData = {} } = {}) => { + wrapper = mountExtended(SetStatusForm, { + propsData: { + ...defaultPropsData, + ...propsData, + }, + }); + + await waitForPromises(); + }; + + const findMessageInput = () => + wrapper.findByPlaceholderText(SetStatusForm.i18n.statusMessagePlaceholder); + const findSelectedEmoji = (emoji) => + wrapper.findByTestId('selected-emoji').find(`gl-emoji[data-name="${emoji}"]`); + + it('sets up emoji autocomplete for the message input', async () => { + const gfmAutoCompleteSetupSpy = jest.spyOn(GfmAutoComplete.prototype, 'setup'); + + await createComponent(); + + expect(gfmAutoCompleteSetupSpy).toHaveBeenCalledWith($(findMessageInput().element), { + emojis: true, + }); + }); + + describe('when emoji is set', () => { + it('displays emoji', async () => { + await createComponent(); + + expect(findSelectedEmoji(defaultPropsData.emoji).exists()).toBe(true); + }); + }); + + describe('when emoji is not set and message is changed', () => { + it('displays default emoji', async () => { + await createComponent({ + propsData: { + emoji: '', + }, + }); + + await findMessageInput().trigger('keyup'); + + expect(findSelectedEmoji(defaultPropsData.defaultEmoji).exists()).toBe(true); + }); + }); + + describe('when message is set', () => { + it('displays filled in message input', async () => { + await createComponent(); + + expect(findMessageInput().element.value).toBe(defaultPropsData.message); + }); + }); + + describe('when clear status after is set', () => { + it('displays value in dropdown toggle button', async () => { + const clearStatusAfter = timeRanges[0]; + + await createComponent({ + propsData: { + clearStatusAfter, + }, + }); + + expect(wrapper.findByRole('button', { name: clearStatusAfter.label }).exists()).toBe(true); + }); + }); + + describe('when emoji is changed', () => { + beforeEach(async () => { + await createComponent(); + + wrapper.findComponent(EmojiPicker).vm.$emit('click', defaultPropsData.emoji); + }); + + it('emits `emoji-click` event', () => { + expect(wrapper.emitted('emoji-click')).toEqual([[defaultPropsData.emoji]]); + }); + }); + + describe('when message is changed', () => { + it('emits `message-input` event', async () => { + await createComponent(); + + const newMessage = 'Foo bar baz'; + + await findMessageInput().setValue(newMessage); + + expect(wrapper.emitted('message-input')).toEqual([[newMessage]]); + }); + }); + + describe('when availability checkbox is changed', () => { + it('emits `availability-input` event', async () => { + await createComponent(); + + await wrapper + .findByLabelText( + `${SetStatusForm.i18n.availabilityCheckboxLabel} ${SetStatusForm.i18n.availabilityCheckboxHelpText}`, + ) + .setChecked(); + + expect(wrapper.emitted('availability-input')).toEqual([[true]]); + }); + }); + + describe('when `Clear status after` dropdown is changed', () => { + it('emits `clear-status-after-click`', async () => { + await wrapper.findByTestId('thirtyMinutes').trigger('click'); + + expect(wrapper.emitted('clear-status-after-click')).toEqual([[timeRanges[0]]]); + }); + }); + + describe('when clear status button is clicked', () => { + beforeEach(async () => { + await createComponent(); + + await wrapper + .findByRole('button', { name: SetStatusForm.i18n.clearStatusButtonLabel }) + .trigger('click'); + }); + + it('clears emoji and message', () => { + expect(wrapper.emitted('emoji-click')).toEqual([['']]); + expect(wrapper.emitted('message-input')).toEqual([['']]); + expect(wrapper.findByTestId('no-emoji-placeholder').exists()).toBe(true); + }); + }); + + describe('when `currentClearStatusAfter` prop is set', () => { + it('displays clear status message', async () => { + const date = '2022-08-25 21:14:48 UTC'; + + await createComponent({ + propsData: { + currentClearStatusAfter: date, + }, + }); + + expect( + wrapper.findByText(sprintf(SetStatusForm.i18n.clearStatusAfterMessage, { date })).exists(), + ).toBe(true); + }); + }); +}); 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 e3b5478290a..c5fb590646d 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,14 +1,14 @@ import { GlModal, GlFormCheckbox } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; import * as UserApi from '~/api/user_api'; import EmojiPicker from '~/emoji/components/picker.vue'; import createFlash from '~/flash'; import stubChildren from 'helpers/stub_children'; -import SetStatusModalWrapper, { - AVAILABILITY_STATUS, -} from '~/set_status_modal/set_status_modal_wrapper.vue'; +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'; jest.mock('~/flash'); @@ -33,7 +33,7 @@ describe('SetStatusModalWrapper', () => { }; const createComponent = (props = {}) => { - return mount(SetStatusModalWrapper, { + return mountExtended(SetStatusModalWrapper, { propsData: { ...defaultProps, ...props, @@ -42,6 +42,7 @@ describe('SetStatusModalWrapper', () => { ...stubChildren(SetStatusModalWrapper), GlFormInput: false, GlFormInputGroup: false, + SetStatusForm: false, EmojiPicker: EmojiPickerStub, }, mocks: { @@ -51,7 +52,8 @@ describe('SetStatusModalWrapper', () => { }; const findModal = () => wrapper.find(GlModal); - const findFormField = (field) => wrapper.find(`[name="user[status][${field}]"]`); + const findMessageField = () => + wrapper.findByPlaceholderText(SetStatusForm.i18n.statusMessagePlaceholder); const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button'); const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox); const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]'); @@ -81,14 +83,8 @@ describe('SetStatusModalWrapper', () => { return initModal(); }); - it('sets the hidden status emoji field', () => { - const field = findFormField('emoji'); - expect(field.exists()).toBe(true); - expect(field.element.value).toBe(defaultEmoji); - }); - it('sets the message field', () => { - const field = findFormField('message'); + const field = findMessageField(); expect(field.exists()).toBe(true); expect(field.element.value).toBe(defaultMessage); }); @@ -118,10 +114,10 @@ describe('SetStatusModalWrapper', () => { }); }); - it('sets emojiTag when clicking in emoji picker', async () => { + it('passes emoji to `SetStatusForm`', async () => { await getEmojiPicker().vm.$emit('click', 'thumbsup'); - expect(wrapper.vm.emojiTag).toContain('data-name="thumbsup"'); + expect(wrapper.findComponent(SetStatusForm).props('emoji')).toBe('thumbsup'); }); }); @@ -133,7 +129,7 @@ describe('SetStatusModalWrapper', () => { }); it('does not set the message field', () => { - expect(findFormField('message').element.value).toBe(''); + expect(findMessageField().element.value).toBe(''); }); it('hides the clear status button', () => { @@ -141,18 +137,6 @@ describe('SetStatusModalWrapper', () => { }); }); - describe('with no currentEmoji set', () => { - beforeEach(async () => { - await initEmojiMock(); - wrapper = createComponent({ currentEmoji: '' }); - return initModal(); - }); - - it('does not set the hidden status emoji field', () => { - expect(findFormField('emoji').element.value).toBe(''); - }); - }); - describe('with currentClearStatusAfter set', () => { beforeEach(async () => { await initEmojiMock(); @@ -182,8 +166,7 @@ describe('SetStatusModalWrapper', () => { findModal().vm.$emit('secondary'); await nextTick(); - expect(findFormField('message').element.value).toBe(''); - expect(findFormField('emoji').element.value).toBe(''); + expect(findMessageField().element.value).toBe(''); }); it('clicking "setStatus" submits the user status', async () => { @@ -194,7 +177,7 @@ describe('SetStatusModalWrapper', () => { findAvailabilityCheckbox().vm.$emit('input', true); // set the currentClearStatusAfter to 30 minutes - wrapper.find('[data-testid="thirtyMinutes"]').vm.$emit('click'); + wrapper.find('[data-testid="thirtyMinutes"]').trigger('click'); 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 new file mode 100644 index 00000000000..eaee0e77311 --- /dev/null +++ b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js @@ -0,0 +1,156 @@ +import { nextTick } from 'vue'; +import { cloneDeep } from 'lodash'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { resetHTMLFixture } from 'helpers/fixtures'; +import { useFakeDate } from 'helpers/fake_date'; +import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue'; +import SetStatusForm from '~/set_status_modal/set_status_form.vue'; +import { TIME_RANGES_WITH_NEVER, NEVER_TIME_RANGE } from '~/set_status_modal/constants'; + +describe('UserProfileSetStatusWrapper', () => { + let wrapper; + + const defaultProvide = { + fields: { + emoji: { name: 'user[status][emoji]', id: 'user_status_emoji', value: '8ball' }, + message: { name: 'user[status][message]', id: 'user_status_message', value: 'foo bar' }, + availability: { + name: 'user[status][availability]', + id: 'user_status_availability', + value: 'busy', + }, + clearStatusAfter: { + name: 'user[status][clear_status_after]', + id: 'user_status_clear_status_after', + value: '2022-09-03 03:06:26 UTC', + }, + }, + }; + + const createComponent = ({ provide = {} } = {}) => { + wrapper = mountExtended(UserProfileSetStatusWrapper, { + provide: { + ...defaultProvide, + ...provide, + }, + }); + }; + + const findInput = (name) => wrapper.find(`[name="${name}"]`); + const findSetStatusForm = () => wrapper.findComponent(SetStatusForm); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders `SetStatusForm` component and passes expected props', () => { + createComponent(); + + expect(cloneDeep(findSetStatusForm().props())).toMatchObject({ + defaultEmoji: 'speech_balloon', + emoji: defaultProvide.fields.emoji.value, + message: defaultProvide.fields.message.value, + availability: true, + clearStatusAfter: NEVER_TIME_RANGE, + currentClearStatusAfter: defaultProvide.fields.clearStatusAfter.value, + }); + }); + + it.each` + input + ${'emoji'} + ${'message'} + ${'availability'} + `('renders hidden $input input with value set', ({ input }) => { + createComponent(); + + expect(findInput(defaultProvide.fields[input].name).attributes('value')).toBe( + defaultProvide.fields[input].value, + ); + }); + + describe('when clear status after dropdown is set to `Never`', () => { + it('renders hidden clear status after input with value unset', () => { + createComponent(); + + expect( + findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value'), + ).toBeUndefined(); + }); + }); + + describe('when clear status after dropdown has a value selected', () => { + it('renders hidden clear status after input with value set', async () => { + createComponent(); + + findSetStatusForm().vm.$emit('clear-status-after-click', TIME_RANGES_WITH_NEVER[1]); + + await nextTick(); + + expect(findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value')).toBe( + TIME_RANGES_WITH_NEVER[1].shortcut, + ); + }); + }); + + describe('when emoji is changed', () => { + it('updates hidden emoji input value', async () => { + createComponent(); + + const newEmoji = 'basketball'; + + findSetStatusForm().vm.$emit('emoji-click', newEmoji); + + await nextTick(); + + expect(findInput(defaultProvide.fields.emoji.name).attributes('value')).toBe(newEmoji); + }); + }); + + describe('when message is changed', () => { + it('updates hidden message input value', async () => { + createComponent(); + + const newMessage = 'foo bar baz'; + + findSetStatusForm().vm.$emit('message-input', newMessage); + + await nextTick(); + + expect(findInput(defaultProvide.fields.message.name).attributes('value')).toBe(newMessage); + }); + }); + + describe('when form is successfully submitted', () => { + // 2022-09-02 00:00:00 UTC + useFakeDate(2022, 8, 2); + + const form = document.createElement('form'); + form.classList.add('js-edit-user'); + + beforeEach(async () => { + document.body.appendChild(form); + createComponent(); + + const oneDay = TIME_RANGES_WITH_NEVER[4]; + + findSetStatusForm().vm.$emit('clear-status-after-click', oneDay); + + await nextTick(); + + form.dispatchEvent(new Event('ajax:success')); + }); + + afterEach(() => { + resetHTMLFixture(); + }); + + it('updates clear status after dropdown to `Never`', () => { + expect(findSetStatusForm().props('clearStatusAfter')).toBe(NEVER_TIME_RANGE); + }); + + it('updates `currentClearStatusAfter` prop', () => { + expect(findSetStatusForm().props('currentClearStatusAfter')).toBe('2022-09-03 00:00:00 UTC'); + }); + }); +}); diff --git a/spec/frontend/set_status_modal/utils_spec.js b/spec/frontend/set_status_modal/utils_spec.js index 273f30f8311..1e918b75a98 100644 --- a/spec/frontend/set_status_modal/utils_spec.js +++ b/spec/frontend/set_status_modal/utils_spec.js @@ -1,4 +1,5 @@ -import { AVAILABILITY_STATUS, isUserBusy } from '~/set_status_modal/utils'; +import { isUserBusy } from '~/set_status_modal/utils'; +import { AVAILABILITY_STATUS } from '~/set_status_modal/constants'; describe('Set status modal utils', () => { describe('isUserBusy', () => { diff --git a/spec/frontend/sidebar/assignee_title_spec.js b/spec/frontend/sidebar/assignee_title_spec.js index 3079cb28406..e29e3d489a5 100644 --- a/spec/frontend/sidebar/assignee_title_spec.js +++ b/spec/frontend/sidebar/assignee_title_spec.js @@ -85,7 +85,7 @@ describe('AssigneeTitle component', () => { editable: false, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBeFalsy(); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); }); it('renders spinner when loading', () => { @@ -95,7 +95,7 @@ describe('AssigneeTitle component', () => { editable: false, }); - expect(wrapper.find(GlLoadingIcon).exists()).toBeTruthy(); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); }); it('does not render edit link when not editable', () => { 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 517b4f12559..8cde70ff8da 100644 --- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js @@ -143,7 +143,7 @@ describe('AssigneeAvatarLink component', () => { issuableType | userId ${'merge_request'} | ${undefined} ${'issue'} | ${'1'} - `('it sets data-user-id as $userId for $issuableType', ({ issuableType, userId }) => { + `('sets data-user-id as $userId for $issuableType', ({ issuableType, userId }) => { createComponent({ issuableType, }); 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 5aa8264b98c..81ff51133bf 100644 --- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js +++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js @@ -23,7 +23,7 @@ describe('CollapsedAssigneeList component', () => { const findNoUsersIcon = () => wrapper.find(GlIcon); const findAvatarCounter = () => wrapper.find('.avatar-counter'); - const findAssignees = () => wrapper.findAll(CollapsedAssignee); + const findAssignees = () => wrapper.findAllComponents(CollapsedAssignee); const getTooltipTitle = () => wrapper.attributes('title'); afterEach(() => { 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 88015ed42a3..3644a51c7fd 100644 --- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js +++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_widget_spec.js @@ -65,6 +65,7 @@ describe('Sidebar assignees widget', () => { issuableId: 0, fullPath: '/mygroup/myProject', allowMultipleAssignees: true, + editable: true, ...props, }, provide: { @@ -350,6 +351,17 @@ describe('Sidebar assignees widget', () => { }); }); + describe('when issuable is not editable by the user', () => { + beforeEach(async () => { + createComponent({ props: { editable: false } }); + await waitForPromises(); + }); + + it('passes editable prop as false to IssuableAssignees', () => { + expect(findAssignees().props('editable')).toBe(false); + }); + }); + it('includes the real-time assignees component', async () => { createComponent(); await waitForPromises(); 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 f7437386814..b902d7313fd 100644 --- a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js +++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js @@ -42,7 +42,7 @@ describe('UncollapsedAssigneeList component', () => { }); it('only has one user', () => { - expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(1); + expect(wrapper.findAllComponents(AssigneeAvatarLink).length).toBe(1); }); it('calls the AssigneeAvatarLink with the proper props', () => { @@ -79,7 +79,7 @@ describe('UncollapsedAssigneeList component', () => { }); it('shows truncated users', () => { - expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT); + expect(wrapper.findAllComponents(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT); }); describe('when more button is clicked', () => { @@ -94,7 +94,9 @@ describe('UncollapsedAssigneeList component', () => { }); it('shows all users', () => { - expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT + 1); + expect(wrapper.findAllComponents(AssigneeAvatarLink).length).toBe( + DEFAULT_RENDER_COUNT + 1, + ); }); }); }); 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 4dbf3d426bb..37c16bc9235 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 @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import { AVAILABILITY_STATUS } from '~/set_status_modal/utils'; +import { AVAILABILITY_STATUS } from '~/set_status_modal/constants'; import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue'; const name = 'Administrator'; 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 7775ed6aa37..1ea035c7184 100644 --- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js +++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_form_spec.js @@ -71,12 +71,7 @@ describe('Sidebar Confidentiality Form', () => { it('creates a flash if mutation contains errors', async () => { createComponent({ mutate: jest.fn().mockResolvedValue({ - data: { - issuableSetConfidential: { - issuable: { confidential: false }, - errors: ['Houston, we have a problem!'], - }, - }, + data: { issuableSetConfidential: { errors: ['Houston, we have a problem!'] } }, }), }); findConfidentialToggle().vm.$emit('click', new MouseEvent('click')); @@ -87,24 +82,6 @@ describe('Sidebar Confidentiality Form', () => { }); }); - it('emits `closeForm` event with confidentiality value when mutation is successful', async () => { - createComponent({ - mutate: jest.fn().mockResolvedValue({ - data: { - issuableSetConfidential: { - issuable: { confidential: true }, - errors: [], - }, - }, - }), - }); - - findConfidentialToggle().vm.$emit('click', new MouseEvent('click')); - await waitForPromises(); - - expect(wrapper.emitted('closeForm')).toEqual([[{ confidential: true }]]); - }); - describe('when issue is not confidential', () => { beforeEach(() => { createComponent(); 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 18ee423d12e..3a3f0b1d9fa 100644 --- a/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js +++ b/spec/frontend/sidebar/components/confidential/sidebar_confidentiality_widget_spec.js @@ -132,7 +132,6 @@ describe('Sidebar Confidentiality Widget', () => { it('closes the form and dispatches an event when `closeForm` is emitted', async () => { createComponent(); const el = wrapper.vm.$el; - const closeFormPayload = { confidential: true }; jest.spyOn(el, 'dispatchEvent'); await waitForPromises(); @@ -141,12 +140,12 @@ describe('Sidebar Confidentiality Widget', () => { expect(findConfidentialityForm().isVisible()).toBe(true); - findConfidentialityForm().vm.$emit('closeForm', closeFormPayload); + findConfidentialityForm().vm.$emit('closeForm'); await nextTick(); expect(findConfidentialityForm().isVisible()).toBe(false); expect(el.dispatchEvent).toHaveBeenCalled(); - expect(wrapper.emitted('closeForm')).toEqual([[closeFormPayload]]); + expect(wrapper.emitted('closeForm')).toEqual([[]]); }); it('emits `expandSidebar` event when it is emitted from child component', async () => { 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 fda21e06987..a7556b9110c 100644 --- a/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js +++ b/spec/frontend/sidebar/components/date/sidebar_inherit_date_spec.js @@ -5,10 +5,10 @@ import SidebarInheritDate from '~/sidebar/components/date/sidebar_inherit_date.v describe('SidebarInheritDate', () => { let wrapper; - const findFixedFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(0); - const findInheritFormattedDate = () => wrapper.findAll(SidebarFormattedDate).at(1); - const findFixedRadio = () => wrapper.findAll(GlFormRadio).at(0); - const findInheritRadio = () => wrapper.findAll(GlFormRadio).at(1); + const findFixedFormattedDate = () => wrapper.findAllComponents(SidebarFormattedDate).at(0); + const findInheritFormattedDate = () => wrapper.findAllComponents(SidebarFormattedDate).at(1); + const findFixedRadio = () => wrapper.findAllComponents(GlFormRadio).at(0); + const findInheritRadio = () => wrapper.findAllComponents(GlFormRadio).at(1); const createComponent = ({ dueDateIsFixed = false } = {}) => { wrapper = shallowMount(SidebarInheritDate, { @@ -36,8 +36,8 @@ describe('SidebarInheritDate', () => { }); it('displays formatted fixed and inherited dates with radio buttons', () => { - expect(wrapper.findAll(SidebarFormattedDate)).toHaveLength(2); - expect(wrapper.findAll(GlFormRadio)).toHaveLength(2); + expect(wrapper.findAllComponents(SidebarFormattedDate)).toHaveLength(2); + expect(wrapper.findAllComponents(GlFormRadio)).toHaveLength(2); expect(findFixedFormattedDate().props('formattedDate')).toBe('Apr 15, 2021'); expect(findInheritFormattedDate().props('formattedDate')).toBe('May 15, 2021'); expect(findFixedRadio().text()).toBe('Fixed:'); 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 2c24df2436a..d00c8dcb653 100644 --- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js +++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js @@ -52,7 +52,7 @@ describe('UncollapsedReviewerList component', () => { }); it('only has one user', () => { - expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(1); + expect(wrapper.findAllComponents(ReviewerAvatarLink).length).toBe(1); }); it('shows one user with avatar, and author name', () => { @@ -96,7 +96,7 @@ describe('UncollapsedReviewerList component', () => { }); it('has both users', () => { - expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(2); + expect(wrapper.findAllComponents(ReviewerAvatarLink).length).toBe(2); }); it('shows both users with avatar, and author name', () => { diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js index 5d80a221d8e..83eb9a18597 100644 --- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js +++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js @@ -97,7 +97,7 @@ describe('SidebarSeverity', () => { }); }); - it('shows error alert when severity update fails ', async () => { + 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'); diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js index 8ebd2dabfc2..6761731c093 100644 --- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js +++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js @@ -238,6 +238,24 @@ describe('SidebarDropdownWidget', () => { expect(findSelectedAttribute().text()).toBe('None'); }); }); + + describe("when user doesn't have permission to view current attribute", () => { + it('renders no permission text', () => { + createComponent({ + data: { + hasCurrentAttribute: true, + currentAttribute: null, + }, + queries: { + currentAttribute: { loading: false }, + }, + }); + + expect(findSelectedAttribute().text()).toBe( + `You don't have permission to view this ${wrapper.props('issuableAttribute')}.`, + ); + }); + }); }); describe('when a user can edit', () => { 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 9a68940590d..430acf9f9e7 100644 --- a/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js +++ b/spec/frontend/sidebar/components/subscriptions/sidebar_subscriptions_widget_spec.js @@ -156,7 +156,7 @@ describe('Sidebar Subscriptions Widget', () => { }); await waitForPromises(); - await wrapper.find('.dropdown-item').trigger('click'); + await wrapper.find('[data-testid="notifications-toggle"]').vm.$emit('change'); await waitForPromises(); diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js index 5ed8810e95e..4e619a4e609 100644 --- a/spec/frontend/sidebar/components/time_tracking/report_spec.js +++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js @@ -161,7 +161,6 @@ describe('Issuable Time Tracking Report', () => { id: timelogToRemoveId, }, }, - update: expect.anything(), }); }); @@ -179,7 +178,6 @@ describe('Issuable Time Tracking Report', () => { id: timelogToRemoveId, }, }, - update: expect.anything(), }); expect(createFlash).toHaveBeenCalledWith({ diff --git a/spec/frontend/sidebar/issuable_assignees_spec.js b/spec/frontend/sidebar/issuable_assignees_spec.js index 3563d478f3f..dc59b68bbd4 100644 --- a/spec/frontend/sidebar/issuable_assignees_spec.js +++ b/spec/frontend/sidebar/issuable_assignees_spec.js @@ -12,6 +12,7 @@ describe('IssuableAssignees', () => { }, propsData: { users: [], + editable: true, ...props, }, }); @@ -25,15 +26,19 @@ describe('IssuableAssignees', () => { }); describe('when no assignees are present', () => { - it('renders "None - assign yourself" when user is logged in', () => { - createComponent({ signedIn: true }); - expect(findEmptyAssignee().text()).toBe('None - assign yourself'); - }); - - it('renders "None" when user is not logged in', () => { - createComponent(); - expect(findEmptyAssignee().text()).toBe('None'); - }); + it.each` + signedIn | editable | message + ${true} | ${true} | ${'None - assign yourself'} + ${true} | ${false} | ${'None'} + ${false} | ${true} | ${'None'} + ${false} | ${false} | ${'None'} + `( + 'renders "$message" when signedIn is $signedIn and editable is $editable', + ({ signedIn, editable, message }) => { + createComponent({ signedIn, editable }); + expect(findEmptyAssignee().text()).toBe(message); + }, + ); }); describe('when assignees are present', () => { diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js index bb757fdf63b..986ccaea4b6 100644 --- a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js +++ b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js @@ -130,7 +130,7 @@ describe('IssuableLockForm', () => { expect(findEditForm().exists()).toBe(true); }); - it('tracks the event ', () => { + it('tracks the event', () => { const spy = mockTracking('_category_', wrapper.element, jest.spyOn); triggerEvent(findEditLink().element); diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index 9c6e23e928c..2afe9647cbe 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -497,6 +497,11 @@ export const searchResponse = { user: mockUser2, }, ], + pageInfo: { + hasNextPage: false, + endCursor: null, + startCursor: null, + }, }, }, }, @@ -559,6 +564,11 @@ export const projectMembersResponse = { }, }, ], + pageInfo: { + hasNextPage: false, + startCursor: null, + endCursor: null, + }, }, }, }, diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js index e32694abcce..355f0c45bbe 100644 --- a/spec/frontend/sidebar/sidebar_mediator_spec.js +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -27,7 +27,7 @@ describe('Sidebar mediator', () => { mock.restore(); }); - it('assigns yourself ', () => { + it('assigns yourself', () => { mediator.assignYourself(); expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser); diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js index 7bb7b18adf8..2e6807ed9d8 100644 --- a/spec/frontend/sidebar/sidebar_move_issue_spec.js +++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js @@ -7,6 +7,7 @@ import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue'; import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; +import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown'; import Mock from './mock_data'; jest.mock('~/flash'); @@ -75,7 +76,9 @@ describe('SidebarMoveIssue', () => { it('should initialize the deprecatedJQueryDropdown', () => { test.sidebarMoveIssue.initDropdown(); - expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeTruthy(); + expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeInstanceOf( + GitLabDropdown, + ); }); it('escapes html from project name', async () => { @@ -97,7 +100,7 @@ describe('SidebarMoveIssue', () => { test.sidebarMoveIssue.onConfirmClicked(); expect(test.mediator.moveIssue).toHaveBeenCalled(); - expect(test.$confirmButton.prop('disabled')).toBeTruthy(); + expect(test.$confirmButton.prop('disabled')).toBe(true); expect(test.$confirmButton.hasClass('is-loading')).toBe(true); }); @@ -113,7 +116,7 @@ describe('SidebarMoveIssue', () => { await waitForPromises(); expect(createFlash).toHaveBeenCalled(); - expect(test.$confirmButton.prop('disabled')).toBeFalsy(); + expect(test.$confirmButton.prop('disabled')).toBe(false); expect(test.$confirmButton.hasClass('is-loading')).toBe(false); }); @@ -139,7 +142,7 @@ describe('SidebarMoveIssue', () => { test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click'); expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0); - expect(test.$confirmButton.prop('disabled')).toBeTruthy(); + expect(test.$confirmButton.prop('disabled')).toBe(true); }); it('should set moveToProjectId on dropdown item click', async () => { diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js index 9316268d2ad..5f696b237e0 100644 --- a/spec/frontend/sidebar/todo_spec.js +++ b/spec/frontend/sidebar/todo_spec.js @@ -55,7 +55,7 @@ describe('SidebarTodo', () => { wrapper.find('button').trigger('click'); await nextTick(); - expect(wrapper.emitted().toggleTodo).toBeTruthy(); + expect(wrapper.emitted().toggleTodo).toHaveLength(1); }); it('renders component container element with proper data attributes', () => { diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index f49ceb2fede..cf897414ccb 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -16,10 +16,10 @@ import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_e import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue'; import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; import { - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PUBLIC, -} from '~/snippets/constants'; + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; import CreateSnippetMutation from '~/snippets/mutations/create_snippet.mutation.graphql'; import UpdateSnippetMutation from '~/snippets/mutations/update_snippet.mutation.graphql'; import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; @@ -41,7 +41,7 @@ const TEST_SNIPPET_GID = 'gid://gitlab/PersonalSnippet/42'; const createSnippet = () => merge(createGQLSnippet(), { webUrl: TEST_WEB_URL, - visibilityLevel: SNIPPET_VISIBILITY_PRIVATE, + visibilityLevel: VISIBILITY_LEVEL_PRIVATE_STRING, }); const createQueryResponse = (obj = {}) => @@ -70,7 +70,7 @@ const getApiData = ({ id, title = '', description = '', - visibilityLevel = SNIPPET_VISIBILITY_PRIVATE, + visibilityLevel = VISIBILITY_LEVEL_PRIVATE_STRING, } = {}) => ({ id, title, @@ -128,7 +128,10 @@ describe('Snippet Edit app', () => { const setDescription = (val) => wrapper.findComponent(SnippetDescriptionEdit).vm.$emit('input', val); - const createComponent = ({ props = {}, selectedLevel = SNIPPET_VISIBILITY_PRIVATE } = {}) => { + const createComponent = ({ + props = {}, + selectedLevel = VISIBILITY_LEVEL_PRIVATE_STRING, + } = {}) => { if (wrapper) { throw new Error('wrapper already created'); } @@ -260,17 +263,18 @@ describe('Snippet Edit app', () => { }, ); - it.each([SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC])( - 'marks %s visibility by default', - async (visibility) => { - createComponent({ - props: { snippetGid: '' }, - selectedLevel: visibility, - }); + it.each([ + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, + ])('marks %s visibility by default', async (visibility) => { + createComponent({ + props: { snippetGid: '' }, + selectedLevel: visibility, + }); - expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility); - }, - ); + expect(wrapper.find(SnippetVisibilityEdit).props('value')).toBe(visibility); + }); describe('form submission handling', () => { describe('when creating a new snippet', () => { diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js index b29ed97099f..032dcf8e5f5 100644 --- a/spec/frontend/snippets/components/show_spec.js +++ b/spec/frontend/snippets/components/show_spec.js @@ -7,10 +7,10 @@ import SnippetBlob from '~/snippets/components/snippet_blob_view.vue'; import SnippetHeader from '~/snippets/components/snippet_header.vue'; import SnippetTitle from '~/snippets/components/snippet_title.vue'; import { - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_PUBLIC, -} from '~/snippets/constants'; + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue'; import { stubPerformanceWebAPI } from 'helpers/performance'; @@ -69,7 +69,7 @@ describe('Snippet view app', () => { createComponent({ data: { snippet: { - visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_STRING, webUrl: 'http://foo.bar', }, }, @@ -85,7 +85,7 @@ describe('Snippet view app', () => { }, }, }); - const blobs = wrapper.findAll(SnippetBlob); + const blobs = wrapper.findAllComponents(SnippetBlob); expect(blobs.length).toBe(2); expect(blobs.at(0).props('blob')).toEqual(Blob); expect(blobs.at(1).props('blob')).toEqual(BinaryBlob); @@ -93,11 +93,11 @@ describe('Snippet view app', () => { describe('Embed dropdown rendering', () => { it.each` - visibilityLevel | condition | isRendered - ${SNIPPET_VISIBILITY_INTERNAL} | ${'not render'} | ${false} - ${SNIPPET_VISIBILITY_PRIVATE} | ${'not render'} | ${false} - ${'foo'} | ${'not render'} | ${false} - ${SNIPPET_VISIBILITY_PUBLIC} | ${'render'} | ${true} + visibilityLevel | condition | isRendered + ${VISIBILITY_LEVEL_INTERNAL_STRING} | ${'not render'} | ${false} + ${VISIBILITY_LEVEL_PRIVATE_STRING} | ${'not render'} | ${false} + ${'foo'} | ${'not render'} | ${false} + ${VISIBILITY_LEVEL_PUBLIC_STRING} | ${'render'} | ${true} `('does $condition embed-dropdown by default', ({ visibilityLevel, isRendered }) => { createComponent({ data: { 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 df98312b498..a650353093d 100644 --- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js @@ -32,7 +32,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => { }; const findLabel = () => wrapper.findComponent(GlFormGroup); - const findBlobEdits = () => wrapper.findAll(SnippetBlobEdit); + const findBlobEdits = () => wrapper.findAllComponents(SnippetBlobEdit); const findBlobsData = () => findBlobEdits().wrappers.map((x) => ({ blob: x.props('blob'), diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index c395112e313..aa31377f390 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -15,7 +15,7 @@ import { BLOB_RENDER_ERRORS, } from '~/blob/components/constants'; import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue'; -import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants'; +import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants'; import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; describe('Blob Embeddable', () => { @@ -23,7 +23,7 @@ describe('Blob Embeddable', () => { const snippet = { id: 'gid://foo.bar/snippet', webUrl: 'https://foo.bar', - visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, + visibilityLevel: VISIBILITY_LEVEL_PUBLIC_STRING, }; const dataMock = { activeViewerType: SimpleViewerMock.type, diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js index 62d1ac9b476..2d043a5caba 100644 --- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js +++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js @@ -1,11 +1,13 @@ import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; +import { + VISIBILITY_LEVEL_PRIVATE_STRING, + VISIBILITY_LEVEL_INTERNAL_STRING, + VISIBILITY_LEVEL_PUBLIC_STRING, +} from '~/visibility_level/constants'; import { SNIPPET_VISIBILITY, - SNIPPET_VISIBILITY_PRIVATE, - SNIPPET_VISIBILITY_INTERNAL, - SNIPPET_VISIBILITY_PUBLIC, SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED, } from '~/snippets/constants'; @@ -38,7 +40,7 @@ describe('Snippet Visibility Edit component', () => { } const findLink = () => wrapper.find('label').find(GlLink); - const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio); + const findRadios = () => wrapper.find(GlFormRadioGroup).findAllComponents(GlFormRadio); const findRadiosData = () => findRadios().wrappers.map((x) => { return { @@ -75,19 +77,19 @@ describe('Snippet Visibility Edit component', () => { const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]'); const RESULTING_OPTIONS = { 0: { - value: SNIPPET_VISIBILITY_PRIVATE, + value: VISIBILITY_LEVEL_PRIVATE_STRING, icon: SNIPPET_VISIBILITY.private.icon, text: SNIPPET_VISIBILITY.private.label, description: SNIPPET_VISIBILITY.private.description, }, 10: { - value: SNIPPET_VISIBILITY_INTERNAL, + value: VISIBILITY_LEVEL_INTERNAL_STRING, icon: SNIPPET_VISIBILITY.internal.icon, text: SNIPPET_VISIBILITY.internal.label, description: SNIPPET_VISIBILITY.internal.description, }, 20: { - value: SNIPPET_VISIBILITY_PUBLIC, + value: VISIBILITY_LEVEL_PUBLIC_STRING, icon: SNIPPET_VISIBILITY.public.icon, text: SNIPPET_VISIBILITY.public.label, description: SNIPPET_VISIBILITY.public.description, @@ -130,7 +132,7 @@ describe('Snippet Visibility Edit component', () => { createComponent({ propsData: { isProjectSnippet: true }, deep: true }); expect(findRadiosData()[0]).toEqual({ - value: SNIPPET_VISIBILITY_PRIVATE, + value: VISIBILITY_LEVEL_PRIVATE_STRING, icon: SNIPPET_VISIBILITY.private.icon, text: SNIPPET_VISIBILITY.private.label, description: SNIPPET_VISIBILITY.private.description_project, @@ -141,7 +143,7 @@ describe('Snippet Visibility Edit component', () => { describe('functionality', () => { it('pre-selects correct option in the list', () => { - const value = SNIPPET_VISIBILITY_INTERNAL; + const value = VISIBILITY_LEVEL_INTERNAL_STRING; createComponent({ propsData: { value } }); diff --git a/spec/frontend/surveys/merge_request_performance/app_spec.js b/spec/frontend/surveys/merge_request_performance/app_spec.js index cd549155914..af91d8aeb6b 100644 --- a/spec/frontend/surveys/merge_request_performance/app_spec.js +++ b/spec/frontend/surveys/merge_request_performance/app_spec.js @@ -6,6 +6,17 @@ import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisse import MergeRequestExperienceSurveyApp from '~/surveys/merge_request_experience/app.vue'; import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue'; +const createRenderTrackedArguments = () => [ + undefined, + 'survey:mr_experience', + { + label: 'render', + extra: { + accountAge: 0, + }, + }, +]; + describe('MergeRequestExperienceSurveyApp', () => { let trackingSpy; let wrapper; @@ -24,6 +35,7 @@ describe('MergeRequestExperienceSurveyApp', () => { dismiss, shouldShowCallout, }); + trackingSpy = mockTracking(undefined, undefined, jest.spyOn); wrapper = shallowMountExtended(MergeRequestExperienceSurveyApp, { propsData: { accountAge: 0, @@ -35,10 +47,13 @@ describe('MergeRequestExperienceSurveyApp', () => { }); }; + beforeEach(() => { + localStorage.clear(); + }); + describe('when user callout is visible', () => { beforeEach(() => { createWrapper(); - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); }); it('shows survey', async () => { @@ -47,14 +62,46 @@ describe('MergeRequestExperienceSurveyApp', () => { expect(wrapper.emitted().close).toBe(undefined); }); - it('triggers user callout on close', async () => { - findCloseButton().vm.$emit('click'); - expect(dismiss).toHaveBeenCalledTimes(1); + it('tracks render once', async () => { + expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments()); }); - it('emits close event on close button click', async () => { - findCloseButton().vm.$emit('click'); - expect(wrapper.emitted()).toMatchObject({ close: [[]] }); + it("doesn't track subsequent renders", async () => { + createWrapper(); + expect(trackingSpy).toHaveBeenCalledWith(...createRenderTrackedArguments()); + expect(trackingSpy).toHaveBeenCalledTimes(1); + }); + + describe('when close button clicked', () => { + beforeEach(() => { + findCloseButton().vm.$emit('click'); + }); + + it('triggers user callout on close', async () => { + expect(dismiss).toHaveBeenCalledTimes(1); + }); + + it('emits close event on close button click', async () => { + expect(wrapper.emitted()).toMatchObject({ close: [[]] }); + }); + + it('tracks dismissal', async () => { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', { + label: 'dismiss', + extra: { + accountAge: 0, + }, + }); + }); + + it('tracks subsequent renders', async () => { + createWrapper(); + expect(trackingSpy.mock.calls).toEqual([ + createRenderTrackedArguments(), + expect.anything(), + createRenderTrackedArguments(), + ]); + }); }); it('applies correct feature name for user callout', () => { @@ -135,6 +182,10 @@ describe('MergeRequestExperienceSurveyApp', () => { it('emits close event', async () => { expect(wrapper.emitted()).toMatchObject({ close: [[]] }); }); + + it("doesn't track anything", async () => { + expect(trackingSpy).toHaveBeenCalledTimes(0); + }); }); describe('when Escape key is pressed', () => { @@ -148,5 +199,14 @@ describe('MergeRequestExperienceSurveyApp', () => { expect(wrapper.emitted()).toMatchObject({ close: [[]] }); expect(dismiss).toHaveBeenCalledTimes(1); }); + + it('tracks dismissal', async () => { + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'survey:mr_experience', { + label: 'dismiss', + extra: { + accountAge: 0, + }, + }); + }); }); }); diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js index 16ffd2b7013..12a44452717 100644 --- a/spec/frontend/terraform/components/states_table_spec.js +++ b/spec/frontend/terraform/components/states_table_spec.js @@ -134,7 +134,7 @@ describe('StatesTable', () => { await nextTick(); }; - const findActions = () => wrapper.findAll(StateActions); + const findActions = () => wrapper.findAllComponents(StateActions); beforeEach(() => { return createComponent(); diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js index 0f121fd1beb..6e2908e659f 100644 --- a/spec/frontend/token_access/mock_data.js +++ b/spec/frontend/token_access/mock_data.js @@ -24,19 +24,6 @@ export const disabledJobTokenScope = { }, }; -export const updateJobTokenScope = { - data: { - ciCdSettingsUpdate: { - ciCdSettings: { - jobTokenScopeEnabled: true, - __typename: 'ProjectCiCdSetting', - }, - errors: [], - __typename: 'CiCdSettingsUpdatePayload', - }, - }, -}; - export const projectsWithScope = { data: { project: { diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js index 5aaeebd5af4..024e7dfff8c 100644 --- a/spec/frontend/token_access/token_access_spec.js +++ b/spec/frontend/token_access/token_access_spec.js @@ -8,13 +8,11 @@ import createFlash from '~/flash'; import TokenAccess from '~/token_access/components/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'; -import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql'; import getCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_ci_job_token_scope.query.graphql'; import getProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql'; import { enabledJobTokenScope, disabledJobTokenScope, - updateJobTokenScope, projectsWithScope, addProjectSuccess, removeProjectSuccess, @@ -32,7 +30,6 @@ describe('TokenAccess component', () => { const enabledJobTokenScopeHandler = jest.fn().mockResolvedValue(enabledJobTokenScope); const disabledJobTokenScopeHandler = jest.fn().mockResolvedValue(disabledJobTokenScope); - const updateJobTokenScopeHandler = jest.fn().mockResolvedValue(updateJobTokenScope); const getProjectsWithScope = jest.fn().mockResolvedValue(projectsWithScope); const addProjectSuccessHandler = jest.fn().mockResolvedValue(addProjectSuccess); const addProjectFailureHandler = jest.fn().mockRejectedValue(error); @@ -95,7 +92,7 @@ describe('TokenAccess component', () => { expect(findTokenSection().exists()).toBe(true); }); - it('the toggle should be disabled and the token section should not show', async () => { + it('the toggle should be disabled and the token section should show', async () => { createComponent([ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler], [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], @@ -104,28 +101,7 @@ describe('TokenAccess component', () => { await waitForPromises(); expect(findToggle().props('value')).toBe(false); - expect(findTokenSection().exists()).toBe(false); - }); - - it('switching the toggle calls the mutation and fetches the projects again', async () => { - createComponent([ - [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler], - [updateCIJobTokenScopeMutation, updateJobTokenScopeHandler], - [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], - ]); - - await waitForPromises(); - - expect(getProjectsWithScope).toHaveBeenCalledTimes(1); - - findToggle().vm.$emit('change', true); - - await waitForPromises(); - - expect(updateJobTokenScopeHandler).toHaveBeenCalledWith({ - input: { fullPath: projectPath, jobTokenScopeEnabled: true }, - }); - expect(getProjectsWithScope).toHaveBeenCalledTimes(2); + expect(findTokenSection().exists()).toBe(true); }); }); diff --git a/spec/frontend/tooltips/components/tooltips_spec.js b/spec/frontend/tooltips/components/tooltips_spec.js index eef352a72ff..998bb2a9ea2 100644 --- a/spec/frontend/tooltips/components/tooltips_spec.js +++ b/spec/frontend/tooltips/components/tooltips_spec.js @@ -28,7 +28,7 @@ describe('tooltips/components/tooltips.vue', () => { return target; }; - const allTooltips = () => wrapper.findAll(GlTooltip); + const allTooltips = () => wrapper.findAllComponents(GlTooltip); afterEach(() => { wrapper.destroy(); @@ -68,7 +68,7 @@ describe('tooltips/components/tooltips.vue', () => { await nextTick(); - expect(wrapper.findAll(GlTooltip)).toHaveLength(1); + expect(wrapper.findAllComponents(GlTooltip)).toHaveLength(1); }); it('sets tooltip content from title attribute', async () => { diff --git a/spec/frontend/user_lists/store/index/actions_spec.js b/spec/frontend/user_lists/store/index/actions_spec.js index 4a8d0afb963..7b2e29ae412 100644 --- a/spec/frontend/user_lists/store/index/actions_spec.js +++ b/spec/frontend/user_lists/store/index/actions_spec.js @@ -41,7 +41,7 @@ describe('~/user_lists/store/index/actions', () => { }); describe('success', () => { - it('dispatches requestUserLists and receiveUserListsSuccess ', () => { + it('dispatches requestUserLists and receiveUserListsSuccess', () => { return testAction( fetchUserLists, null, @@ -61,7 +61,7 @@ describe('~/user_lists/store/index/actions', () => { }); describe('error', () => { - it('dispatches requestUserLists and receiveUserListsError ', () => { + it('dispatches requestUserLists and receiveUserListsError', () => { Api.fetchFeatureFlagUserLists.mockRejectedValue(); return testAction( 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 cb53dc1fb61..063425454d7 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 @@ -1,10 +1,10 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import AddedCommentMessage from '~/vue_merge_request_widget/components/added_commit_message.vue'; let wrapper; function factory(propsData) { - wrapper = shallowMount(AddedCommentMessage, { + wrapper = mount(AddedCommentMessage, { propsData: { isFastForwardEnabled: false, targetBranch: 'main', @@ -23,4 +23,13 @@ describe('Widget added commit message', () => { expect(wrapper.element.outerHTML).toContain('The changes were not merged'); }); + + it('renders merge commit as a link', () => { + factory({ state: 'merged', mergeCommitPath: 'https://test.host/merge-commit-link' }); + + expect(wrapper.find('[data-testid="merge-commit-sha"]').exists()).toBe(true); + expect(wrapper.find('[data-testid="merge-commit-sha"]').attributes('href')).toBe( + 'https://test.host/merge-commit-link', + ); + }); }); 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 712abfe228a..d519ad2cdb0 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 @@ -39,10 +39,12 @@ describe('Artifacts List', () => { }); it('renders job url', () => { - expect(wrapper.findAll(GlLink).at(1).attributes('href')).toEqual(data.artifacts[0].job_path); + expect(wrapper.findAllComponents(GlLink).at(1).attributes('href')).toEqual( + data.artifacts[0].job_path, + ); }); it('renders job name', () => { - expect(wrapper.findAll(GlLink).at(1).text()).toEqual(data.artifacts[0].job_name); + expect(wrapper.findAllComponents(GlLink).at(1).text()).toEqual(data.artifacts[0].job_name); }); }); 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 6347e3c3be3..7f0173b7445 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 @@ -4,9 +4,8 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { trimText } from 'helpers/text_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue'; -import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue'; -import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; +import MRWidgetPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; +import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import { SUCCESS } from '~/vue_merge_request_widget/constants'; import mockData from '../mock_data'; @@ -30,14 +29,13 @@ describe('MRWidgetPipeline', () => { const findPipelineInfoContainer = () => wrapper.findByTestId('pipeline-info-container'); const findCommitLink = () => wrapper.findByTestId('commit-link'); const findPipelineFinishedAt = () => wrapper.findByTestId('finished-at'); - const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); - const findAllPipelineStages = () => wrapper.findAllComponents(PipelineStage); const findPipelineCoverage = () => wrapper.findByTestId('pipeline-coverage'); const findPipelineCoverageDelta = () => wrapper.findByTestId('pipeline-coverage-delta'); const findPipelineCoverageTooltipText = () => wrapper.findByTestId('pipeline-coverage-tooltip').text(); const findPipelineCoverageDeltaTooltipText = () => wrapper.findByTestId('pipeline-coverage-delta-tooltip').text(); + const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph); const findMonitoringPipelineMessage = () => wrapper.findByTestId('monitoring-pipeline-message'); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); @@ -45,7 +43,7 @@ describe('MRWidgetPipeline', () => { const createWrapper = (props = {}, mountFn = shallowMount) => { wrapper = extendedWrapper( - mountFn(PipelineComponent, { + mountFn(MRWidgetPipelineComponent, { propsData: { ...defaultProps, ...props, @@ -106,8 +104,10 @@ describe('MRWidgetPipeline', () => { }); it('should render pipeline graph', () => { + const stagesCount = mockData.pipeline.details.stages.length; + expect(findPipelineMiniGraph().exists()).toBe(true); - expect(findAllPipelineStages()).toHaveLength(mockData.pipeline.details.stages.length); + expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount); }); describe('should render pipeline coverage information', () => { @@ -176,15 +176,11 @@ describe('MRWidgetPipeline', () => { expect(findPipelineInfoContainer().text()).toMatch(mockData.pipeline.details.status.label); }); - it('should render pipeline graph with correct styles', () => { + it('should render pipeline graph', () => { const stagesCount = mockData.pipeline.details.stages.length; expect(findPipelineMiniGraph().exists()).toBe(true); - expect(findPipelineMiniGraph().findAll('.mr-widget-pipeline-stages')).toHaveLength( - stagesCount, - ); - - expect(findAllPipelineStages()).toHaveLength(stagesCount); + expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount); }); it('should render coverage information', () => { @@ -266,13 +262,13 @@ describe('MRWidgetPipeline', () => { }); describe('for a detached merge request pipeline', () => { - it('renders a pipeline widget that reads "Detached merge request pipeline for "', () => { - pipeline.details.name = 'Detached merge request pipeline'; + it('renders a pipeline widget that reads "Merge request pipeline for "', () => { + pipeline.details.name = 'Merge request pipeline'; pipeline.merge_request_event_type = 'detached'; factory(); - const expected = `Detached merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`; + const expected = `Merge request pipeline #${pipeline.id} ${pipeline.details.status.label} for ${pipeline.commit.short_id}`; const actual = trimText(findPipelineInfoContainer().text()); expect(actual).toBe(expected); 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 534c0baf35d..05c259de370 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 @@ -110,7 +110,7 @@ describe('Merge request widget rebase component', () => { expect(findRebaseMessageText()).toContain('Something went wrong!'); }); - describe('Rebase buttons with', () => { + describe('Rebase buttons', () => { beforeEach(() => { createWrapper( { @@ -148,6 +148,79 @@ describe('Merge request widget rebase component', () => { expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); }); }); + + describe('Rebase when pipelines must succeed is enabled', () => { + beforeEach(() => { + createWrapper( + { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + onlyAllowMergeIfPipelineSucceeds: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + mergeRequestWidgetGraphql, + ); + }); + + it('renders only the rebase button', () => { + expect(findRebaseWithoutCiButton().exists()).toBe(false); + expect(findStandardRebaseButton().exists()).toBe(true); + }); + + it('starts the rebase when clicking', async () => { + findStandardRebaseButton().vm.$emit('click'); + + await nextTick(); + + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); + }); + }); + + describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => { + beforeEach(() => { + createWrapper( + { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: true, + onlyAllowMergeIfPipelineSucceeds: true, + allowMergeOnSkippedPipeline: true, + }, + service: { + rebase: rebaseMock, + poll: pollMock, + }, + }, + mergeRequestWidgetGraphql, + ); + }); + + it('renders both rebase buttons', () => { + expect(findRebaseWithoutCiButton().exists()).toBe(true); + expect(findStandardRebaseButton().exists()).toBe(true); + }); + + it('starts the rebase when clicking', async () => { + findStandardRebaseButton().vm.$emit('click'); + + await nextTick(); + + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false }); + }); + + it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => { + findRebaseWithoutCiButton().vm.$emit('click'); + + await nextTick(); + + expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true }); + }); + }); }); describe('without permissions', () => { 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 11373be578a..530549b7b9c 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 @@ -1,14 +1,16 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue'; describe('MR widget status icon component', () => { let wrapper; - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findStatusIcon = () => wrapper.findComponent(StatusIcon); + const findIcon = () => wrapper.findComponent(GlIcon); - const createWrapper = (props, mountFn = shallowMount) => { - wrapper = mountFn(mrStatusIcon, { + const createWrapper = (props) => { + wrapper = shallowMount(mrStatusIcon, { propsData: { ...props, }, @@ -17,27 +19,45 @@ describe('MR widget status icon component', () => { afterEach(() => { wrapper.destroy(); + wrapper = null; }); describe('while loading', () => { it('renders loading icon', () => { createWrapper({ status: 'loading' }); - expect(findLoadingIcon().exists()).toBe(true); + expect(findStatusIcon().exists()).toBe(true); + expect(findStatusIcon().props().isLoading).toBe(true); }); }); describe('with status icon', () => { it('renders success status icon', () => { - createWrapper({ status: 'success' }, mount); + createWrapper({ status: 'success' }); - expect(wrapper.find('[data-testid="status_success-icon"]').exists()).toBe(true); + expect(findStatusIcon().exists()).toBe(true); + expect(findStatusIcon().props().iconName).toBe('success'); }); it('renders failed status icon', () => { - createWrapper({ status: 'failed' }, mount); + createWrapper({ status: 'failed' }); - expect(wrapper.find('[data-testid="status_failed-icon"]').exists()).toBe(true); + expect(findStatusIcon().exists()).toBe(true); + expect(findStatusIcon().props().iconName).toBe('failed'); + }); + + it('renders merged status icon', () => { + createWrapper({ status: 'merged' }); + + expect(findIcon().exists()).toBe(true); + expect(findIcon().props().name).toBe('merge'); + }); + + it('renders closed status icon', () => { + createWrapper({ status: 'closed' }); + + expect(findIcon().exists()).toBe(true); + expect(findIcon().props().name).toBe('merge-request-close'); }); }); }); 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 352bc1a08ea..d6c67dab381 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 @@ -128,7 +128,7 @@ describe('MRWidgetSuggestPipeline', () => { it('emits dismiss upon dismissal button click', () => { findDismissContainer().vm.$emit('dismiss'); - expect(wrapper.emitted().dismiss).toBeTruthy(); + expect(wrapper.emitted().dismiss).toHaveLength(1); }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap index de25e2a0450..635ef0f6b0d 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap @@ -4,117 +4,171 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
- -
- -

- Set by - - - - - - - - to be merged automatically when the pipeline succeeds -

- +
+
+ + + +
+
+
+
+
+
+ +
-
- + +
+ -
+ + + + +
@@ -124,117 +178,171 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
- -
- -

- Set by - - - - - - - - to be merged automatically when the pipeline succeeds -

- +
+
+ + + +
+
+
+
+
+
+ +
-
- + +
+ -
+ + + + +
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap deleted file mode 100644 index 7e741bf4660..00000000000 --- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PipelineFailed should render error message with a disabled merge button 1`] = ` -
- - -
- - - -
-
-`; 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 9332b7e334a..5c07f4ce143 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 @@ -1,25 +1,26 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue'; +import StateContainer from '~/vue_merge_request_widget/components/state_container.vue'; describe('MRWidgetArchived', () => { - let vm; + let wrapper; beforeEach(() => { - const Component = Vue.extend(archivedComponent); - vm = mountComponent(Component); + wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); - it('renders a ci status failed icon', () => { - expect(vm.$el.querySelector('.ci-status-icon')).not.toBeNull(); + it('renders error icon', () => { + expect(wrapper.findComponent(StateContainer).exists()).toBe(true); + expect(wrapper.findComponent(StateContainer).props().status).toBe('failed'); }); - it('renders information', () => { - expect(vm.$el.querySelector('.bold').textContent.trim()).toEqual( + it('renders information about merging', () => { + expect(wrapper.text()).toContain( 'Merge unavailable: merge requests are read-only on archived projects.', ); }); 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 02de426204b..ac18ccf9e26 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 @@ -1,27 +1,25 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; -import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue'; +import { shallowMount } from '@vue/test-utils'; +import CheckingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue'; +import StateContainer from '~/vue_merge_request_widget/components/state_container.vue'; describe('MRWidgetChecking', () => { - let Component; - let vm; + let wrapper; beforeEach(() => { - Component = Vue.extend(checkingComponent); - vm = mountComponent(Component); + wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); it('renders loading icon', () => { - expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner'); + expect(wrapper.findComponent(StateContainer).exists()).toBe(true); + expect(wrapper.findComponent(StateContainer).props().status).toBe('loading'); }); it('renders information about merging', () => { - expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual( - 'Checking if merge request can be merged…', - ); + expect(wrapper.text()).toContain('Checking if merge request can be merged…'); }); }); 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 f7d046eb8f9..06ee017dee7 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 @@ -1,39 +1,54 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue'; +import MrWidgetAuthorTime from '~/vue_merge_request_widget/components/mr_widget_author_time.vue'; +import StateContainer from '~/vue_merge_request_widget/components/state_container.vue'; + +const MOCK_DATA = { + metrics: { + mergedBy: {}, + closedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm UTC', + closedAt: 'Jan 24, 2018 1:02pm UTC', + readableMergedAt: '', + readableClosedAt: 'less than a minute ago', + }, + targetBranchPath: '/twitter/flight/commits/so_long_jquery', + targetBranch: 'so_long_jquery', +}; describe('MRWidgetClosed', () => { - let vm; + let wrapper; beforeEach(() => { - const Component = Vue.extend(closedComponent); - vm = mountComponent(Component, { - mr: { - metrics: { - mergedBy: {}, - closedBy: { - name: 'Administrator', - username: 'root', - webUrl: 'http://localhost:3000/root', - avatarUrl: - 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - }, - mergedAt: 'Jan 24, 2018 1:02pm UTC', - closedAt: 'Jan 24, 2018 1:02pm UTC', - readableMergedAt: '', - readableClosedAt: 'less than a minute ago', - }, - targetBranchPath: '/twitter/flight/commits/so_long_jquery', - targetBranch: 'so_long_jquery', + wrapper = shallowMount(closedComponent, { + propsData: { + mr: MOCK_DATA, }, }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; + }); + + it('renders closed icon', () => { + expect(wrapper.findComponent(StateContainer).exists()).toBe(true); + expect(wrapper.findComponent(StateContainer).props().status).toBe('closed'); }); - it('renders warning icon', () => { - expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); + it('renders mr widget author time', () => { + expect(wrapper.findComponent(MrWidgetAuthorTime).exists()).toBe(true); + expect(wrapper.findComponent(MrWidgetAuthorTime).props()).toEqual({ + actionText: 'Closed by', + author: MOCK_DATA.metrics.closedBy, + dateTitle: MOCK_DATA.metrics.closedAt, + dateReadable: MOCK_DATA.metrics.readableClosedAt, + }); }); }); 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 663fabb761c..5d2d1fdd6f1 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 @@ -40,7 +40,7 @@ describe('Commits message dropdown component', () => { wrapper.destroy(); }); - const findDropdownElements = () => wrapper.findAll(GlDropdownItem); + const findDropdownElements = () => wrapper.findAllComponents(GlDropdownItem); const findFirstDropdownElement = () => findDropdownElements().at(0); it('should have 3 elements in dropdown list', () => { 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 989aa76f09b..833fa27d453 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 @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import MrWidgetFailedToMerge from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue'; +import StateContainer from '~/vue_merge_request_widget/components/state_container.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; describe('MRWidgetFailedToMerge', () => { @@ -39,7 +40,7 @@ describe('MRWidgetFailedToMerge', () => { expect(wrapper.vm.intervalId).toBe(dummyIntervalId); }); - it('clears interval when destroying ', () => { + it('clears interval when destroying', () => { createComponent(); wrapper.destroy(); @@ -128,7 +129,11 @@ describe('MRWidgetFailedToMerge', () => { await nextTick(); - expect(wrapper.find('.js-refresh-label').text().trim()).toBe('Refreshing now'); + const stateContainerWrapper = wrapper.findComponent(StateContainer); + + expect(stateContainerWrapper.exists()).toBe(true); + expect(stateContainerWrapper.props('status')).toBe('loading'); + expect(stateContainerWrapper.text().trim()).toBe('Refreshing now'); }); }); @@ -146,9 +151,9 @@ describe('MRWidgetFailedToMerge', () => { }); it('renders refresh button', () => { - expect( - wrapper.find('[data-testid="merge-request-failed-refresh-button"]').text().trim(), - ).toBe('Refresh now'); + expect(wrapper.findComponent(StateContainer).props('actions')).toMatchObject([ + { text: 'Refresh now', onClick: expect.any(Function) }, + ]); }); it('renders remaining time', () => { 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 63e93074857..c6e7198c678 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 @@ -1,25 +1,27 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue'; +import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; describe('MRWidgetNotAllowed', () => { - let vm; + let wrapper; + beforeEach(() => { - const Component = Vue.extend(notAllowedComponent); - vm = mountComponent(Component); + wrapper = shallowMount(notAllowedComponent); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); + wrapper = null; }); it('renders success icon', () => { - expect(vm.$el.querySelector('.ci-status-icon-success')).not.toBe(null); + expect(wrapper.findComponent(StatusIcon).exists()).toBe(true); + expect(wrapper.findComponent(StatusIcon).props().status).toBe('success'); }); it('renders informative text', () => { - expect(vm.$el.innerText).toContain('Ready to be merged automatically.'); - expect(vm.$el.innerText).toContain( + expect(wrapper.text()).toContain('Ready to be merged automatically.'); + expect(wrapper.text()).toContain( 'Ask someone with write access to this repository to merge this request', ); }); 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 9b10b078e89..4219ad70b4c 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 @@ -1,26 +1,25 @@ -import { shallowMount, mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import PipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue'; +import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; describe('MRWidgetPipelineBlocked', () => { let wrapper; - const createWrapper = (mountFn = shallowMount) => { - wrapper = mountFn(PipelineBlockedComponent); - }; + beforeEach(() => { + wrapper = shallowMount(PipelineBlockedComponent); + }); afterEach(() => { wrapper.destroy(); + wrapper = null; }); - it('renders warning icon', () => { - createWrapper(mount); - - expect(wrapper.find('.ci-status-icon-warning').exists()).toBe(true); + it('renders error icon', () => { + expect(wrapper.findComponent(StatusIcon).exists()).toBe(true); + expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed'); }); it('renders information text', () => { - createWrapper(); - expect(wrapper.text()).toBe( "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.", ); 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 4e44ac539f2..d5619d4996d 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 @@ -1,11 +1,17 @@ +import { GlSprintf, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue'; +import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; describe('PipelineFailed', () => { let wrapper; const createComponent = () => { - wrapper = shallowMount(PipelineFailed); + wrapper = shallowMount(PipelineFailed, { + stubs: { + GlSprintf, + }, + }); }; beforeEach(() => { @@ -17,7 +23,14 @@ describe('PipelineFailed', () => { wrapper = null; }); + it('should render error status icon', () => { + expect(wrapper.findComponent(StatusIcon).exists()).toBe(true); + expect(wrapper.findComponent(StatusIcon).props().status).toBe('failed'); + }); + it('should render error message with a disabled merge button', () => { - expect(wrapper.element).toMatchSnapshot(); + expect(wrapper.text()).toContain('Merge blocked: pipeline must succeed.'); + expect(wrapper.text()).toContain('Push a commit that fixes the failure'); + expect(wrapper.findComponent(GlLink).text()).toContain('learn about other solutions'); }); }); 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 6e89cd41559..9a6bf66909e 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 @@ -111,7 +111,7 @@ const createComponent = ( }; const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); -const findCommitEditElements = () => wrapper.findAll(CommitEdit); +const findCommitEditElements = () => wrapper.findAllComponents(CommitEdit); const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown); const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label'); const findTipLink = () => wrapper.find(GlSprintf); @@ -549,7 +549,7 @@ describe('ReadyToMerge', () => { ${'squashIsSelected'} | ${'selected'} | ${'value'} | ${false} ${'squashIsSelected'} | ${'unselected'} | ${'value'} | ${false} `( - 'is $state when squashIsReadonly returns $expectation ', + 'is $state when squashIsReadonly returns $expectation', ({ squashState, prop, expectation }) => { createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true, [squashState]: expectation }, diff --git a/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js index 8f20d6a8fc9..7a868eb8cc9 100644 --- a/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/terraform/mr_widget_terraform_container_spec.js @@ -16,7 +16,8 @@ describe('MrWidgetTerraformConainer', () => { const propsData = { endpoint: '/path/to/terraform/report.json' }; const findHeader = () => wrapper.find('[data-testid="terraform-header-text"]'); - const findPlans = () => wrapper.findAll(TerraformPlan).wrappers.map((x) => x.props('plan')); + const findPlans = () => + wrapper.findAllComponents(TerraformPlan).wrappers.map((x) => x.props('plan')); const mockPollingApi = (response, body, header) => { mock.onGet(propsData.endpoint).reply(response, body, header); diff --git a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js index 6bb718082a4..8dbee9b370c 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/widget/app_spec.js @@ -12,8 +12,8 @@ describe('MR Widget App', () => { }); }; - it('mounts the component', () => { + it('does not mount if widgets array is empty', () => { createComponent(); - expect(wrapper.findByTestId('mr-widget-app').exists()).toBe(true); + expect(wrapper.findByTestId('mr-widget-app').exists()).toBe(false); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js new file mode 100644 index 00000000000..c2128d3ff33 --- /dev/null +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_content_section_spec.js @@ -0,0 +1,39 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WidgetContentSection from '~/vue_merge_request_widget/components/widget/widget_content_section.vue'; +import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue'; + +describe('~/vue_merge_request_widget/components/widget/widget_content_section.vue', () => { + let wrapper; + + const findStatusIcon = () => wrapper.findComponent(StatusIcon); + + const createComponent = ({ propsData, slots } = {}) => { + wrapper = shallowMountExtended(WidgetContentSection, { + propsData: { + widgetName: 'MyWidget', + ...propsData, + }, + slots, + }); + }; + + it('does not render the status icon when it is not provided', () => { + createComponent(); + expect(findStatusIcon().exists()).toBe(false); + }); + + it('renders the status icon when provided', () => { + createComponent({ propsData: { statusIconName: 'failed' } }); + expect(findStatusIcon().exists()).toBe(true); + }); + + it('renders the default slot', () => { + createComponent({ + slots: { + default: 'Hello world', + }, + }); + + expect(wrapper.findByText('Hello world').exists()).toBe(true); + }); +}); 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 3c08ffdef18..b67b5703ad5 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,16 +3,21 @@ import * as Sentry from '@sentry/browser'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue'; +import ActionButtons from '~/vue_merge_request_widget/components/action_buttons.vue'; import Widget from '~/vue_merge_request_widget/components/widget/widget.vue'; describe('MR Widget', () => { let wrapper; const findStatusIcon = () => wrapper.findComponent(StatusIcon); + const findExpandedSection = () => wrapper.findByTestId('widget-extension-collapsed-section'); + const findActionButtons = () => wrapper.findComponent(ActionButtons); + const findToggleButton = () => wrapper.findByTestId('toggle-button'); const createComponent = ({ propsData, slots } = {}) => { wrapper = shallowMountExtended(Widget, { propsData: { + isCollapsible: false, loadingText: 'Loading widget', widgetName: 'MyWidget', value: { @@ -38,14 +43,15 @@ describe('MR Widget', () => { createComponent({ propsData: { fetchCollapsedData } }); await waitForPromises(); expect(fetchCollapsedData).toHaveBeenCalled(); - expect(wrapper.vm.error).toBe(null); + expect(wrapper.vm.summaryError).toBe(null); }); it('sets the error text when fetch method fails', async () => { const fetchCollapsedData = jest.fn().mockReturnValue(() => Promise.reject()); createComponent({ propsData: { fetchCollapsedData } }); await waitForPromises(); - expect(wrapper.vm.error).toBe('Failed to load'); + expect(wrapper.findByText('Failed to load').exists()).toBe(true); + expect(findStatusIcon().props()).toMatchObject({ iconName: 'failed', isLoading: false }); }); it('displays loading icon until request is made and then displays status icon when the request is complete', async () => { @@ -111,7 +117,7 @@ describe('MR Widget', () => { jest.spyOn(Sentry, 'captureException').mockImplementation(); createComponent({ propsData: { - fetchCollapsedData: async () => Promise.reject(error), + fetchCollapsedData: () => Promise.reject(error), }, }); await waitForPromises(); @@ -125,7 +131,7 @@ describe('MR Widget', () => { createComponent({ propsData: { summary: 'Hello world', - fetchCollapsedData: async () => Promise.resolve(), + fetchCollapsedData: () => Promise.resolve(), }, }); @@ -137,7 +143,7 @@ describe('MR Widget', () => { it('displays the summary slot when provided', () => { createComponent({ propsData: { - fetchCollapsedData: async () => Promise.resolve(), + fetchCollapsedData: () => Promise.resolve(), }, slots: { summary: 'More complex summary', @@ -149,19 +155,167 @@ describe('MR Widget', () => { ); }); - it('displays the content slot when provided', () => { + it('does not display action buttons if actionButtons is not provided', () => { createComponent({ propsData: { - fetchCollapsedData: async () => Promise.resolve(), + fetchCollapsedData: () => Promise.resolve(), + }, + }); + + expect(findActionButtons().exists()).toBe(false); + }); + + it('does display action buttons if actionButtons is provided', () => { + const actionButtons = [{ text: 'click-me', href: '#' }]; + + createComponent({ + propsData: { + fetchCollapsedData: () => Promise.resolve(), + actionButtons, + }, + }); + + expect(findActionButtons().props('tertiaryButtons')).toEqual(actionButtons); + }); + }); + + describe('handle collapse toggle', () => { + it('displays the toggle button correctly', () => { + createComponent({ + propsData: { + isCollapsible: true, + fetchCollapsedData: () => Promise.resolve(), }, slots: { content: 'More complex content', }, }); - expect(wrapper.findByTestId('widget-extension-collapsed-section').text()).toBe( - 'More complex content', - ); + expect(findToggleButton().attributes('title')).toBe('Show details'); + expect(findToggleButton().attributes('aria-label')).toBe('Show details'); + }); + + it('does not display the content slot until toggle is clicked', async () => { + createComponent({ + propsData: { + isCollapsible: true, + fetchCollapsedData: () => Promise.resolve(), + }, + slots: { + content: 'More complex content', + }, + }); + + expect(findExpandedSection().exists()).toBe(false); + findToggleButton().vm.$emit('click'); + await nextTick(); + expect(findExpandedSection().text()).toBe('More complex content'); + }); + + it('does not display the toggle button if isCollapsible is false', () => { + createComponent({ + propsData: { + isCollapsible: false, + fetchCollapsedData: () => Promise.resolve(), + }, + }); + + expect(findToggleButton().exists()).toBe(false); + }); + + it('fetches expanded data when clicked for the first time', async () => { + const mockDataCollapsed = { + headers: {}, + status: 200, + data: { vulnerabilities: [{ vuln: 1 }] }, + }; + + const mockDataExpanded = { + headers: {}, + status: 200, + data: { vulnerabilities: [{ vuln: 2 }] }, + }; + + const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded); + + createComponent({ + propsData: { + isCollapsible: true, + fetchCollapsedData: () => Promise.resolve(mockDataCollapsed), + fetchExpandedData, + }, + }); + + findToggleButton().vm.$emit('click'); + await waitForPromises(); + + // First fetches the collapsed data + expect(wrapper.emitted('input')[0][0]).toEqual({ + collapsed: mockDataCollapsed.data, + expanded: null, + }); + + // Then fetches the expanded data + expect(wrapper.emitted('input')[1][0]).toEqual({ + collapsed: null, + expanded: mockDataExpanded.data, + }); + + // Triggering a click does not call the expanded data again + findToggleButton().vm.$emit('click'); + await waitForPromises(); + expect(fetchExpandedData).toHaveBeenCalledTimes(1); + }); + + it('allows refetching when fetch expanded data returns an error', async () => { + const fetchExpandedData = jest.fn().mockRejectedValue({ error: true }); + + createComponent({ + propsData: { + isCollapsible: true, + fetchCollapsedData: () => Promise.resolve([]), + fetchExpandedData, + }, + }); + + findToggleButton().vm.$emit('click'); + await waitForPromises(); + + // First fetches the collapsed data + expect(wrapper.emitted('input')[0][0]).toEqual({ + collapsed: undefined, + expanded: null, + }); + + expect(fetchExpandedData).toHaveBeenCalledTimes(1); + expect(wrapper.emitted('input')).toHaveLength(1); // Should not an emit an input call because request failed + + findToggleButton().vm.$emit('click'); + await waitForPromises(); + expect(fetchExpandedData).toHaveBeenCalledTimes(2); + }); + + it('resets the error message when another request is fetched', async () => { + const fetchExpandedData = jest.fn().mockRejectedValue({ error: true }); + + createComponent({ + propsData: { + isCollapsible: true, + fetchCollapsedData: () => Promise.resolve([]), + fetchExpandedData, + }, + }); + + findToggleButton().vm.$emit('click'); + await waitForPromises(); + + expect(wrapper.findByText('Failed to load').exists()).toBe(true); + fetchExpandedData.mockImplementation(() => new Promise(() => {})); + + findToggleButton().vm.$emit('click'); + await nextTick(); + + expect(wrapper.findByText('Failed to load').exists()).toBe(false); }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_merge_request_widget/deployment/deployment_actions_spec.js index a285d26f404..a8912405fa8 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 @@ -189,7 +189,7 @@ describe('DeploymentAction component', () => { }); }); - describe('it should call the executeAction method ', () => { + describe('it should call the executeAction method', () => { beforeEach(async () => { jest.spyOn(wrapper.vm, 'executeAction').mockImplementation(); 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 5c1d3c8e8e8..82743275739 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 @@ -15,6 +15,7 @@ import { failedReport } from 'jest/reports/mock_data/mock_data'; import mixedResultsTestReports from 'jest/reports/mock_data/new_and_fixed_failures_report.json'; import newErrorsTestReports from 'jest/reports/mock_data/new_errors_report.json'; import newFailedTestReports from 'jest/reports/mock_data/new_failures_report.json'; +import newFailedTestWithNullFilesReport from 'jest/reports/mock_data/new_failures_with_null_files_report.json'; import successTestReports from 'jest/reports/mock_data/no_failures_report.json'; import resolvedFailures from 'jest/reports/mock_data/resolved_failures.json'; import recentFailures from 'jest/reports/mock_data/recent_failures_report.json'; @@ -157,6 +158,15 @@ describe('Test report extension', () => { ); }); + it('hides copy failed tests button when endpoint returns null files', async () => { + mockApi(httpStatusCodes.OK, newFailedTestWithNullFilesReport); + createComponent(); + + await waitForPromises(); + + expect(findCopyFailedSpecsBtn().exists()).toBe(false); + }); + it('copy failed tests button updates tooltip text when clicked', async () => { mockApi(httpStatusCodes.OK, newFailedTestReports); createComponent(); 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 819841317f9..cc894f94f80 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 @@ -845,7 +845,7 @@ describe('MrWidgetOptions', () => { ${'closed'} | ${false} | ${'hides'} ${'merged'} | ${true} | ${'shows'} ${'open'} | ${true} | ${'shows'} - `('it $showText merge error when state is $state', ({ state, show }) => { + `('$showText merge error when state is $state', ({ state, show }) => { createComponent({ ...mockData, state, merge_error: 'Error!' }); expect(wrapper.find('[data-testid="merge_error"]').exists()).toBe(show); @@ -1133,7 +1133,7 @@ describe('MrWidgetOptions', () => { widgetName | nonStandardEvent ${'WidgetCodeQuality'} | ${'i_testing_code_quality_widget_total'} ${'WidgetTerraform'} | ${'i_testing_terraform_widget_total'} - ${'WidgetIssues'} | ${'i_testing_load_performance_widget_total'} + ${'WidgetIssues'} | ${'i_testing_issues_widget_total'} ${'WidgetTestReport'} | ${'i_testing_summary_widget_total'} `( "sends non-standard events for the '$widgetName' widget", diff --git a/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js index 22562bb4ddb..1a109aad911 100644 --- a/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js +++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js @@ -60,7 +60,7 @@ describe('Artifacts App Store Actions', () => { }); describe('success', () => { - it('dispatches requestArtifacts and receiveArtifactsSuccess ', () => { + it('dispatches requestArtifacts and receiveArtifactsSuccess', () => { mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [ { text: 'result.txt', @@ -103,7 +103,7 @@ describe('Artifacts App Store Actions', () => { mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); }); - it('dispatches requestArtifacts and receiveArtifactsError ', () => { + it('dispatches requestArtifacts and receiveArtifactsError', () => { return testAction( fetchArtifacts, null, 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 59e21b2ff40..d309432bc63 100644 --- a/spec/frontend/vue_shared/alert_details/alert_details_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js @@ -248,7 +248,7 @@ describe('AlertDetails', () => { }); }); - it('shows error alert when incident creation fails ', async () => { + it('shows error alert when incident creation fails', async () => { const errorMsg = 'Something went wrong'; mountComponent({ mountMethod: mount, diff --git a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js index cf04c1eb24a..9d84a535d67 100644 --- a/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js +++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js @@ -42,7 +42,7 @@ describe('Alert Metrics', () => { }); describe('Empty state', () => { - it('should display a message when metrics dashboard url is not provided ', () => { + 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."); diff --git a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap deleted file mode 100644 index 7f655d67ae8..00000000000 --- a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap +++ /dev/null @@ -1,26 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Code Block with default props renders correctly 1`] = ` -
-  
-    test-code
-  
-
-`; - -exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = ` -
-  
-    test-code
-  
-
-`; 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 a943d931f67..27b6718fb8e 100644 --- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js +++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js @@ -1,6 +1,11 @@ import { shallowMount } from '@vue/test-utils'; import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { visitUrl } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); describe('CI Badge Link Component', () => { let wrapper; @@ -79,17 +84,20 @@ describe('CI Badge Link Component', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); - it.each(Object.keys(statuses))('should render badge for status: %s', (status) => { + it.each(Object.keys(statuses))('should render badge for status: %s', async (status) => { createComponent({ status: statuses[status] }); - expect(wrapper.attributes('href')).toBe(statuses[status].details_path); + expect(wrapper.attributes('href')).toBe(); expect(wrapper.text()).toBe(statuses[status].text); expect(wrapper.classes()).toContain('ci-status'); expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`); expect(findIcon().exists()).toBe(true); + + await wrapper.trigger('click'); + + expect(visitUrl).toHaveBeenCalledWith(statuses[status].details_path); }); it('should not render label', () => { @@ -97,4 +105,12 @@ describe('CI Badge Link Component', () => { expect(wrapper.text()).toBe(''); }); + + it('should emit ciStatusBadgeClick event', async () => { + createComponent({ status: statuses.success }); + + await wrapper.trigger('click'); + + expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]); + }); }); diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js new file mode 100644 index 00000000000..181692e61b5 --- /dev/null +++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js @@ -0,0 +1,65 @@ +import { shallowMount } from '@vue/test-utils'; +import CodeBlock from '~/vue_shared/components/code_block_highlighted.vue'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('Code Block Highlighted', () => { + let wrapper; + + const code = 'const foo = 1;'; + + const createComponent = (propsData = {}) => { + wrapper = shallowMount(CodeBlock, { propsData }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders highlighted code if language is supported', async () => { + createComponent({ code, language: 'javascript' }); + + await waitForPromises(); + + expect(wrapper.element).toMatchInlineSnapshot(` + + + + const + + foo = + + 1 + + ; + + + `); + }); + + it("renders plain text if language isn't supported", async () => { + createComponent({ code, language: 'foobar' }); + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[expect.any(TypeError)]]); + + expect(wrapper.element).toMatchInlineSnapshot(` + + + const foo = 1; + + + `); + }); +}); diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js index 60b0b0b566b..9a4dbcc47ff 100644 --- a/spec/frontend/vue_shared/components/code_block_spec.js +++ b/spec/frontend/vue_shared/components/code_block_spec.js @@ -4,41 +4,77 @@ import CodeBlock from '~/vue_shared/components/code_block.vue'; describe('Code Block', () => { let wrapper; - const defaultProps = { - code: 'test-code', - }; + const code = 'test-code'; - const createComponent = (props = {}) => { + const createComponent = (propsData, slots = {}) => { wrapper = shallowMount(CodeBlock, { - propsData: { - ...defaultProps, - ...props, - }, + slots, + propsData, }); }; afterEach(() => { wrapper.destroy(); - wrapper = null; }); - describe('with default props', () => { - beforeEach(() => { - createComponent(); - }); + it('overwrites the default slot', () => { + createComponent({}, { default: 'DEFAULT SLOT' }); - it('renders correctly', () => { - expect(wrapper.element).toMatchSnapshot(); - }); + expect(wrapper.element).toMatchInlineSnapshot(` +
+          DEFAULT SLOT
+        
+ `); }); - describe('with maxHeight set to "200px"', () => { - beforeEach(() => { - createComponent({ maxHeight: '200px' }); - }); + it('renders with empty code prop', () => { + createComponent({}); - it('renders correctly', () => { - expect(wrapper.element).toMatchSnapshot(); - }); + expect(wrapper.element).toMatchInlineSnapshot(` +
+        
+          
+        
+      
+ `); + }); + + it('renders code prop when provided', () => { + createComponent({ code }); + + expect(wrapper.element).toMatchInlineSnapshot(` +
+          
+            test-code
+          
+        
+ `); + }); + + it('sets maxHeight properly when provided', () => { + createComponent({ code, maxHeight: '200px' }); + + expect(wrapper.element).toMatchInlineSnapshot(` +
+          
+            test-code
+          
+        
+ `); }); }); 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 04f63b4bd45..68684004b82 100644 --- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js @@ -66,7 +66,7 @@ describe('Diff Stats Dropdown', () => { createComponent({ files: mockFiles }); }); - it('when no file name provided ', () => { + it('when no file name provided', () => { expect(findChangedFiles().at(0).text()).toContain(i18n.noFileNameAvailable); }); @@ -153,7 +153,7 @@ describe('Diff Stats Dropdown', () => { createComponent({ files: mockFiles }); }); - it('updates the URL ', () => { + it('updates the URL', () => { findChangedFiles().at(0).vm.$emit('click'); expect(window.location.hash).toBe(mockFiles[0].href); findChangedFiles().at(1).vm.$emit('click'); diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js index 2dcd91f737f..6dc018797a6 100644 --- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js +++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js @@ -157,13 +157,13 @@ describe('GlModalVuex', () => { const handler = modalFooterSlotContent.mock.calls[0][0][handlerName]; - expect(wrapper.emitted(handlerName)).toBeFalsy(); + expect(wrapper.emitted(handlerName)).toBeUndefined(); expect(actions.hide).not.toHaveBeenCalled(); handler(); expect(actions.hide).toHaveBeenCalledTimes(1); - expect(wrapper.emitted(handlerName)).toBeTruthy(); + expect(wrapper.emitted(handlerName)).toHaveLength(1); }, ); }); diff --git a/spec/frontend/vue_shared/components/paginated_list_spec.js b/spec/frontend/vue_shared/components/paginated_list_spec.js index 9f819cc4e94..ae9c920ebd2 100644 --- a/spec/frontend/vue_shared/components/paginated_list_spec.js +++ b/spec/frontend/vue_shared/components/paginated_list_spec.js @@ -49,7 +49,7 @@ describe('Pagination links component', () => { }); describe('rendering', () => { - it('it renders the gl-paginated-list', () => { + it('renders the gl-paginated-list', () => { expect(wrapper.find('ul.list-group').exists()).toBe(true); expect(wrapper.findAll('li.list-group-item').length).toBe(2); }); 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 70f4693ae81..fa7fabfaef6 100644 --- a/spec/frontend/vue_shared/components/registry/registry_search_spec.js +++ b/spec/frontend/vue_shared/components/registry/registry_search_spec.js @@ -108,7 +108,7 @@ describe('Registry Search', () => { ]); }); - it('on sort item click emits sorting:changed event ', () => { + it('on sort item click emits sorting:changed event', () => { mountComponent(); findSortingItems().at(1).vm.$emit('click'); diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js index afad9314ace..48530a0261f 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/mock_data.js @@ -56,7 +56,7 @@ export const mockSuggestedColors = { '#013220': 'Dark green', '#6699cc': 'Blue-gray', '#0000ff': 'Blue', - '#e6e6fa': 'Lavendar', + '#e6e6fa': 'Lavender', '#9400d3': 'Dark violet', '#330066': 'Deep violet', '#808080': 'Gray', 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 4fbc907a813..e020d9a557e 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 @@ -110,6 +110,13 @@ describe('Source Viewer component', () => { expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default); }); + it('correctly maps languages starting with uppercase', async () => { + await createComponent({ language: 'Python3' }); + const languageDefinition = await import(`highlight.js/lib/languages/python`); + + expect(hljs.registerLanguage).toHaveBeenCalledWith('python', languageDefinition.default); + }); + it('highlights the first chunk', () => { expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage }); }); @@ -149,7 +156,7 @@ describe('Source Viewer component', () => { it('emits showBlobInteractionZones on the eventHub when chunk appears', () => { findChunks().at(0).vm.$emit('appear'); - expect(eventHub.$emit).toBeCalledWith('showBlobInteractionZones', path); + expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path); }); describe('LineHighlighter', () => { 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 1798ca5ccde..f9d615d4f68 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" >